Files
deerflow-code/frontend-web/src/core/settings/local.ts
T

227 lines
6.7 KiB
TypeScript

import type { AgentThreadContext } from "../threads";
export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
notification: {
enabled: true,
},
display: {
streamSpeedMeterEnabled: true,
ragModeEnabled: false,
artifactDirectoryModeEnabled: false,
contextCompressionEnabled: false,
citationCleanupWords: [],
fontScale: 1,
},
context: {
model_name: undefined,
mode: undefined,
reasoning_effort: undefined,
writing_mode: false,
},
};
export const LOCAL_SETTINGS_KEY = "deerflow.local-settings";
export const THREAD_MODEL_KEY_PREFIX = "deerflow.thread-model.";
export const THREAD_RAG_MODE_KEY_PREFIX = "deerflow.thread-rag-mode.";
export const LAST_AGENT_ID_KEY = "deerflow.last-agent-id";
function isBrowser(): boolean {
return typeof window !== "undefined";
}
export interface LocalSettings {
notification: {
enabled: boolean;
};
display: {
/**
* Whether to render a small token/s speed meter while a streaming
* AI reply is in flight. Default on; controlled by the user from
* the appearance settings page.
*/
streamSpeedMeterEnabled: boolean;
/**
* Global switch for skill result display adapters. When disabled, chat
* messages render tool results with the legacy generic search UI only.
*/
ragModeEnabled: boolean;
/**
* When enabled, the artifact panel scans every generated output and keeps
* a directory tree visible beside the selected file preview.
*/
artifactDirectoryModeEnabled: boolean;
/**
* Per-user switch for context compression (上下文压缩). When enabled, long
* conversations are summarized transiently before the model call to save
* tokens. Default off; toggled from the chat input box (below 参考文献).
*/
contextCompressionEnabled: boolean;
/**
* Admin-maintained words or short phrases removed from reference snippets
* and dialogs, one entry per line in the settings UI.
*/
citationCleanupWords: string[];
/**
* Global UI font scale multiplier applied to the root <html> font-size.
* Base is 16px (deep-blue UI spec §2.1.2 body text), so `1` === 16px.
* The user picks a size from the appearance settings page; because the
* app sizes text/spacing in rem, the whole UI scales proportionally.
*/
fontScale: number;
/**
* Per-user override for the skill / agent list display mode (card vs list).
* `undefined` = follow the admin-configured system default. Set when the
* user flips the card/list toggle on the skills or agents page.
*/
skillViewMode?: "card" | "list";
agentViewMode?: "card" | "list";
/**
* Layout mode: "full" shows the page sidebar + header; "compact" hides them
* for a cleaner work area. Defaults to "full" (undefined treated as "full").
*/
layoutMode?: "full" | "compact";
};
context: Omit<
AgentThreadContext,
| "thread_id"
| "is_plan_mode"
| "thinking_enabled"
| "subagent_enabled"
| "model_name"
| "reasoning_effort"
// Session-only sidebar switch (core/settings/knowledge-rag.ts) — must not
// round-trip through localStorage.
| "knowledge_rag_enabled"
> & {
model_name?: string | undefined;
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
reasoning_effort?: "minimal" | "low" | "medium" | "high";
};
}
function mergeLocalSettings(settings?: Partial<LocalSettings>): LocalSettings {
const context = {
...DEFAULT_LOCAL_SETTINGS.context,
...settings?.context,
};
// Drop the value persisted by the retired per-thread header toggle; the
// knowledge-RAG switch now lives in the sidebar and is session-only.
delete (context as Record<string, unknown>).knowledge_rag_enabled;
return {
...DEFAULT_LOCAL_SETTINGS,
context,
notification: {
...DEFAULT_LOCAL_SETTINGS.notification,
...settings?.notification,
},
display: {
...DEFAULT_LOCAL_SETTINGS.display,
...settings?.display,
},
};
}
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 getThreadRagModeStorageKey(threadId: string): string {
return `${THREAD_RAG_MODE_KEY_PREFIX}${threadId}`;
}
export function isThreadRagModeLocked(threadId: string): boolean {
if (!isBrowser() || !threadId) {
return false;
}
return localStorage.getItem(getThreadRagModeStorageKey(threadId)) === "true";
}
export function lockThreadRagMode(threadId: string) {
if (!isBrowser() || !threadId) {
return;
}
localStorage.setItem(getThreadRagModeStorageKey(threadId), "true");
}
export function applyThreadModelOverride(
settings: LocalSettings,
threadModelName: string | undefined,
): LocalSettings {
if (!threadModelName) {
return settings;
}
return {
...settings,
context: {
...settings.context,
model_name: threadModelName,
},
};
}
/**
* The id of the agent the user last started a conversation with. New-chat
* landing screens pre-select this agent so the user doesn't have to re-pick it
* every time. Returns ``null`` when nothing was stored yet.
*/
export function getLastAgentId(): string | null {
if (!isBrowser()) {
return null;
}
return localStorage.getItem(LAST_AGENT_ID_KEY) || null;
}
export function saveLastAgentId(agentId: string | null | undefined) {
if (!isBrowser()) {
return;
}
if (!agentId) {
localStorage.removeItem(LAST_AGENT_ID_KEY);
return;
}
localStorage.setItem(LAST_AGENT_ID_KEY, agentId);
}
export function getLocalSettings(): LocalSettings {
if (!isBrowser()) {
return DEFAULT_LOCAL_SETTINGS;
}
const json = localStorage.getItem(LOCAL_SETTINGS_KEY);
try {
if (json) {
const settings = JSON.parse(json) as Partial<LocalSettings>;
return mergeLocalSettings(settings);
}
} catch {}
return DEFAULT_LOCAL_SETTINGS;
}
export function saveLocalSettings(settings: LocalSettings) {
if (!isBrowser()) {
return;
}
localStorage.setItem(LOCAL_SETTINGS_KEY, JSON.stringify(settings));
}