diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx index 12ba855e3..482f25c74 100644 --- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx @@ -20,7 +20,7 @@ import { Tooltip } from "@/components/workspace/tooltip"; import { useAgent } from "@/core/agents"; import { useI18n } from "@/core/i18n/hooks"; import { useNotification } from "@/core/notification/hooks"; -import { useLocalSettings } from "@/core/settings"; +import { useThreadSettings } from "@/core/settings"; import { useThreadStream } from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; @@ -28,7 +28,6 @@ import { cn } from "@/lib/utils"; export default function AgentChatPage() { const { t } = useI18n(); - const [settings, setSettings] = useLocalSettings(); const router = useRouter(); const { agent_name } = useParams<{ @@ -38,6 +37,7 @@ export default function AgentChatPage() { const { agent } = useAgent(agent_name); const { threadId, isNewThread, setIsNewThread } = useThreadChat(); + const [settings, setSettings] = useThreadSettings(threadId); const { showNotification } = useNotification(); const [thread, sendMessage] = useThreadStream({ diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 8805522ad..0cff87d0d 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -19,7 +19,7 @@ import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicato import { Welcome } from "@/components/workspace/welcome"; import { useI18n } from "@/core/i18n/hooks"; import { useNotification } from "@/core/notification/hooks"; -import { useLocalSettings } from "@/core/settings"; +import { useThreadSettings } from "@/core/settings"; import { useThreadStream } from "@/core/threads/hooks"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; @@ -27,9 +27,8 @@ import { cn } from "@/lib/utils"; export default function ChatPage() { const { t } = useI18n(); - const [settings, setSettings] = useLocalSettings(); - const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); + const [settings, setSettings] = useThreadSettings(threadId); useSpecificChatMode(); const { showNotification } = useNotification(); diff --git a/frontend/src/core/settings/hooks.ts b/frontend/src/core/settings/hooks.ts index 47797d489..f5ec0d2a6 100644 --- a/frontend/src/core/settings/hooks.ts +++ b/frontend/src/core/settings/hooks.ts @@ -3,44 +3,62 @@ import { useCallback, useLayoutEffect, useState } from "react"; import { DEFAULT_LOCAL_SETTINGS, getLocalSettings, + getThreadLocalSettings, saveLocalSettings, + saveThreadLocalSettings, type LocalSettings, } from "./local"; -export function useLocalSettings(): [ - LocalSettings, - ( - key: keyof LocalSettings, - value: Partial, - ) => void, -] { - const [mounted, setMounted] = useState(false); +type LocalSettingsSetter = ( + key: keyof LocalSettings, + value: Partial, +) => void; + +function useSettingsState( + getSettings: () => LocalSettings, + saveSettings: (settings: LocalSettings) => void, +): [LocalSettings, LocalSettingsSetter] { const [state, setState] = useState(DEFAULT_LOCAL_SETTINGS); + + const [mounted, setMounted] = useState(false); useLayoutEffect(() => { - if (!mounted) { - setState(getLocalSettings()); - } + setState(getSettings()); setMounted(true); - }, [mounted]); - const setter = useCallback( - ( - key: keyof LocalSettings, - value: Partial, - ) => { + }, [getSettings]); + + const setter = useCallback( + (key, value) => { if (!mounted) return; setState((prev) => { - const newState = { + const newState: LocalSettings = { ...prev, [key]: { ...prev[key], ...value, }, }; - saveLocalSettings(newState); + saveSettings(newState); return newState; }); }, - [mounted], + [mounted, saveSettings], ); + return [state, setter]; } + +export function useLocalSettings(): [LocalSettings, LocalSettingsSetter] { + return useSettingsState(getLocalSettings, saveLocalSettings); +} + +export function useThreadSettings( + threadId: string, +): [LocalSettings, LocalSettingsSetter] { + return useSettingsState( + useCallback(() => getThreadLocalSettings(threadId), [threadId]), + useCallback( + (settings: LocalSettings) => saveThreadLocalSettings(threadId, settings), + [threadId], + ), + ); +} diff --git a/frontend/src/core/settings/local.ts b/frontend/src/core/settings/local.ts index d560e7b8d..562ed7046 100644 --- a/frontend/src/core/settings/local.ts +++ b/frontend/src/core/settings/local.ts @@ -15,6 +15,11 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = { }; const LOCAL_SETTINGS_KEY = "deerflow.local-settings"; +const THREAD_MODEL_KEY_PREFIX = "deerflow.thread-model."; + +function isBrowser(): boolean { + return typeof window !== "undefined"; +} export interface LocalSettings { notification: { @@ -22,8 +27,14 @@ export interface LocalSettings { }; context: Omit< AgentThreadContext, - "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" + | "thread_id" + | "is_plan_mode" + | "thinking_enabled" + | "subagent_enabled" + | "model_name" + | "reasoning_effort" > & { + model_name?: string | undefined; mode: "flash" | "thinking" | "pro" | "ultra" | undefined; reasoning_effort?: "minimal" | "low" | "medium" | "high"; }; @@ -32,35 +43,96 @@ export interface LocalSettings { }; } +function mergeLocalSettings(settings?: Partial): LocalSettings { + return { + ...DEFAULT_LOCAL_SETTINGS, + context: { + ...DEFAULT_LOCAL_SETTINGS.context, + ...settings?.context, + }, + layout: { + ...DEFAULT_LOCAL_SETTINGS.layout, + ...settings?.layout, + }, + notification: { + ...DEFAULT_LOCAL_SETTINGS.notification, + ...settings?.notification, + }, + }; +} + +function getThreadModelStorageKey(threadId: string): string { + return `${THREAD_MODEL_KEY_PREFIX}${threadId}`; +} + +export function getThreadModelName(threadId: string): string | undefined { + if (!isBrowser()) { + return undefined; + } + return localStorage.getItem(getThreadModelStorageKey(threadId)) ?? undefined; +} + +export function saveThreadModelName( + threadId: string, + modelName: string | undefined, +) { + if (!isBrowser()) { + return; + } + const key = getThreadModelStorageKey(threadId); + if (!modelName) { + localStorage.removeItem(key); + return; + } + localStorage.setItem(key, modelName); +} + +function applyThreadModelOverride( + settings: LocalSettings, + threadId?: string, +): LocalSettings { + const threadModelName = threadId ? getThreadModelName(threadId) : undefined; + if (!threadModelName) { + return settings; + } + return { + ...settings, + context: { + ...settings.context, + model_name: threadModelName, + }, + }; +} + export function getLocalSettings(): LocalSettings { - if (typeof window === "undefined") { + if (!isBrowser()) { return DEFAULT_LOCAL_SETTINGS; } const json = localStorage.getItem(LOCAL_SETTINGS_KEY); try { if (json) { - const settings = JSON.parse(json); - const mergedSettings = { - ...DEFAULT_LOCAL_SETTINGS, - context: { - ...DEFAULT_LOCAL_SETTINGS.context, - ...settings.context, - }, - layout: { - ...DEFAULT_LOCAL_SETTINGS.layout, - ...settings.layout, - }, - notification: { - ...DEFAULT_LOCAL_SETTINGS.notification, - ...settings.notification, - }, - }; - return mergedSettings; + const settings = JSON.parse(json) as Partial; + return mergeLocalSettings(settings); } } catch {} return DEFAULT_LOCAL_SETTINGS; } +export function getThreadLocalSettings(threadId: string): LocalSettings { + return applyThreadModelOverride(getLocalSettings(), threadId); +} + export function saveLocalSettings(settings: LocalSettings) { + if (!isBrowser()) { + return; + } localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settings)); } + +export function saveThreadLocalSettings( + threadId: string, + settings: LocalSettings, +) { + saveLocalSettings(settings); + saveThreadModelName(threadId, settings.context.model_name); +}