From 3307b27a80e9642b2e7391b831c83c747a7d4654 Mon Sep 17 00:00:00 2001 From: Acbox Date: Sat, 11 Apr 2026 19:39:17 +0800 Subject: [PATCH] feat(web): add timezone setup for each bot --- apps/web/src/i18n/locales/en.json | 3 +- apps/web/src/i18n/locales/zh.json | 3 +- .../pages/bots/components/bot-settings.vue | 77 ++++++++++++++++--- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 9d2b5622..57e0751d 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -39,7 +39,8 @@ "no": "No", "details": "Details", "pin": "Pin", - "unpin": "Unpin" + "unpin": "Unpin", + "searchTimezone": "Search timezone" }, "auth": { "welcome": "Welcome Back", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index d66796f3..e8932035 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -39,7 +39,8 @@ "no": "否", "details": "详情", "pin": "置顶", - "unpin": "取消置顶" + "unpin": "取消置顶", + "searchTimezone": "搜索时区" }, "auth": { "welcome": "欢迎回来", diff --git a/apps/web/src/pages/bots/components/bot-settings.vue b/apps/web/src/pages/bots/components/bot-settings.vue index 5132249e..c85d3169 100644 --- a/apps/web/src/pages/bots/components/bot-settings.vue +++ b/apps/web/src/pages/bots/components/bot-settings.vue @@ -212,6 +212,19 @@ /> +
+ + +

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

+
+ @@ -262,10 +275,10 @@
@@ -322,6 +335,7 @@ import { useRouter } from 'vue-router' import { toast } from 'vue-sonner' import { useI18n } from 'vue-i18n' import ConfirmPopover from '@/components/confirm-popover/index.vue' +import TimezoneSelect from '@/components/timezone-select/index.vue' import ModelSelect from './model-select.vue' import ReasoningEffortSelect from './reasoning-effort-select.vue' import { EFFORT_LABELS, EFFORT_OPACITY } from './reasoning-effort' @@ -330,10 +344,11 @@ import MemoryProviderSelect from './memory-provider-select.vue' import TtsModelSelect from './tts-model-select.vue' import BrowserContextSelect from './browser-context-select.vue' import { useQuery, useMutation, useQueryCache } from '@pinia/colada' -import { getBotsByBotIdSettings, putBotsByBotIdSettings, deleteBotsById, getModels, getProviders, getSearchProviders, getMemoryProviders, getSpeechProviders, getSpeechModels, getBrowserContexts, getBotsByBotIdMemoryStatus, postBotsByBotIdMemoryRebuild } from '@memohai/sdk' +import { getBotsById, putBotsById, getBotsByBotIdSettings, putBotsByBotIdSettings, deleteBotsById, getModels, getProviders, getSearchProviders, getMemoryProviders, getSpeechProviders, getSpeechModels, getBrowserContexts, getBotsByBotIdMemoryStatus, postBotsByBotIdMemoryRebuild } from '@memohai/sdk' import type { SettingsSettings } from '@memohai/sdk' import type { Ref } from 'vue' import { resolveApiErrorMessage } from '@/utils/api-error' +import { useUserStore } from '@/store/user' const props = defineProps<{ botId: string @@ -341,8 +356,13 @@ const props = defineProps<{ const { t } = useI18n() const router = useRouter() +const userStore = useUserStore() const botIdRef = computed(() => props.botId) as Ref +const defaultTimezone = computed(() => userStore.userInfo.timezone || 'UTC') +const timezoneEmptyLabel = computed(() => + `${t('bots.timezoneInherited')} (${defaultTimezone.value})`, +) // ---- Data ---- const queryCache = useQueryCache() @@ -356,6 +376,15 @@ const { data: settings } = useQuery({ enabled: () => !!botIdRef.value, }) +const { data: bot } = useQuery({ + key: () => ['bot', botIdRef.value], + query: async () => { + const { data } = await getBotsById({ path: { id: botIdRef.value }, throwOnError: true }) + return data + }, + enabled: () => !!botIdRef.value, +}) + const { data: modelData } = useQuery({ key: ['all-models'], query: async () => { @@ -424,6 +453,21 @@ const { mutateAsync: updateSettings, isLoading } = useMutation({ onSettled: () => queryCache.invalidateQueries({ key: ['bot-settings', botIdRef.value] }), }) +const { mutateAsync: updateBot, isLoading: isUpdatingBot } = useMutation({ + mutation: async (timezone: string) => { + const { data } = await putBotsById({ + path: { id: botIdRef.value }, + body: { timezone }, + throwOnError: true, + }) + return data + }, + onSettled: () => { + queryCache.invalidateQueries({ key: ['bot', botIdRef.value] }) + queryCache.invalidateQueries({ key: ['bots'] }) + }, +}) + const { mutateAsync: deleteBot, isLoading: deleteLoading } = useMutation({ mutation: async () => { await deleteBotsById({ path: { id: botIdRef.value }, throwOnError: true }) @@ -455,6 +499,7 @@ const form = reactive({ memory_provider_id: '', tts_model_id: '', browser_context_id: '', + timezone: '', language: '', reasoning_enabled: false, reasoning_effort: 'medium', @@ -599,10 +644,14 @@ watch(settings, (val) => { } }, { immediate: true }) -const hasChanges = computed(() => { +watch(bot, (val) => { + form.timezone = val?.timezone ?? '' +}, { immediate: true }) + +const hasSettingsChanges = computed(() => { if (!settings.value) return true const s = settings.value - let changed = + return ( form.chat_model_id !== (s.chat_model_id ?? '') || form.title_model_id !== (s.title_model_id ?? '') || form.image_model_id !== (s.image_model_id ?? '') @@ -613,15 +662,25 @@ const hasChanges = computed(() => { || form.language !== (s.language ?? '') || form.reasoning_enabled !== (s.reasoning_enabled ?? false) || form.reasoning_effort !== (s.reasoning_effort || 'medium') - return changed + ) }) +const hasTimezoneChanges = computed(() => form.timezone !== (bot.value?.timezone ?? '')) +const hasChanges = computed(() => hasSettingsChanges.value || hasTimezoneChanges.value) +const saveLoading = computed(() => isLoading.value || isUpdatingBot.value) + async function handleSave() { try { - await updateSettings({ ...form }) + if (hasSettingsChanges.value) { + const { timezone: _timezone, ...settingsPayload } = form + await updateSettings(settingsPayload) + } + if (hasTimezoneChanges.value) { + await updateBot(form.timezone) + } toast.success(t('bots.settings.saveSuccess')) - } catch { - return + } catch (error) { + toast.error(resolveApiErrorMessage(error, t('common.saveFailed'))) } }