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 @@
+
+
+
+
+
+
+
+
+
('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 @@
+
+
@@ -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"
/>
+
+
+
+