返回 JoyAI-Echo
LlmParamsPage.tsx
根目录 / echo_longvideo / Director_Agent / webui / src / pages / LlmParamsPage.tsx
1 import { useCallback, useEffect, useRef, useState } from "react";
2 import { Link } from "react-router";
3
4 import type { GenerationLlmSettings } from "@/lib/api";
5 import { readLlmParams, writeLlmParams } from "@/lib/llm-params-storage";
6 import { cn } from "@/lib/utils";
7 import { ROUTES } from "@/router/routes";
8
9 type FieldKey = keyof GenerationLlmSettings;
10
11 const FIELDS: {
12 key: FieldKey;
13 label: string;
14 min: number;
15 max: number;
16 step: number;
17 hint: string;
18 }[] = [
19 {
20 key: "temperature",
21 label: "Temperature",
22 min: 0,
23 max: 1.99,
24 step: 0.1,
25 hint: "0–2, leave blank for default",
26 },
27 {
28 key: "top_p",
29 label: "Top P",
30 min: 0,
31 max: 1,
32 step: 0.05,
33 hint: "0–1, leave blank for default",
34 },
35 {
36 key: "top_k",
37 label: "Top K",
38 min: 1,
39 max: 64,
40 step: 1,
41 hint: "1–64, leave blank for default",
42 },
43 ];
44
45 function draftFromSettings(s: GenerationLlmSettings): Record<FieldKey, string> {
46 return {
47 temperature: s.temperature != null ? String(s.temperature) : "",
48 top_p: s.top_p != null ? String(s.top_p) : "",
49 top_k: s.top_k != null ? String(s.top_k) : "",
50 };
51 }
52
53 function draftsEqual(
54 a: Record<FieldKey, string>,
55 b: Record<FieldKey, string>,
56 ): boolean {
57 return (
58 a.temperature === b.temperature &&
59 a.top_p === b.top_p &&
60 a.top_k === b.top_k
61 );
62 }
63
64 function settingsEqual(
65 a: GenerationLlmSettings,
66 b: GenerationLlmSettings,
67 ): boolean {
68 return (
69 a.temperature === b.temperature &&
70 a.top_p === b.top_p &&
71 a.top_k === b.top_k
72 );
73 }
74
75 export function LlmParamsPage() {
76 const [draft, setDraft] = useState(() => draftFromSettings(readLlmParams()));
77 const [saved, setSaved] = useState(false);
78 const savedTimerRef = useRef<number | null>(null);
79
80 const persist = useCallback(() => {
81 const payload: Partial<GenerationLlmSettings> = {};
82 for (const { key } of FIELDS) {
83 const raw = draft[key].trim();
84 payload[key] = raw === "" ? null : Number(raw);
85 }
86 const storedBefore = readLlmParams();
87 const next = writeLlmParams(payload);
88 const draftAfter = draftFromSettings(next);
89 const draftChanged = !draftsEqual(draft, draftAfter);
90 const storageChanged = !settingsEqual(storedBefore, next);
91
92 if (!draftChanged && !storageChanged) {
93 return;
94 }
95
96 if (draftChanged) {
97 setDraft(draftAfter);
98 }
99
100 if (!storageChanged) {
101 return;
102 }
103
104 if (savedTimerRef.current != null) {
105 window.clearTimeout(savedTimerRef.current);
106 }
107 setSaved(true);
108 savedTimerRef.current = window.setTimeout(() => {
109 savedTimerRef.current = null;
110 setSaved(false);
111 }, 1500);
112 }, [draft]);
113
114 const persistRef = useRef(persist);
115 persistRef.current = persist;
116
117 useEffect(() => {
118 const timer = window.setTimeout(() => {
119 persistRef.current();
120 }, 600);
121 return () => window.clearTimeout(timer);
122 }, [draft]);
123
124 useEffect(() => {
125 return () => {
126 if (savedTimerRef.current != null) {
127 window.clearTimeout(savedTimerRef.current);
128 }
129 };
130 }, []);
131
132 return (
133 <div className="flex min-h-screen w-screen flex-col bg-background text-foreground">
134 <header className="flex items-center justify-between border-b px-6 py-4">
135 <div>
136 <h1 className="text-lg font-semibold">LLM Sampling Parameters</h1>
137 <p className="mt-1 text-sm text-muted-foreground">
138 Parameters are stored in this browser and applied to future generation requests.
139 </p>
140 </div>
141 <Link
142 to={ROUTES.home}
143 className="rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
144 >
145 Back to Home
146 </Link>
147 </header>
148
149 <main className="mx-auto w-full max-w-md flex-1 px-6 py-8">
150 <div className="grid gap-4 rounded-xl border border-border/60 bg-card/40 p-4">
151 {FIELDS.map(({ key, label, min, max, step, hint }) => (
152 <label key={key} className="grid gap-1.5">
153 <span className="text-sm font-medium text-foreground">
154 {label}
155 <span className="ml-1 text-xs font-normal text-muted-foreground">
156 ({hint})
157 </span>
158 </span>
159 <input
160 type="number"
161 min={min}
162 max={max}
163 step={step}
164 value={draft[key]}
165 placeholder="Default"
166 onChange={(e) =>
167 setDraft((prev) => ({ ...prev, [key]: e.target.value }))
168 }
169 onBlur={() => persist()}
170 className={cn(
171 "h-9 rounded-lg border border-border/60 bg-background px-3 text-sm tabular-nums outline-none focus:ring-1 focus:ring-foreground/10",
172 )}
173 />
174 </label>
175 ))}
176 <p className="text-xs text-muted-foreground">
177 {saved ? "Saved in this browser" : "Changes save automatically"}
178 </p>
179 </div>
180 </main>
181 </div>
182 );
183 }
184
184 lines Plain Text