@darkrishabh/bench-ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +333 -0
  2. package/dist/cli/app.d.ts +11 -0
  3. package/dist/cli/app.d.ts.map +1 -0
  4. package/dist/cli/app.js +48 -0
  5. package/dist/cli/app.js.map +1 -0
  6. package/dist/cli/components/DiffView.d.ts +5 -0
  7. package/dist/cli/components/DiffView.d.ts.map +1 -0
  8. package/dist/cli/components/DiffView.js +14 -0
  9. package/dist/cli/components/DiffView.js.map +1 -0
  10. package/dist/cli/components/EvalView.d.ts +6 -0
  11. package/dist/cli/components/EvalView.d.ts.map +1 -0
  12. package/dist/cli/components/EvalView.js +82 -0
  13. package/dist/cli/components/EvalView.js.map +1 -0
  14. package/dist/cli/components/Spinner.d.ts +4 -0
  15. package/dist/cli/components/Spinner.d.ts.map +1 -0
  16. package/dist/cli/components/Spinner.js +15 -0
  17. package/dist/cli/components/Spinner.js.map +1 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +117 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/run-command.d.ts +11 -0
  23. package/dist/cli/run-command.d.ts.map +1 -0
  24. package/dist/cli/run-command.js +119 -0
  25. package/dist/cli/run-command.js.map +1 -0
  26. package/dist/engine/cost.d.ts +3 -0
  27. package/dist/engine/cost.d.ts.map +1 -0
  28. package/dist/engine/cost.js +52 -0
  29. package/dist/engine/cost.js.map +1 -0
  30. package/dist/engine/diff.d.ts +6 -0
  31. package/dist/engine/diff.d.ts.map +1 -0
  32. package/dist/engine/diff.js +43 -0
  33. package/dist/engine/diff.js.map +1 -0
  34. package/dist/engine/eval.d.ts +14 -0
  35. package/dist/engine/eval.d.ts.map +1 -0
  36. package/dist/engine/eval.js +194 -0
  37. package/dist/engine/eval.js.map +1 -0
  38. package/dist/engine/index.d.ts +15 -0
  39. package/dist/engine/index.d.ts.map +1 -0
  40. package/dist/engine/index.js +10 -0
  41. package/dist/engine/index.js.map +1 -0
  42. package/dist/engine/providers/base.d.ts +7 -0
  43. package/dist/engine/providers/base.d.ts.map +1 -0
  44. package/dist/engine/providers/base.js +2 -0
  45. package/dist/engine/providers/base.js.map +1 -0
  46. package/dist/engine/providers/claude.d.ts +15 -0
  47. package/dist/engine/providers/claude.d.ts.map +1 -0
  48. package/dist/engine/providers/claude.js +53 -0
  49. package/dist/engine/providers/claude.js.map +1 -0
  50. package/dist/engine/providers/minimax.d.ts +16 -0
  51. package/dist/engine/providers/minimax.d.ts.map +1 -0
  52. package/dist/engine/providers/minimax.js +67 -0
  53. package/dist/engine/providers/minimax.js.map +1 -0
  54. package/dist/engine/providers/ollama.d.ts +14 -0
  55. package/dist/engine/providers/ollama.d.ts.map +1 -0
  56. package/dist/engine/providers/ollama.js +60 -0
  57. package/dist/engine/providers/ollama.js.map +1 -0
  58. package/dist/engine/providers/openai-compatible.d.ts +19 -0
  59. package/dist/engine/providers/openai-compatible.d.ts.map +1 -0
  60. package/dist/engine/providers/openai-compatible.js +109 -0
  61. package/dist/engine/providers/openai-compatible.js.map +1 -0
  62. package/dist/engine/providers/subprocess.d.ts +55 -0
  63. package/dist/engine/providers/subprocess.d.ts.map +1 -0
  64. package/dist/engine/providers/subprocess.js +111 -0
  65. package/dist/engine/providers/subprocess.js.map +1 -0
  66. package/dist/engine/suite-loader.d.ts +11 -0
  67. package/dist/engine/suite-loader.d.ts.map +1 -0
  68. package/dist/engine/suite-loader.js +75 -0
  69. package/dist/engine/suite-loader.js.map +1 -0
  70. package/dist/engine/types.d.ts +104 -0
  71. package/dist/engine/types.d.ts.map +1 -0
  72. package/dist/engine/types.js +2 -0
  73. package/dist/engine/types.js.map +1 -0
  74. package/next-env.d.ts +6 -0
  75. package/next.config.ts +26 -0
  76. package/package.json +72 -0
  77. package/public/icon.svg +14 -0
  78. package/src/app/api/diff/route.ts +135 -0
  79. package/src/app/api/models/route.ts +96 -0
  80. package/src/app/api/suite/route.ts +314 -0
  81. package/src/app/globals.css +215 -0
  82. package/src/app/icon.svg +14 -0
  83. package/src/app/layout.tsx +44 -0
  84. package/src/app/opengraph-image.tsx +73 -0
  85. package/src/app/page.tsx +952 -0
  86. package/src/app/suite/layout.tsx +12 -0
  87. package/src/app/suite/page.tsx +206 -0
  88. package/src/app/twitter-image.tsx +1 -0
  89. package/src/components/BenchAiLogo.tsx +38 -0
  90. package/src/components/ComparePanel.tsx +643 -0
  91. package/src/components/ConfigPanel.tsx +809 -0
  92. package/src/components/MarkdownOutput.tsx +16 -0
  93. package/src/components/ModelResponseCard.tsx +313 -0
  94. package/src/components/QuickComparisonBar.tsx +184 -0
  95. package/src/components/ResponsesLineDiff.tsx +149 -0
  96. package/src/components/SettingsPanel.tsx +591 -0
  97. package/src/components/SuitePanel.tsx +875 -0
  98. package/src/lib/brand.ts +4 -0
  99. package/src/lib/config-yaml.ts +70 -0
  100. package/src/lib/consume-suite-sse.ts +70 -0
  101. package/src/lib/describe-judge.ts +23 -0
  102. package/src/lib/model-chip-palette.ts +9 -0
  103. package/src/lib/openai-model-list.ts +33 -0
  104. package/src/lib/provider-ui.ts +30 -0
  105. package/src/lib/resolve-credentials.ts +80 -0
  106. package/src/lib/run-history.ts +66 -0
  107. package/src/lib/simple-line-diff.ts +50 -0
  108. package/src/lib/storage.ts +100 -0
  109. package/src/lib/suite-judge-meta.ts +13 -0
  110. package/src/lib/suite-run-history.ts +81 -0
  111. package/src/types.ts +170 -0
  112. package/vercel.json +5 -0
@@ -0,0 +1,591 @@
1
+ "use client";
2
+
3
+ import React, { useId, useMemo, useState } from "react";
4
+ import type { JudgeSettings, LLMInstance, SecretsMap } from "../types";
5
+ import { DEFAULT_JUDGE_SETTINGS, SUGGESTED_SECRET_KEYS } from "../types";
6
+ import { ModelsSettingsSection } from "./ConfigPanel";
7
+ import { exportAppConfigYaml, mergeImportedConfig, parseAppConfigYaml } from "../lib/config-yaml";
8
+
9
+ type SettingsTab = "models" | "secrets" | "judge" | "config";
10
+
11
+ const inputStyle: React.CSSProperties = {
12
+ background: "var(--surface)",
13
+ border: "1px solid var(--border)",
14
+ borderRadius: "var(--r-md)",
15
+ color: "var(--text-1)",
16
+ padding: "0.45rem 0.65rem",
17
+ fontSize: "0.8125rem",
18
+ outline: "none",
19
+ width: "100%",
20
+ fontFamily: "inherit",
21
+ };
22
+
23
+ interface SettingsPanelProps {
24
+ open: boolean;
25
+ onClose: () => void;
26
+ instances: LLMInstance[];
27
+ onUpdateInstances: (instances: LLMInstance[]) => void;
28
+ secrets: SecretsMap;
29
+ onUpdateSecrets: (secrets: SecretsMap) => void;
30
+ judge: JudgeSettings;
31
+ onUpdateJudge: (j: JudgeSettings) => void;
32
+ }
33
+
34
+ export function SettingsPanel({
35
+ open,
36
+ onClose,
37
+ instances,
38
+ onUpdateInstances,
39
+ secrets,
40
+ onUpdateSecrets,
41
+ judge,
42
+ onUpdateJudge,
43
+ }: SettingsPanelProps) {
44
+ const anthropicDatalistId = useId();
45
+ const [tab, setTab] = useState<SettingsTab>("models");
46
+ const [yamlDraft, setYamlDraft] = useState("");
47
+ const [configMsg, setConfigMsg] = useState<string | null>(null);
48
+
49
+ const secretNames = useMemo(
50
+ () => Object.keys(secrets).sort((a, b) => a.localeCompare(b)),
51
+ [secrets]
52
+ );
53
+
54
+ const secretRows = useMemo(
55
+ () => secretNames.map((k) => ({ key: k, value: secrets[k] ?? "" })),
56
+ [secretNames, secrets]
57
+ );
58
+
59
+ const setSecretKey = (oldKey: string, newKey: string) => {
60
+ const k = newKey.trim();
61
+ if (!k || k === oldKey) return;
62
+ if (secrets[k] !== undefined && k !== oldKey) return;
63
+ const next = { ...secrets };
64
+ const v = next[oldKey] ?? "";
65
+ delete next[oldKey];
66
+ next[k] = v;
67
+ onUpdateSecrets(next);
68
+ };
69
+
70
+ const setSecretValue = (key: string, value: string) => {
71
+ onUpdateSecrets({ ...secrets, [key]: value });
72
+ };
73
+
74
+ const removeSecret = (key: string) => {
75
+ const next = { ...secrets };
76
+ delete next[key];
77
+ onUpdateSecrets(next);
78
+ };
79
+
80
+ const addEmptySecret = () => {
81
+ let n = 1;
82
+ let id = "api_key";
83
+ while (secrets[id] !== undefined) {
84
+ id = `api_key_${n++}`;
85
+ }
86
+ onUpdateSecrets({ ...secrets, [id]: "" });
87
+ };
88
+
89
+ const addSuggested = (key: string) => {
90
+ if (secrets[key] !== undefined) return;
91
+ onUpdateSecrets({ ...secrets, [key]: "" });
92
+ };
93
+
94
+ if (!open) return null;
95
+
96
+ return (
97
+ <>
98
+ <div
99
+ onClick={onClose}
100
+ role="presentation"
101
+ style={{
102
+ position: "fixed",
103
+ inset: 0,
104
+ background: "rgba(15, 23, 42, 0.35)",
105
+ zIndex: 40,
106
+ backdropFilter: "blur(8px)",
107
+ }}
108
+ />
109
+
110
+ <aside
111
+ style={{
112
+ position: "fixed",
113
+ top: 0,
114
+ right: 0,
115
+ bottom: 0,
116
+ width: "min(620px, 100vw)",
117
+ background: "var(--surface)",
118
+ borderLeft: "1px solid var(--border)",
119
+ zIndex: 50,
120
+ display: "flex",
121
+ flexDirection: "column",
122
+ boxShadow: "var(--shadow-drawer)",
123
+ overflow: "hidden",
124
+ }}
125
+ >
126
+ <header
127
+ style={{
128
+ flexShrink: 0,
129
+ background: "var(--surface)",
130
+ borderBottom: "1px solid var(--border)",
131
+ padding: "1rem 1.15rem",
132
+ display: "flex",
133
+ alignItems: "flex-start",
134
+ justifyContent: "space-between",
135
+ gap: "0.75rem",
136
+ }}
137
+ >
138
+ <div>
139
+ <h2
140
+ style={{
141
+ fontSize: "1.05rem",
142
+ fontWeight: 700,
143
+ color: "var(--text-1)",
144
+ letterSpacing: "-0.03em",
145
+ margin: 0,
146
+ }}
147
+ >
148
+ Settings
149
+ </h2>
150
+ <p style={{ margin: "0.25rem 0 0", fontSize: "0.78rem", color: "var(--text-3)", fontWeight: 500, lineHeight: 1.45 }}>
151
+ Models, secrets, judge, and config backup
152
+ </p>
153
+ </div>
154
+ <button
155
+ type="button"
156
+ onClick={onClose}
157
+ aria-label="Close"
158
+ style={{
159
+ background: "var(--surface-muted)",
160
+ border: "1px solid var(--border)",
161
+ color: "var(--text-2)",
162
+ borderRadius: "var(--r-md)",
163
+ width: 34,
164
+ height: 34,
165
+ cursor: "pointer",
166
+ fontSize: "1.15rem",
167
+ lineHeight: 1,
168
+ display: "flex",
169
+ alignItems: "center",
170
+ justifyContent: "center",
171
+ fontFamily: "inherit",
172
+ flexShrink: 0,
173
+ transition: "background 0.15s",
174
+ }}
175
+ >
176
+ ×
177
+ </button>
178
+ </header>
179
+
180
+ <nav
181
+ style={{
182
+ flexShrink: 0,
183
+ display: "flex",
184
+ gap: "0.15rem",
185
+ padding: "0 1.15rem",
186
+ borderBottom: "1px solid var(--border)",
187
+ background: "var(--surface)",
188
+ overflowX: "auto",
189
+ }}
190
+ aria-label="Settings sections"
191
+ >
192
+ {(
193
+ [
194
+ ["models", "Models"],
195
+ ["secrets", "Secrets"],
196
+ ["judge", "Judge"],
197
+ ["config", "YAML"],
198
+ ] as const
199
+ ).map(([id, label]) => {
200
+ const on = tab === id;
201
+ return (
202
+ <button
203
+ key={id}
204
+ type="button"
205
+ onClick={() => {
206
+ setTab(id);
207
+ setConfigMsg(null);
208
+ }}
209
+ style={{
210
+ padding: "0.65rem 0.15rem",
211
+ marginRight: "1.1rem",
212
+ marginBottom: -1,
213
+ border: "none",
214
+ borderBottom: on ? "2px solid var(--text-1)" : "2px solid transparent",
215
+ background: "transparent",
216
+ color: on ? "var(--text-1)" : "var(--text-3)",
217
+ fontSize: "0.8125rem",
218
+ fontWeight: on ? 600 : 500,
219
+ cursor: "pointer",
220
+ fontFamily: "inherit",
221
+ whiteSpace: "nowrap",
222
+ transition: "color 0.15s, border-color 0.15s",
223
+ }}
224
+ >
225
+ {label}
226
+ </button>
227
+ );
228
+ })}
229
+ </nav>
230
+
231
+ <div style={{ flex: 1, overflowY: "auto", padding: "1rem 1.15rem 1.25rem", minHeight: 0, background: "var(--surface-subtle)" }}>
232
+ {tab === "models" && (
233
+ <ModelsSettingsSection
234
+ instances={instances}
235
+ onUpdate={onUpdateInstances}
236
+ secretNames={secretNames}
237
+ secrets={secrets}
238
+ />
239
+ )}
240
+
241
+ {tab === "secrets" && (
242
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
243
+ <p style={{ margin: 0, fontSize: "0.75rem", color: "var(--text-2)", lineHeight: 1.5 }}>
244
+ Name each key (e.g. <code style={{ fontSize: "0.7rem" }}>anthropic</code>) and paste the
245
+ value. In <strong>Models</strong>, pick <em>Variable: …</em> to use a saved secret instead of
246
+ an inline key. Values stay in this browser (localStorage).
247
+ </p>
248
+ <div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}>
249
+ {SUGGESTED_SECRET_KEYS.map(({ key, label }) => (
250
+ <button
251
+ key={key}
252
+ type="button"
253
+ onClick={() => addSuggested(key)}
254
+ disabled={secrets[key] !== undefined}
255
+ style={{
256
+ fontSize: "0.68rem",
257
+ padding: "0.25rem 0.5rem",
258
+ borderRadius: 4,
259
+ border: "1px solid var(--border)",
260
+ background: secrets[key] !== undefined ? "var(--surface-hover)" : "var(--surface)",
261
+ color: secrets[key] !== undefined ? "var(--text-3)" : "var(--text-2)",
262
+ cursor: secrets[key] !== undefined ? "default" : "pointer",
263
+ fontFamily: "inherit",
264
+ }}
265
+ >
266
+ + {label}
267
+ </button>
268
+ ))}
269
+ </div>
270
+ <button
271
+ type="button"
272
+ onClick={addEmptySecret}
273
+ style={{
274
+ alignSelf: "flex-start",
275
+ fontSize: "0.75rem",
276
+ padding: "0.35rem 0.65rem",
277
+ borderRadius: 4,
278
+ border: "1px solid var(--border)",
279
+ background: "var(--surface)",
280
+ cursor: "pointer",
281
+ fontFamily: "inherit",
282
+ color: "var(--text-1)",
283
+ }}
284
+ >
285
+ + Custom variable
286
+ </button>
287
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
288
+ {secretRows.length === 0 ? (
289
+ <p style={{ color: "var(--text-3)", fontSize: "0.8125rem", margin: 0 }}>
290
+ No secrets yet. Add a suggested key or a custom variable.
291
+ </p>
292
+ ) : (
293
+ secretRows.map(({ key, value }) => (
294
+ <div
295
+ key={key}
296
+ style={{
297
+ display: "grid",
298
+ gridTemplateColumns: "minmax(100px, 1fr) 2fr auto",
299
+ gap: "0.4rem",
300
+ alignItems: "center",
301
+ }}
302
+ >
303
+ <input
304
+ value={key}
305
+ onChange={(e) => setSecretKey(key, e.target.value)}
306
+ style={{ ...inputStyle, fontFamily: "var(--font-mono)", fontSize: "0.75rem" }}
307
+ placeholder="variable_name"
308
+ spellCheck={false}
309
+ />
310
+ <input
311
+ type="password"
312
+ value={value}
313
+ onChange={(e) => setSecretValue(key, e.target.value)}
314
+ style={inputStyle}
315
+ placeholder="Secret value"
316
+ autoComplete="off"
317
+ />
318
+ <button
319
+ type="button"
320
+ onClick={() => removeSecret(key)}
321
+ aria-label={`Remove ${key}`}
322
+ style={{
323
+ border: "none",
324
+ background: "transparent",
325
+ color: "var(--text-3)",
326
+ cursor: "pointer",
327
+ fontSize: "1rem",
328
+ padding: "0.2rem",
329
+ }}
330
+ >
331
+ ×
332
+ </button>
333
+ </div>
334
+ ))
335
+ )}
336
+ </div>
337
+ </div>
338
+ )}
339
+
340
+ {tab === "judge" && (
341
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.65rem", maxWidth: 420 }}>
342
+ <p style={{ margin: 0, fontSize: "0.75rem", color: "var(--text-2)", lineHeight: 1.5 }}>
343
+ Used for <strong>llm-rubric</strong> assertions in Test Suites. Choose how responses are
344
+ scored when the suite YAML includes <code style={{ fontSize: "0.7rem" }}>type: llm-rubric</code>
345
+ .
346
+ </p>
347
+ <label style={{ fontSize: "0.6875rem", color: "var(--text-3)", fontWeight: 500 }}>Mode</label>
348
+ <select
349
+ value={judge.mode}
350
+ onChange={(e) =>
351
+ onUpdateJudge({
352
+ ...judge,
353
+ mode: e.target.value as JudgeSettings["mode"],
354
+ })
355
+ }
356
+ style={{ ...inputStyle, cursor: "pointer" }}
357
+ >
358
+ <option value="auto">Auto — Claude if Anthropic secret / env is available</option>
359
+ <option value="claude">Claude (Anthropic API)</option>
360
+ <option value="ollama">Ollama (local)</option>
361
+ <option value="none">None — llm-rubric will fail without a judge</option>
362
+ </select>
363
+
364
+ {(judge.mode === "auto" || judge.mode === "claude") && (
365
+ <>
366
+ <label style={{ fontSize: "0.6875rem", color: "var(--text-3)", fontWeight: 500 }}>
367
+ Anthropic API key — Secrets variable name
368
+ </label>
369
+ <input
370
+ value={judge.anthropicSecretRef}
371
+ onChange={(e) => onUpdateJudge({ ...judge, anthropicSecretRef: e.target.value })}
372
+ style={{ ...inputStyle, fontFamily: "var(--font-mono)", fontSize: "0.75rem" }}
373
+ placeholder="anthropic"
374
+ spellCheck={false}
375
+ list={anthropicDatalistId}
376
+ />
377
+ <datalist id={anthropicDatalistId}>
378
+ {secretNames.map((k) => (
379
+ <option key={k} value={k} />
380
+ ))}
381
+ </datalist>
382
+ <p style={{ margin: 0, fontSize: "0.65rem", color: "var(--text-3)" }}>
383
+ Server also falls back to <code style={{ fontSize: "0.62rem" }}>ANTHROPIC_API_KEY</code> when
384
+ mode is Auto and this variable is empty.
385
+ </p>
386
+ <label style={{ fontSize: "0.6875rem", color: "var(--text-3)", fontWeight: 500 }}>
387
+ Claude model (judge)
388
+ </label>
389
+ <input
390
+ value={judge.claudeModel}
391
+ onChange={(e) => onUpdateJudge({ ...judge, claudeModel: e.target.value })}
392
+ style={inputStyle}
393
+ placeholder={DEFAULT_JUDGE_SETTINGS.claudeModel}
394
+ />
395
+ </>
396
+ )}
397
+
398
+ {judge.mode === "ollama" && (
399
+ <>
400
+ <label style={{ fontSize: "0.6875rem", color: "var(--text-3)", fontWeight: 500 }}>
401
+ Ollama base URL
402
+ </label>
403
+ <input
404
+ value={judge.ollamaBaseUrl}
405
+ onChange={(e) => onUpdateJudge({ ...judge, ollamaBaseUrl: e.target.value })}
406
+ style={inputStyle}
407
+ placeholder="http://localhost:11434"
408
+ />
409
+ <label style={{ fontSize: "0.6875rem", color: "var(--text-3)", fontWeight: 500 }}>
410
+ Model
411
+ </label>
412
+ <input
413
+ value={judge.ollamaModel}
414
+ onChange={(e) => onUpdateJudge({ ...judge, ollamaModel: e.target.value })}
415
+ style={inputStyle}
416
+ placeholder="llama3.2"
417
+ />
418
+ </>
419
+ )}
420
+ </div>
421
+ )}
422
+
423
+ {tab === "config" && (
424
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.65rem" }}>
425
+ <p style={{ margin: 0, fontSize: "0.75rem", color: "var(--text-2)", lineHeight: 1.5 }}>
426
+ Export includes <strong>secrets</strong>, models, and judge settings as YAML. Treat exports as
427
+ sensitive. Import merges <code style={{ fontSize: "0.7rem" }}>secrets</code> and replaces{" "}
428
+ <code style={{ fontSize: "0.7rem" }}>judge</code> / <code style={{ fontSize: "0.7rem" }}>instances</code>{" "}
429
+ when present.
430
+ </p>
431
+ <div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
432
+ <button
433
+ type="button"
434
+ onClick={() => {
435
+ setYamlDraft(
436
+ exportAppConfigYaml({ secrets, judge, instances })
437
+ );
438
+ setConfigMsg("YAML generated below — copy or edit and re-import.");
439
+ }}
440
+ style={{
441
+ fontSize: "0.75rem",
442
+ padding: "0.4rem 0.75rem",
443
+ borderRadius: 4,
444
+ border: "1px solid var(--border)",
445
+ background: "var(--text-1)",
446
+ color: "var(--surface)",
447
+ cursor: "pointer",
448
+ fontFamily: "inherit",
449
+ fontWeight: 600,
450
+ }}
451
+ >
452
+ Export to editor
453
+ </button>
454
+ <button
455
+ type="button"
456
+ onClick={() => {
457
+ const text = exportAppConfigYaml({ secrets, judge, instances });
458
+ void navigator.clipboard.writeText(text).then(
459
+ () => setConfigMsg("Copied YAML to clipboard."),
460
+ () => setConfigMsg("Could not copy — copy from the editor manually.")
461
+ );
462
+ }}
463
+ style={{
464
+ fontSize: "0.75rem",
465
+ padding: "0.4rem 0.75rem",
466
+ borderRadius: 4,
467
+ border: "1px solid var(--border)",
468
+ background: "var(--surface)",
469
+ cursor: "pointer",
470
+ fontFamily: "inherit",
471
+ }}
472
+ >
473
+ Copy to clipboard
474
+ </button>
475
+ <button
476
+ type="button"
477
+ onClick={() => {
478
+ try {
479
+ const parsed = parseAppConfigYaml(yamlDraft.trim() || exportAppConfigYaml({ secrets, judge, instances }));
480
+ const merged = mergeImportedConfig(parsed, { secrets, judge, instances });
481
+ onUpdateSecrets(merged.secrets);
482
+ onUpdateJudge(merged.judge);
483
+ onUpdateInstances(merged.instances);
484
+ setConfigMsg("Imported and saved.");
485
+ } catch (e) {
486
+ setConfigMsg(e instanceof Error ? e.message : String(e));
487
+ }
488
+ }}
489
+ style={{
490
+ fontSize: "0.75rem",
491
+ padding: "0.4rem 0.75rem",
492
+ borderRadius: 4,
493
+ border: "1px solid var(--accent)",
494
+ background: "var(--accent-subtle)",
495
+ color: "var(--accent-text)",
496
+ cursor: "pointer",
497
+ fontFamily: "inherit",
498
+ fontWeight: 600,
499
+ }}
500
+ >
501
+ Import & apply
502
+ </button>
503
+ </div>
504
+ {configMsg && (
505
+ <p style={{ margin: 0, fontSize: "0.75rem", color: "var(--text-2)" }}>{configMsg}</p>
506
+ )}
507
+ <textarea
508
+ value={yamlDraft}
509
+ onChange={(e) => setYamlDraft(e.target.value)}
510
+ spellCheck={false}
511
+ placeholder="Click “Export to editor” or paste YAML here, then Import & apply…"
512
+ style={{
513
+ width: "100%",
514
+ minHeight: 280,
515
+ fontFamily: "var(--font-mono)",
516
+ fontSize: "0.72rem",
517
+ lineHeight: 1.5,
518
+ padding: "0.65rem 0.75rem",
519
+ borderRadius: 6,
520
+ border: "1px solid var(--border)",
521
+ background: "var(--surface-subtle)",
522
+ color: "var(--text-1)",
523
+ resize: "vertical",
524
+ boxSizing: "border-box",
525
+ }}
526
+ />
527
+ </div>
528
+ )}
529
+ </div>
530
+
531
+ <footer
532
+ style={{
533
+ flexShrink: 0,
534
+ borderTop: "1px solid var(--border)",
535
+ background: "var(--surface)",
536
+ padding: "0.85rem 1.15rem",
537
+ display: "flex",
538
+ alignItems: "center",
539
+ justifyContent: "space-between",
540
+ gap: "1rem",
541
+ }}
542
+ >
543
+ <span style={{ fontSize: "0.78rem", color: "var(--text-3)", fontWeight: 500 }}>
544
+ {tab === "models" && (
545
+ <>
546
+ {instances.length} model{instances.length !== 1 ? "s" : ""} configured
547
+ <span style={{ display: "block", fontSize: "0.65rem", marginTop: "0.2rem", color: "var(--text-3)", opacity: 0.85 }}>
548
+ Edits save to this browser as you change them.
549
+ </span>
550
+ </>
551
+ )}
552
+ {tab === "secrets" && (
553
+ <>
554
+ {secretNames.length} secret{secretNames.length !== 1 ? "s" : ""} stored
555
+ </>
556
+ )}
557
+ {tab === "judge" && "Judge configuration"}
558
+ {tab === "config" && "YAML import / export"}
559
+ </span>
560
+ <button
561
+ type="button"
562
+ onClick={onClose}
563
+ style={{
564
+ padding: "0.5rem 1.15rem",
565
+ borderRadius: "var(--r-md)",
566
+ border: "2px solid var(--text-1)",
567
+ background: "var(--surface)",
568
+ color: "var(--text-1)",
569
+ fontSize: "0.8125rem",
570
+ fontWeight: 700,
571
+ cursor: "pointer",
572
+ fontFamily: "inherit",
573
+ flexShrink: 0,
574
+ transition: "background 0.15s, color 0.15s",
575
+ }}
576
+ onMouseEnter={(e) => {
577
+ e.currentTarget.style.background = "var(--text-1)";
578
+ e.currentTarget.style.color = "var(--surface)";
579
+ }}
580
+ onMouseLeave={(e) => {
581
+ e.currentTarget.style.background = "var(--surface)";
582
+ e.currentTarget.style.color = "var(--text-1)";
583
+ }}
584
+ >
585
+ Save changes
586
+ </button>
587
+ </footer>
588
+ </aside>
589
+ </>
590
+ );
591
+ }