fix(frontend): persist model selection per thread (#1553)

* fix(frontend): persist model selection per thread

* fix(frontend): apply thread model override on fallback

* refactor(frontend): split thread settings hook

* fix frontend local storage guards
This commit is contained in:
Admire
2026-04-01 23:27:03 +08:00
committed by GitHub
parent 0a379602b8
commit 0eb6550cf4
4 changed files with 133 additions and 44 deletions
+91 -19
View File
@@ -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>): 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<LocalSettings>;
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);
}