File size: 4,406 Bytes
b2d4d0e
200e3fe
b2d4d0e
 
 
 
 
 
 
200e3fe
 
 
 
 
 
 
 
 
73c8079
 
 
 
 
 
 
97db435
 
 
 
200e3fe
14c6f28
97db435
 
 
 
 
 
 
 
14c6f28
97db435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73c8079
 
 
 
 
200e3fe
73c8079
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200e3fe
 
73c8079
 
 
 
 
 
 
 
200e3fe
73c8079
 
 
 
200e3fe
73c8079
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2d4d0e
 
 
 
73c8079
 
b2d4d0e
65949d0
73c8079
 
 
 
 
 
 
 
 
65949d0
73c8079
 
b2d4d0e
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import type { ParamSpec } from "@/lib/api";
import InfoTip from "@/components/InfoTip";

type Props = {
  specs: ParamSpec[];
  values: Record<string, unknown>;
  onChange: (next: Record<string, unknown>) => void;
};

function ParamLabel({ id, label, help }: { id: string; label: string; help?: string }) {
  return (
    <span className="inline-flex items-center gap-1.5">
      <label htmlFor={id} className="label-mono">{label}</label>
      {help && <InfoTip text={help} />}
    </span>
  );
}

function renderControl(
  s: ParamSpec,
  values: Record<string, unknown>,
  set: (name: string, v: unknown) => void,
) {
  const id = `param-${s.name}`;
  const current: unknown = values[s.name] ?? s.default;
  if (s.name === "seed") {
    const v = (values[s.name] ?? s.default) as number;
    return (
      <div key={s.name} className="space-y-1.5">
        <ParamLabel id={id} label={s.label} help={s.help} />
        <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
          <input
            id={id}
            aria-label={s.label}
            type="number"
            min={s.min}
            step={s.step ?? 1}
            value={v}
            onChange={(e) => set(s.name, Number(e.target.value))}
            className="field-input !w-40 sm:!w-44 font-mono text-[12px] py-1"
          />
          <button
            type="button"
            onClick={() => set(s.name, -1)}
            className="label-mono hover:text-foreground transition-colors"
          >
            ↻ random
          </button>
          {v === -1 && (
            <span className="label-mono text-muted-foreground">(random per generate)</span>
          )}
        </div>
      </div>
    );
  }
  if (s.type === "float" || s.type === "int") {
    const n = typeof current === "number" ? current : Number(current);
    return (
      <div key={s.name} className="space-y-1.5">
        <div className="flex items-baseline justify-between">
          <ParamLabel id={id} label={s.label} help={s.help} />
          <span className="font-mono text-[12px] text-foreground tracking-wider">
            {Number.isFinite(n) ? n.toFixed(2) : String(current)}
          </span>
        </div>
        <input
          id={id}
          aria-label={s.label}
          type="range"
          min={s.min}
          max={s.max}
          step={s.step ?? 0.01}
          value={Number.isFinite(n) ? n : 0}
          onChange={(e) => set(s.name, Number(e.target.value))}
          className="w-full accent-[hsl(var(--ember))]"
        />
      </div>
    );
  }
  if (s.type === "bool") {
    return (
      <div key={s.name} className="flex items-center justify-between">
        <ParamLabel id={id} label={s.label} help={s.help} />
        <input
          id={id}
          aria-label={s.label}
          type="checkbox"
          checked={!!current}
          onChange={(e) => set(s.name, e.target.checked)}
          className="accent-[hsl(var(--ember))]"
        />
      </div>
    );
  }
  return (
    <div key={s.name} className="space-y-1.5">
      <ParamLabel id={id} label={s.label} help={s.help} />
      <select
        id={id}
        aria-label={s.label}
        value={String(current)}
        onChange={(e) => set(s.name, e.target.value)}
        className="field-input font-mono text-[12px]"
      >
        {(s.choices ?? []).map((c) => (
          <option key={c} value={c}>{c}</option>
        ))}
      </select>
    </div>
  );
}

export default function ParamsPanel({ specs, values, onChange }: Props) {
  function set(name: string, v: unknown) {
    onChange({ ...values, [name]: v });
  }
  const basic = specs.filter((s) => (s.group ?? "basic") === "basic");
  const advanced = specs.filter((s) => s.group === "advanced");
  return (
    <div className="space-y-5">
      {basic.map((s) => renderControl(s, values, set))}
      {advanced.length > 0 && (
        <details className="card-paper p-3 [&_summary::-webkit-details-marker]:hidden">
          <summary className="label-mono cursor-pointer select-none flex items-center gap-2">
            <span className="inline-block transition-transform [details[open]>summary>&]:rotate-90"></span>
            advanced · {advanced.length} params
          </summary>
          <div className="mt-4 space-y-5">
            {advanced.map((s) => renderControl(s, values, set))}
          </div>
        </details>
      )}
    </div>
  );
}