@@ -198,7 +198,190 @@ const ErrorViewer = {
198198 </div>
199199 ` ,
200200 setup ( ) {
201+ }
202+ }
201203
204+ const SettingsPage = {
205+ template : `
206+ <div class="max-w-2xl mx-auto p-6">
207+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-8">Settings</h1>
208+
209+ <!-- User Avatar Section -->
210+ <div class="mb-8 p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
211+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">User Avatar</h2>
212+ <div class="flex items-center gap-6">
213+ <label for="userAvatarInput" class="relative group cursor-pointer">
214+ <img
215+ :src="userAvatarUrl"
216+ class="w-20 h-20 rounded-full object-cover border-2 border-gray-200 dark:border-gray-600 shadow-md"
217+ alt="User Avatar"
218+ />
219+ <div class="absolute inset-0 rounded-full bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
220+ <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
221+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
222+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
223+ </svg>
224+ </div>
225+ <input id="userAvatarInput" type="file" class="hidden" accept="image/*" @change="uploadUserAvatar" />
226+ </label>
227+ <div class="flex-1">
228+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
229+ Upload a new image for your avatar
230+ </p>
231+ <div class="flex items-center gap-3">
232+ <label for="userAvatarInput" class="cursor-pointer px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors shadow-sm">
233+ <span>Choose File</span>
234+ </label>
235+ <span v-if="userUploading" class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
236+ <svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
237+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
238+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
239+ </svg>
240+ Uploading...
241+ </span>
242+ <span v-if="userSuccess" class="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
243+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
244+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
245+ </svg>
246+ Uploaded!
247+ </span>
248+ </div>
249+ <p v-if="userError" class="mt-2 text-sm text-red-600 dark:text-red-400">{{ userError }}</p>
250+ </div>
251+ </div>
252+ </div>
253+
254+ <!-- Agent Avatar Section -->
255+ <div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
256+ <h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Agent Avatar</h2>
257+ <div class="flex items-center gap-6">
258+ <label for="agentAvatarInput" class="relative group cursor-pointer">
259+ <img
260+ :src="agentAvatarUrl"
261+ class="w-20 h-20 rounded-full object-cover border-2 border-gray-200 dark:border-gray-600 shadow-md"
262+ alt="Agent Avatar"
263+ />
264+ <div class="absolute inset-0 rounded-full bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
265+ <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
266+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
267+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
268+ </svg>
269+ </div>
270+ <input id="agentAvatarInput" type="file" class="hidden" accept="image/*" @change="uploadAgentAvatar" />
271+ </label>
272+ <div class="flex-1">
273+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
274+ Upload a new image for your Agent's avatar
275+ </p>
276+ <div class="flex items-center gap-3">
277+ <label for="agentAvatarInput" class="cursor-pointer px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors shadow-sm">
278+ <span>Choose File</span>
279+ </label>
280+ <span v-if="agentUploading" class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
281+ <svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
282+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
283+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
284+ </svg>
285+ Uploading...
286+ </span>
287+ <span v-if="agentSuccess" class="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
288+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
289+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
290+ </svg>
291+ Uploaded!
292+ </span>
293+ </div>
294+ <p v-if="agentError" class="mt-2 text-sm text-red-600 dark:text-red-400">{{ agentError }}</p>
295+ </div>
296+ </div>
297+ </div>
298+ </div>
299+ ` ,
300+ setup ( ) {
301+ const ctx = inject ( 'ctx' )
302+
303+ const userAvatarUrl = computed ( ( ) => ctx . getUserAvatar ( ) )
304+ const agentAvatarUrl = computed ( ( ) => ctx . getAgentAvatar ( ) )
305+
306+ const userUploading = ref ( false )
307+ const userSuccess = ref ( false )
308+ const userError = ref ( '' )
309+ const agentUploading = ref ( false )
310+ const agentSuccess = ref ( false )
311+ const agentError = ref ( '' )
312+
313+ async function uploadUserAvatar ( event ) {
314+ const file = event . target . files ?. [ 0 ]
315+ if ( ! file ) return
316+
317+ userUploading . value = true
318+ userSuccess . value = false
319+ userError . value = ''
320+
321+ try {
322+ const formData = new FormData ( )
323+ formData . append ( 'file' , file )
324+
325+ const response = await ctx . postForm ( '/user/avatar' , { body : formData } )
326+ const result = await response . json ( )
327+
328+ if ( response . ok && result . success ) {
329+ userSuccess . value = true
330+ ctx . incCacheBreaker ( )
331+ setTimeout ( ( ) => { userSuccess . value = false } , 3000 )
332+ } else {
333+ userError . value = result . message || 'Upload failed'
334+ }
335+ } catch ( e ) {
336+ userError . value = e . message || 'Upload failed'
337+ } finally {
338+ userUploading . value = false
339+ event . target . value = ''
340+ }
341+ }
342+
343+ async function uploadAgentAvatar ( event ) {
344+ const file = event . target . files ?. [ 0 ]
345+ if ( ! file ) return
346+
347+ agentUploading . value = true
348+ agentSuccess . value = false
349+ agentError . value = ''
350+
351+ try {
352+ const formData = new FormData ( )
353+ formData . append ( 'file' , file )
354+
355+ const response = await ctx . postForm ( '/agents/avatar' , { body : formData } )
356+ const result = await response . json ( )
357+
358+ if ( response . ok && result . success ) {
359+ agentSuccess . value = true
360+ ctx . incCacheBreaker ( )
361+ setTimeout ( ( ) => { agentSuccess . value = false } , 3000 )
362+ } else {
363+ agentError . value = result . message || 'Upload failed'
364+ }
365+ } catch ( e ) {
366+ agentError . value = e . message || 'Upload failed'
367+ } finally {
368+ agentUploading . value = false
369+ event . target . value = ''
370+ }
371+ }
372+
373+ return {
374+ userAvatarUrl,
375+ agentAvatarUrl,
376+ userUploading,
377+ userSuccess,
378+ userError,
379+ agentUploading,
380+ agentSuccess,
381+ agentError,
382+ uploadUserAvatar,
383+ uploadAgentAvatar,
384+ }
202385 }
203386}
204387
@@ -210,6 +393,11 @@ export default {
210393 Avatar,
211394 SignIn,
212395 ErrorViewer,
396+ SettingsPage,
213397 } )
398+
399+ ctx . routes . push ( ...[
400+ { path : '/settings' , component : SettingsPage } ,
401+ ] )
214402 }
215403}
0 commit comments