diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 10e0cca2..720d59b7 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -75,6 +75,8 @@ "userID": "User ID", "displayName": "Display Name", "avatarUrl": "Avatar URL", + "timezone": "Timezone", + "timezonePlaceholder": "e.g. America/New_York", "saveProfile": "Save Profile", "profileUpdated": "Profile updated", "profileUpdateFailed": "Failed to update profile", @@ -511,10 +513,18 @@ "displayNamePlaceholder": "Give your bot a name", "avatarUrl": "Avatar URL", "avatarUrlPlaceholder": "Enter avatar image URL", + "timezone": "Timezone", + "timezonePlaceholder": "e.g. America/New_York", + "timezoneInherited": "Inherited from user/system", + "timezoneInheritedHint": "Leave empty to inherit the user's timezone, or fall back to the system timezone.", "editAvatar": "Edit Avatar", "editAvatarDescription": "Set the image URL for the bot avatar.", + "editTimezone": "Edit Timezone", + "editTimezoneDescription": "Set a timezone override for this bot.", "avatarUpdateSuccess": "Avatar updated", "avatarUpdateFailed": "Failed to update avatar", + "timezoneUpdateSuccess": "Timezone updated", + "timezoneUpdateFailed": "Failed to update timezone", "typePlaceholder": "Select bot type", "types": { "personal": "Personal", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index 9a320c88..5c9e1b7b 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -75,6 +75,8 @@ "userID": "用户 ID", "displayName": "显示名称", "avatarUrl": "头像链接", + "timezone": "时区", + "timezonePlaceholder": "例如 Asia/Tokyo", "saveProfile": "保存资料", "profileUpdated": "用户资料已更新", "profileUpdateFailed": "用户资料更新失败", @@ -400,7 +402,7 @@ "locale": "语言区域", "localePlaceholder": "例如 zh-CN", "timezoneId": "时区", - "timezonePlaceholder": "例如 Asia/Shanghai", + "timezonePlaceholder": "例如 Asia/Tokyo", "ignoreHTTPSErrors": "忽略 HTTPS 错误", "core": "浏览器内核", "chromium": "Chromium", @@ -507,10 +509,18 @@ "displayNamePlaceholder": "给你的 Bot 起个名字", "avatarUrl": "头像链接", "avatarUrlPlaceholder": "输入头像图片地址", + "timezone": "时区", + "timezonePlaceholder": "例如 Asia/Shanghai", + "timezoneInherited": "继承用户或系统时区", + "timezoneInheritedHint": "留空则继承用户时区,若用户未设置则回退到系统时区。", "editAvatar": "编辑头像", "editAvatarDescription": "设置 Bot 头像图片的链接地址", + "editTimezone": "编辑时区", + "editTimezoneDescription": "为这个 Bot 设置独立时区。", "avatarUpdateSuccess": "头像已更新", "avatarUpdateFailed": "更新头像失败", + "timezoneUpdateSuccess": "时区已更新", + "timezoneUpdateFailed": "更新时区失败", "typePlaceholder": "选择 Bot 类型", "types": { "personal": "个人", diff --git a/apps/web/src/pages/bots/components/create-bot.vue b/apps/web/src/pages/bots/components/create-bot.vue index 1164b05e..78fda3e7 100644 --- a/apps/web/src/pages/bots/components/create-bot.vue +++ b/apps/web/src/pages/bots/components/create-bot.vue @@ -57,6 +57,42 @@ + + + + + + + + +
{{ $t('bots.createBotWaitHint') }}
@@ -98,6 +134,12 @@ import { FormItem, Separator, Label, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, Spinner, } from '@memohai/ui' import { useForm } from 'vee-validate' @@ -108,6 +150,7 @@ import { useMutation, useQueryCache } from '@pinia/colada' import { postBotsMutation, getBotsQueryKey } from '@memohai/sdk/colada' import { useI18n } from 'vue-i18n' import { useDialogMutation } from '@/composables/useDialogMutation' +import { emptyTimezoneValue, timezones } from '@/utils/timezones' const open = defineModel('open', { default: false }) const { t } = useI18n() @@ -116,6 +159,7 @@ const { run } = useDialogMutation() const formSchema = toTypedSchema(z.object({ display_name: z.string().min(1), avatar_url: z.string().optional(), + timezone: z.string().optional(), })) const form = useForm({ @@ -123,6 +167,7 @@ const form = useForm({ initialValues: { display_name: '', avatar_url: '', + timezone: '', }, }) @@ -138,6 +183,7 @@ watch(open, (val) => { values: { display_name: '', avatar_url: '', + timezone: '', }, }) } else { @@ -151,6 +197,7 @@ const handleSubmit = form.handleSubmit(async (values) => { body: { display_name: values.display_name, avatar_url: values.avatar_url || undefined, + timezone: values.timezone || undefined, is_active: true, }, }), diff --git a/apps/web/src/pages/bots/detail.vue b/apps/web/src/pages/bots/detail.vue index 48c1c5da..2bc577b0 100644 --- a/apps/web/src/pages/bots/detail.vue +++ b/apps/web/src/pages/bots/detail.vue @@ -93,6 +93,22 @@ {{ botTypeLabel }} +
+ {{ $t('bots.timezone') }}: {{ bot.timezone || $t('bots.timezoneInherited') }} + +
@@ -193,6 +209,67 @@ + + + + + {{ $t('bots.editTimezone') }} + + {{ $t('bots.editTimezoneDescription') }} + + +
+ +

+ {{ $t('bots.timezoneInheritedHint') }} +

+
+ + + + + + +
+
@@ -211,6 +288,12 @@ import { DialogHeader, DialogTitle, Input, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, Separator, Spinner, ScrollArea, @@ -255,6 +338,7 @@ import { useAvatarInitials } from '@/composables/useAvatarInitials' import { useSyncedQueryParam } from '@/composables/useSyncedQueryParam' import { useBotStatusMeta } from '@/composables/useBotStatusMeta' import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue' +import { emptyTimezoneValue, timezones } from '@/utils/timezones' type BotCheck = BotsBotCheck type BotContainerInfo = HandlersGetContainerResponse type BotContainerSnapshot = HandlersListSnapshotsResponse extends { snapshots?: (infer T)[] } ? T : never @@ -344,9 +428,12 @@ watch(bot, (val) => { const activeTab = useSyncedQueryParam('tab', 'overview') const avatarDialogOpen = ref(false) const avatarUrlDraft = ref('') +const timezoneDialogOpen = ref(false) +const timezoneDraft = ref('') const avatarFallback = useAvatarInitials(() => bot.value?.display_name || botId.value || '') const isSavingBotName = computed(() => updateBotLoading.value) const avatarSaving = computed(() => updateBotLoading.value) +const timezoneSaving = computed(() => updateBotLoading.value) const canConfirmAvatar = computed(() => { if (!bot.value) return false const next = avatarUrlDraft.value.trim() @@ -359,6 +446,10 @@ const canConfirmBotName = computed(() => { if (!nextName) return false return nextName !== (bot.value.display_name || '').trim() }) +const canConfirmTimezone = computed(() => { + if (!bot.value) return false + return timezoneDraft.value.trim() !== (bot.value.timezone || '').trim() +}) const { hasIssue, isPending: botLifecyclePending, @@ -412,6 +503,12 @@ function handleEditAvatar() { avatarDialogOpen.value = true } +function handleEditTimezone() { + if (!bot.value || botLifecyclePending.value) return + timezoneDraft.value = bot.value.timezone || '' + timezoneDialogOpen.value = true +} + async function handleConfirmAvatar() { if (!bot.value || !canConfirmAvatar.value || avatarSaving.value) return const nextUrl = avatarUrlDraft.value.trim() @@ -427,6 +524,21 @@ async function handleConfirmAvatar() { } } +async function handleConfirmTimezone() { + if (!bot.value || !canConfirmTimezone.value || timezoneSaving.value) return + const nextTimezone = timezoneDraft.value.trim() + try { + await updateBot({ + id: bot.value.id as string, + timezone: nextTimezone, + }) + timezoneDialogOpen.value = false + toast.success(t('bots.timezoneUpdateSuccess')) + } catch (error) { + toast.error(resolveErrorMessage(error, t('bots.timezoneUpdateFailed'))) + } +} + function handleStartEditBotName() { if (!bot.value) return isEditingBotName.value = true @@ -517,4 +629,4 @@ async function loadSnapshots() { } } - \ No newline at end of file + diff --git a/apps/web/src/pages/browser-contexts/components/context-setting.vue b/apps/web/src/pages/browser-contexts/components/context-setting.vue index 21568d12..13dd68fe 100644 --- a/apps/web/src/pages/browser-contexts/components/context-setting.vue +++ b/apps/web/src/pages/browser-contexts/components/context-setting.vue @@ -155,11 +155,28 @@ - + @@ -232,6 +249,12 @@ import { Label, Separator, Button, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, Switch, } from '@memohai/ui' import { toTypedSchema } from '@vee-validate/zod' @@ -247,6 +270,7 @@ import { toast } from 'vue-sonner' import { useDialogMutation } from '@/composables/useDialogMutation' import ConfirmPopover from '@/components/confirm-popover/index.vue' import { resolveApiErrorMessage } from '@/utils/api-error' +import { emptyTimezoneValue, timezones } from '@/utils/timezones' const { t } = useI18n() const { run } = useDialogMutation() diff --git a/apps/web/src/pages/login/index.vue b/apps/web/src/pages/login/index.vue index d0d43374..1bb373e6 100644 --- a/apps/web/src/pages/login/index.vue +++ b/apps/web/src/pages/login/index.vue @@ -187,6 +187,7 @@ const login = form.handleSubmit(async (values) => { displayName: data.display_name ?? '', role: data.role ?? '', avatarUrl: data.avatar_url ?? '', + timezone: data.timezone ?? 'UTC', }, data.access_token) } else { throw new Error(t('auth.loginFailed')) diff --git a/apps/web/src/pages/settings/components/profile-section.vue b/apps/web/src/pages/settings/components/profile-section.vue index aca46952..0d8d0abf 100644 --- a/apps/web/src/pages/settings/components/profile-section.vue +++ b/apps/web/src/pages/settings/components/profile-section.vue @@ -46,6 +46,32 @@ @update:model-value="onAvatarUrlChange" /> +
+ + +