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')))
}
}