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:
parent
0a379602b8
commit
0eb6550cf4
|
|
@ -20,7 +20,7 @@ import { Tooltip } from "@/components/workspace/tooltip";
|
||||||
import { useAgent } from "@/core/agents";
|
import { useAgent } from "@/core/agents";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useThreadSettings } from "@/core/settings";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
|
|
@ -28,7 +28,6 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function AgentChatPage() {
|
export default function AgentChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [settings, setSettings] = useLocalSettings();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { agent_name } = useParams<{
|
const { agent_name } = useParams<{
|
||||||
|
|
@ -38,6 +37,7 @@ export default function AgentChatPage() {
|
||||||
const { agent } = useAgent(agent_name);
|
const { agent } = useAgent(agent_name);
|
||||||
|
|
||||||
const { threadId, isNewThread, setIsNewThread } = useThreadChat();
|
const { threadId, isNewThread, setIsNewThread } = useThreadChat();
|
||||||
|
const [settings, setSettings] = useThreadSettings(threadId);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const [thread, sendMessage] = useThreadStream({
|
const [thread, sendMessage] = useThreadStream({
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicato
|
||||||
import { Welcome } from "@/components/workspace/welcome";
|
import { Welcome } from "@/components/workspace/welcome";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useThreadSettings } from "@/core/settings";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
|
|
@ -27,9 +27,8 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [settings, setSettings] = useLocalSettings();
|
|
||||||
|
|
||||||
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
||||||
|
const [settings, setSettings] = useThreadSettings(threadId);
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
|
||||||
|
|
@ -3,44 +3,62 @@ import { useCallback, useLayoutEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCAL_SETTINGS,
|
DEFAULT_LOCAL_SETTINGS,
|
||||||
getLocalSettings,
|
getLocalSettings,
|
||||||
|
getThreadLocalSettings,
|
||||||
saveLocalSettings,
|
saveLocalSettings,
|
||||||
|
saveThreadLocalSettings,
|
||||||
type LocalSettings,
|
type LocalSettings,
|
||||||
} from "./local";
|
} from "./local";
|
||||||
|
|
||||||
export function useLocalSettings(): [
|
type LocalSettingsSetter = (
|
||||||
LocalSettings,
|
key: keyof LocalSettings,
|
||||||
(
|
value: Partial<LocalSettings[keyof LocalSettings]>,
|
||||||
key: keyof LocalSettings,
|
) => void;
|
||||||
value: Partial<LocalSettings[keyof LocalSettings]>,
|
|
||||||
) => void,
|
function useSettingsState(
|
||||||
] {
|
getSettings: () => LocalSettings,
|
||||||
const [mounted, setMounted] = useState(false);
|
saveSettings: (settings: LocalSettings) => void,
|
||||||
|
): [LocalSettings, LocalSettingsSetter] {
|
||||||
const [state, setState] = useState<LocalSettings>(DEFAULT_LOCAL_SETTINGS);
|
const [state, setState] = useState<LocalSettings>(DEFAULT_LOCAL_SETTINGS);
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!mounted) {
|
setState(getSettings());
|
||||||
setState(getLocalSettings());
|
|
||||||
}
|
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, [mounted]);
|
}, [getSettings]);
|
||||||
const setter = useCallback(
|
|
||||||
(
|
const setter = useCallback<LocalSettingsSetter>(
|
||||||
key: keyof LocalSettings,
|
(key, value) => {
|
||||||
value: Partial<LocalSettings[keyof LocalSettings]>,
|
|
||||||
) => {
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const newState = {
|
const newState: LocalSettings = {
|
||||||
...prev,
|
...prev,
|
||||||
[key]: {
|
[key]: {
|
||||||
...prev[key],
|
...prev[key],
|
||||||
...value,
|
...value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
saveLocalSettings(newState);
|
saveSettings(newState);
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[mounted],
|
[mounted, saveSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [state, setter];
|
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],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
|
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 {
|
export interface LocalSettings {
|
||||||
notification: {
|
notification: {
|
||||||
|
|
@ -22,8 +27,14 @@ export interface LocalSettings {
|
||||||
};
|
};
|
||||||
context: Omit<
|
context: Omit<
|
||||||
AgentThreadContext,
|
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;
|
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
||||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
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 {
|
export function getLocalSettings(): LocalSettings {
|
||||||
if (typeof window === "undefined") {
|
if (!isBrowser()) {
|
||||||
return DEFAULT_LOCAL_SETTINGS;
|
return DEFAULT_LOCAL_SETTINGS;
|
||||||
}
|
}
|
||||||
const json = localStorage.getItem(LOCAL_SETTINGS_KEY);
|
const json = localStorage.getItem(LOCAL_SETTINGS_KEY);
|
||||||
try {
|
try {
|
||||||
if (json) {
|
if (json) {
|
||||||
const settings = JSON.parse(json);
|
const settings = JSON.parse(json) as Partial<LocalSettings>;
|
||||||
const mergedSettings = {
|
return mergeLocalSettings(settings);
|
||||||
...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;
|
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return DEFAULT_LOCAL_SETTINGS;
|
return DEFAULT_LOCAL_SETTINGS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getThreadLocalSettings(threadId: string): LocalSettings {
|
||||||
|
return applyThreadModelOverride(getLocalSettings(), threadId);
|
||||||
|
}
|
||||||
|
|
||||||
export function saveLocalSettings(settings: LocalSettings) {
|
export function saveLocalSettings(settings: LocalSettings) {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settings));
|
localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function saveThreadLocalSettings(
|
||||||
|
threadId: string,
|
||||||
|
settings: LocalSettings,
|
||||||
|
) {
|
||||||
|
saveLocalSettings(settings);
|
||||||
|
saveThreadModelName(threadId, settings.context.model_name);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue