techfreakworm commited on
Commit
73c8079
·
unverified ·
1 Parent(s): dd2ec5b

feat(web): ParamsPanel splits params into basic + advanced disclosure

Browse files
web/src/components/ParamsPanel.tsx CHANGED
@@ -6,78 +6,97 @@ type Props = {
6
  onChange: (next: Record<string, unknown>) => void;
7
  };
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  export default function ParamsPanel({ specs, values, onChange }: Props) {
10
  function set(name: string, v: unknown) {
11
  onChange({ ...values, [name]: v });
12
  }
 
 
13
  return (
14
  <div className="space-y-5">
15
- {specs.map((s) => {
16
- const id = `param-${s.name}`;
17
- const current: unknown = values[s.name] ?? s.default;
18
- if (s.type === "float" || s.type === "int") {
19
- const n = typeof current === "number" ? current : Number(current);
20
- return (
21
- <div key={s.name} className="space-y-1.5">
22
- <div className="flex items-baseline justify-between">
23
- <label htmlFor={id} className="label-mono">{s.label}</label>
24
- <span className="font-mono text-[12px] text-foreground tracking-wider">
25
- {Number.isFinite(n) ? n.toFixed(2) : String(current)}
26
- </span>
27
- </div>
28
- <input
29
- id={id}
30
- aria-label={s.label}
31
- type="range"
32
- min={s.min}
33
- max={s.max}
34
- step={s.step ?? 0.01}
35
- value={Number.isFinite(n) ? n : 0}
36
- onChange={(e) => set(s.name, Number(e.target.value))}
37
- className="w-full accent-[hsl(var(--ember))]"
38
- />
39
- {s.help && (
40
- <p className="text-[11px] text-muted-foreground/80 italic">{s.help}</p>
41
- )}
42
- </div>
43
- );
44
- }
45
- if (s.type === "bool") {
46
- return (
47
- <label
48
- key={s.name}
49
- htmlFor={id}
50
- className="flex items-center justify-between cursor-pointer"
51
- >
52
- <span className="label-mono">{s.label}</span>
53
- <input
54
- id={id}
55
- aria-label={s.label}
56
- type="checkbox"
57
- checked={!!current}
58
- onChange={(e) => set(s.name, e.target.checked)}
59
- className="accent-[hsl(var(--ember))]"
60
- />
61
- </label>
62
- );
63
- }
64
- return (
65
- <div key={s.name} className="space-y-1.5">
66
- <label htmlFor={id} className="label-mono block">{s.label}</label>
67
- <select
68
- id={id}
69
- aria-label={s.label}
70
- value={String(current)}
71
- onChange={(e) => set(s.name, e.target.value)}
72
- className="field-input font-mono text-[12px]"
73
- >
74
- {(s.choices ?? []).map((c) => (
75
- <option key={c} value={c}>{c}</option>
76
- ))}
77
- </select>
78
  </div>
79
- );
80
- })}
81
  </div>
82
  );
83
  }
 
6
  onChange: (next: Record<string, unknown>) => void;
7
  };
8
 
9
+ function renderControl(
10
+ s: ParamSpec,
11
+ values: Record<string, unknown>,
12
+ set: (name: string, v: unknown) => void,
13
+ ) {
14
+ const id = `param-${s.name}`;
15
+ const current: unknown = values[s.name] ?? s.default;
16
+ if (s.type === "float" || s.type === "int") {
17
+ const n = typeof current === "number" ? current : Number(current);
18
+ return (
19
+ <div key={s.name} className="space-y-1.5">
20
+ <div className="flex items-baseline justify-between">
21
+ <label htmlFor={id} className="label-mono">{s.label}</label>
22
+ <span className="font-mono text-[12px] text-foreground tracking-wider">
23
+ {Number.isFinite(n) ? n.toFixed(2) : String(current)}
24
+ </span>
25
+ </div>
26
+ <input
27
+ id={id}
28
+ aria-label={s.label}
29
+ type="range"
30
+ min={s.min}
31
+ max={s.max}
32
+ step={s.step ?? 0.01}
33
+ value={Number.isFinite(n) ? n : 0}
34
+ onChange={(e) => set(s.name, Number(e.target.value))}
35
+ className="w-full accent-[hsl(var(--ember))]"
36
+ />
37
+ {s.help && (
38
+ <p className="text-[11px] text-muted-foreground/80 italic">{s.help}</p>
39
+ )}
40
+ </div>
41
+ );
42
+ }
43
+ if (s.type === "bool") {
44
+ return (
45
+ <label
46
+ key={s.name}
47
+ htmlFor={id}
48
+ className="flex items-center justify-between cursor-pointer"
49
+ >
50
+ <span className="label-mono">{s.label}</span>
51
+ <input
52
+ id={id}
53
+ aria-label={s.label}
54
+ type="checkbox"
55
+ checked={!!current}
56
+ onChange={(e) => set(s.name, e.target.checked)}
57
+ className="accent-[hsl(var(--ember))]"
58
+ />
59
+ </label>
60
+ );
61
+ }
62
+ return (
63
+ <div key={s.name} className="space-y-1.5">
64
+ <label htmlFor={id} className="label-mono block">{s.label}</label>
65
+ <select
66
+ id={id}
67
+ aria-label={s.label}
68
+ value={String(current)}
69
+ onChange={(e) => set(s.name, e.target.value)}
70
+ className="field-input font-mono text-[12px]"
71
+ >
72
+ {(s.choices ?? []).map((c) => (
73
+ <option key={c} value={c}>{c}</option>
74
+ ))}
75
+ </select>
76
+ </div>
77
+ );
78
+ }
79
+
80
  export default function ParamsPanel({ specs, values, onChange }: Props) {
81
  function set(name: string, v: unknown) {
82
  onChange({ ...values, [name]: v });
83
  }
84
+ const basic = specs.filter((s) => (s.group ?? "basic") === "basic");
85
+ const advanced = specs.filter((s) => s.group === "advanced");
86
  return (
87
  <div className="space-y-5">
88
+ {basic.map((s) => renderControl(s, values, set))}
89
+ {advanced.length > 0 && (
90
+ <details className="card-paper p-3 [&_summary::-webkit-details-marker]:hidden">
91
+ <summary className="label-mono cursor-pointer select-none flex items-center gap-2">
92
+ <span className="inline-block transition-transform [details[open]>summary>&]:rotate-90">▸</span>
93
+ advanced · {advanced.length} params
94
+ </summary>
95
+ <div className="mt-4 space-y-5">
96
+ {advanced.map((s) => renderControl(s, values, set))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
+ </details>
99
+ )}
100
  </div>
101
  );
102
  }
web/src/lib/api.ts CHANGED
@@ -10,6 +10,7 @@ export type ParamSpec = {
10
  step?: number;
11
  choices?: string[];
12
  help?: string;
 
13
  };
14
 
15
  export type ModelInfo = {
 
10
  step?: number;
11
  choices?: string[];
12
  help?: string;
13
+ group?: "basic" | "advanced";
14
  };
15
 
16
  export type ModelInfo = {
web/src/test/ParamsPanel.test.tsx CHANGED
@@ -24,3 +24,37 @@ describe("ParamsPanel", () => {
24
  expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ exaggeration: 1.2 }));
25
  });
26
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ exaggeration: 1.2 }));
25
  });
26
  });
27
+
28
+ const specsMixed: ParamSpec[] = [
29
+ { name: "temperature", label: "Temperature", type: "float", default: 0.8, min: 0.1, max: 1.5, step: 0.05, group: "basic" },
30
+ { name: "seed", label: "Seed", type: "int", default: -1, min: -1, step: 1, group: "advanced" },
31
+ { name: "top_p", label: "Top p", type: "float", default: 1.0, min: 0, max: 1, step: 0.01, group: "advanced" },
32
+ ];
33
+
34
+ describe("ParamsPanel groups", () => {
35
+ it("renders basic params and a closed advanced disclosure by default", () => {
36
+ render(<ParamsPanel specs={specsMixed} values={{}} onChange={() => {}} />);
37
+ expect(screen.getByLabelText(/temperature/i)).toBeInTheDocument();
38
+ // advanced is in the DOM but not visible until <details> opens
39
+ const seed = screen.getByLabelText(/^seed$/i) as HTMLInputElement;
40
+ const detailsAncestor = seed.closest("details");
41
+ expect(detailsAncestor).not.toBeNull();
42
+ expect(detailsAncestor!.open).toBe(false);
43
+ });
44
+
45
+ it("opens disclosure on summary click and shows advanced params", () => {
46
+ render(<ParamsPanel specs={specsMixed} values={{}} onChange={() => {}} />);
47
+ const summary = screen.getByText(/advanced/i);
48
+ fireEvent.click(summary);
49
+ const seed = screen.getByLabelText(/^seed$/i) as HTMLInputElement;
50
+ expect(seed.closest("details")!.open).toBe(true);
51
+ });
52
+
53
+ it("propagates onChange from advanced params", () => {
54
+ const onChange = vi.fn();
55
+ render(<ParamsPanel specs={specsMixed} values={{}} onChange={onChange} />);
56
+ fireEvent.click(screen.getByText(/advanced/i));
57
+ fireEvent.change(screen.getByLabelText(/^top p$/i), { target: { value: "0.6" } });
58
+ expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ top_p: 0.6 }));
59
+ });
60
+ });