dsh-model-arena 0.3.0 → 0.4.1

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.
@@ -0,0 +1,388 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Shared UI component kit for dsh plugin panels.
4
+ *
5
+ * Self-contained: imports nothing but React. All styling is inline + CSS custom
6
+ * properties so the components follow the host theme. A single <style> tag injects
7
+ * keyframes the first time any component mounts.
8
+ *
9
+ * @module client/ui
10
+ */
11
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
12
+ // ─── Theme tokens ──────────────────────────────────────────────
13
+ const T = {
14
+ accent: 'var(--accent, #4B8BBE)',
15
+ accentHover: 'var(--accent-hover, #3a6f9e)',
16
+ error: 'var(--error, #e53935)',
17
+ success: 'var(--success, #2e7d32)',
18
+ warning: 'var(--warning, #ed6c02)',
19
+ bg: 'var(--bg-secondary, rgba(128,128,128,0.04))',
20
+ bgHover: 'var(--bg-tertiary, rgba(128,128,128,0.08))',
21
+ border: 'var(--border, rgba(128,128,128,0.2))',
22
+ text: 'var(--text-primary, inherit)',
23
+ muted: 'var(--text-secondary, rgba(128,128,128,0.65))',
24
+ surface: 'var(--bg-primary, #fff)',
25
+ };
26
+ // ─── Style injection (once) ────────────────────────────────────
27
+ let injected = false;
28
+ function ensureStyles() {
29
+ if (injected || typeof document === 'undefined')
30
+ return;
31
+ injected = true;
32
+ const el = document.createElement('style');
33
+ el.textContent = `
34
+ @keyframes dsh-fade-in{from{opacity:0}to{opacity:1}}
35
+ @keyframes dsh-slide-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
36
+ @keyframes dsh-spin{to{transform:rotate(360deg)}}
37
+ .dsh-modal-overlay{animation:dsh-fade-in .15s ease}
38
+ .dsh-modal-body{animation:dsh-slide-up .2s ease}
39
+ .dsh-toast{animation:dsh-slide-up .2s ease}
40
+ .dsh-spin{animation:dsh-spin .6s linear infinite}
41
+ `;
42
+ document.head.appendChild(el);
43
+ }
44
+ // ─── Modal ─────────────────────────────────────────────────────
45
+ export function Modal({ title, onClose, children, footer, width = 520, }) {
46
+ useEffect(() => {
47
+ ensureStyles();
48
+ const handler = (e) => {
49
+ if (e.key === 'Escape')
50
+ onClose();
51
+ };
52
+ document.addEventListener('keydown', handler);
53
+ return () => document.removeEventListener('keydown', handler);
54
+ }, [onClose]);
55
+ return (_jsx("div", { className: "dsh-modal-overlay", style: {
56
+ position: 'fixed',
57
+ inset: 0,
58
+ zIndex: 1000,
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ justifyContent: 'center',
62
+ background: 'rgba(0,0,0,0.45)',
63
+ backdropFilter: 'blur(3px)',
64
+ WebkitBackdropFilter: 'blur(3px)',
65
+ }, onClick: onClose, children: _jsxs("div", { className: "dsh-modal-body", style: {
66
+ background: T.surface,
67
+ borderRadius: 14,
68
+ boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
69
+ width: `min(92vw, ${width}px)`,
70
+ maxHeight: '85vh',
71
+ display: 'flex',
72
+ flexDirection: 'column',
73
+ overflow: 'hidden',
74
+ }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { style: {
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ justifyContent: 'space-between',
78
+ padding: '16px 22px',
79
+ borderBottom: `1px solid ${T.border}`,
80
+ }, children: [_jsx("h3", { style: { margin: 0, fontSize: 15, fontWeight: 600 }, children: title }), _jsx("button", { type: "button", onClick: onClose, style: {
81
+ background: 'none',
82
+ border: 'none',
83
+ cursor: 'pointer',
84
+ fontSize: 20,
85
+ color: T.muted,
86
+ padding: '0 4px',
87
+ lineHeight: 1,
88
+ borderRadius: 4,
89
+ }, "aria-label": "Close", children: "\u00D7" })] }), _jsx("div", { style: { padding: 22, overflow: 'auto', flex: 1 }, children: children }), footer !== undefined && (_jsx("div", { style: {
90
+ padding: '14px 22px',
91
+ borderTop: `1px solid ${T.border}`,
92
+ display: 'flex',
93
+ justifyContent: 'flex-end',
94
+ gap: 10,
95
+ background: T.bg,
96
+ }, children: footer }))] }) }));
97
+ }
98
+ // ─── ConfirmDialog ─────────────────────────────────────────────
99
+ export function ConfirmDialog({ title, message, confirmLabel, cancelLabel = 'Cancel', onConfirm, onClose, danger, }) {
100
+ return (_jsx(Modal, { title: title, onClose: onClose, width: 420, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: onClose, children: cancelLabel }), _jsx(Button, { variant: danger === true ? 'danger' : 'primary', onClick: () => {
101
+ onConfirm();
102
+ onClose();
103
+ }, children: confirmLabel })] }), children: _jsx("p", { style: { margin: 0, fontSize: 14, lineHeight: 1.6 }, children: message }) }));
104
+ }
105
+ // ─── Button ────────────────────────────────────────────────────
106
+ export function Button({ variant = 'secondary', size = 'md', children, style, ...rest }) {
107
+ const base = {
108
+ cursor: 'pointer',
109
+ borderRadius: 7,
110
+ fontWeight: 500,
111
+ transition: 'all 0.15s ease',
112
+ border: 'none',
113
+ display: 'inline-flex',
114
+ alignItems: 'center',
115
+ gap: 4,
116
+ fontSize: size === 'sm' ? 12 : 13,
117
+ padding: size === 'sm' ? '5px 12px' : '8px 18px',
118
+ fontFamily: 'inherit',
119
+ };
120
+ const variants = {
121
+ primary: { background: T.accent, color: '#fff' },
122
+ secondary: {
123
+ background: T.bg,
124
+ color: T.text,
125
+ border: `1px solid ${T.border}`,
126
+ },
127
+ danger: {
128
+ background: 'transparent',
129
+ color: T.error,
130
+ border: `1px solid ${T.error}`,
131
+ },
132
+ ghost: { background: 'transparent', color: T.muted, border: 'none' },
133
+ };
134
+ return (_jsx("button", { type: "button", style: { ...base, ...variants[variant], ...style }, ...rest, children: children }));
135
+ }
136
+ // ─── Card ──────────────────────────────────────────────────────
137
+ export function Card({ title, icon, children, actions, style, padding = 14, }) {
138
+ return (_jsxs("div", { style: {
139
+ borderRadius: 10,
140
+ border: `1px solid ${T.border}`,
141
+ background: T.bg,
142
+ overflow: 'hidden',
143
+ ...style,
144
+ }, children: [(title !== undefined || actions !== undefined) && (_jsxs("div", { style: {
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ justifyContent: 'space-between',
148
+ padding: '12px 16px',
149
+ borderBottom: title !== undefined ? `1px solid ${T.border}` : 'none',
150
+ }, children: [_jsxs("span", { style: { fontSize: 13, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 }, children: [icon !== undefined && _jsx("span", { children: icon }), title] }), actions] })), _jsx("div", { style: { padding }, children: children })] }));
151
+ }
152
+ // ─── StatCard ──────────────────────────────────────────────────
153
+ export function StatCard({ value, label, color, unit, }) {
154
+ return (_jsxs("div", { style: {
155
+ flex: 1,
156
+ minWidth: 90,
157
+ textAlign: 'center',
158
+ padding: '14px 10px',
159
+ borderRadius: 10,
160
+ border: `1px solid ${T.border}`,
161
+ background: T.bg,
162
+ }, children: [_jsxs("div", { style: { fontSize: 24, fontWeight: 700, color: color ?? T.text, lineHeight: 1.2 }, children: [value, unit !== undefined && (_jsx("span", { style: { fontSize: 13, fontWeight: 500, opacity: 0.7, marginLeft: 2 }, children: unit }))] }), _jsx("div", { style: { fontSize: 11, color: T.muted, marginTop: 4 }, children: label })] }));
163
+ }
164
+ // ─── Badge ─────────────────────────────────────────────────────
165
+ export function Badge({ children, color = 'default', }) {
166
+ const colors = {
167
+ default: { bg: 'rgba(128,128,128,0.12)', fg: T.muted },
168
+ success: { bg: 'rgba(46,125,50,0.12)', fg: T.success },
169
+ error: { bg: 'rgba(229,57,53,0.12)', fg: T.error },
170
+ warning: { bg: 'rgba(245,124,0,0.12)', fg: T.warning },
171
+ info: { bg: 'rgba(75,139,190,0.12)', fg: T.accent },
172
+ };
173
+ const c = colors[color];
174
+ return (_jsx("span", { style: {
175
+ display: 'inline-block',
176
+ padding: '2px 9px',
177
+ borderRadius: 10,
178
+ fontSize: 11,
179
+ fontWeight: 600,
180
+ background: c.bg,
181
+ color: c.fg,
182
+ }, children: children }));
183
+ }
184
+ // ─── Input ─────────────────────────────────────────────────────
185
+ export function Input({ style, ...rest }) {
186
+ return (_jsx("input", { ...rest, style: {
187
+ fontSize: 13,
188
+ padding: '8px 12px',
189
+ borderRadius: 7,
190
+ border: `1px solid ${T.border}`,
191
+ background: T.surface,
192
+ color: T.text,
193
+ width: '100%',
194
+ boxSizing: 'border-box',
195
+ fontFamily: 'inherit',
196
+ outline: 'none',
197
+ transition: 'border-color 0.15s',
198
+ ...style,
199
+ } }));
200
+ }
201
+ // ─── Select ────────────────────────────────────────────────────
202
+ export function Select({ style, children, ...rest }) {
203
+ return (_jsx("select", { ...rest, style: {
204
+ fontSize: 13,
205
+ padding: '8px 12px',
206
+ borderRadius: 7,
207
+ border: `1px solid ${T.border}`,
208
+ background: T.surface,
209
+ color: T.text,
210
+ cursor: 'pointer',
211
+ fontFamily: 'inherit',
212
+ outline: 'none',
213
+ ...style,
214
+ }, children: children }));
215
+ }
216
+ // ─── Textarea ──────────────────────────────────────────────────
217
+ export function Textarea({ style, ...rest }) {
218
+ return (_jsx("textarea", { ...rest, style: {
219
+ fontSize: 13,
220
+ padding: '8px 12px',
221
+ borderRadius: 7,
222
+ border: `1px solid ${T.border}`,
223
+ background: T.surface,
224
+ color: T.text,
225
+ width: '100%',
226
+ boxSizing: 'border-box',
227
+ fontFamily: 'inherit',
228
+ outline: 'none',
229
+ resize: 'vertical',
230
+ lineHeight: 1.5,
231
+ ...style,
232
+ } }));
233
+ }
234
+ const ToastContext = createContext(() => { });
235
+ export function ToastProvider({ children }) {
236
+ const [toasts, setToasts] = useState([]);
237
+ const show = useCallback((type, message) => {
238
+ const id = Date.now() + Math.random();
239
+ setToasts((t) => [...t, { id, type, message }]);
240
+ setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3500);
241
+ }, []);
242
+ return (_jsxs(ToastContext.Provider, { value: show, children: [children, toasts.length > 0 && (_jsx("div", { style: {
243
+ position: 'fixed',
244
+ bottom: 24,
245
+ right: 24,
246
+ zIndex: 2000,
247
+ display: 'flex',
248
+ flexDirection: 'column',
249
+ gap: 8,
250
+ }, children: toasts.map((t) => (_jsx("div", { className: "dsh-toast", style: {
251
+ padding: '12px 18px',
252
+ borderRadius: 8,
253
+ fontSize: 13,
254
+ fontWeight: 500,
255
+ boxShadow: '0 6px 20px rgba(0,0,0,0.2)',
256
+ color: '#fff',
257
+ background: t.type === 'success'
258
+ ? T.success
259
+ : t.type === 'error'
260
+ ? T.error
261
+ : T.accent,
262
+ maxWidth: 360,
263
+ }, children: t.message }, t.id))) }))] }));
264
+ }
265
+ export function useToast() {
266
+ return useContext(ToastContext);
267
+ }
268
+ // ─── Spinner ───────────────────────────────────────────────────
269
+ export function Spinner({ size = 16 }) {
270
+ ensureStyles();
271
+ return (_jsx("span", { className: "dsh-spin", style: {
272
+ display: 'inline-block',
273
+ width: size,
274
+ height: size,
275
+ border: `2px solid ${T.border}`,
276
+ borderTopColor: T.accent,
277
+ borderRadius: '50%',
278
+ } }));
279
+ }
280
+ // ─── EmptyState ────────────────────────────────────────────────
281
+ export function EmptyState({ icon, message, }) {
282
+ return (_jsxs("div", { style: {
283
+ textAlign: 'center',
284
+ padding: '36px 16px',
285
+ color: T.muted,
286
+ }, children: [icon !== undefined && (_jsx("div", { style: { fontSize: 36, marginBottom: 10, opacity: 0.4 }, children: icon })), _jsx("div", { style: { fontSize: 13 }, children: message })] }));
287
+ }
288
+ // ─── SectionTitle ──────────────────────────────────────────────
289
+ export function SectionTitle({ icon, children, actions, }) {
290
+ return (_jsxs("div", { style: {
291
+ display: 'flex',
292
+ alignItems: 'center',
293
+ justifyContent: 'space-between',
294
+ marginTop: 6,
295
+ marginBottom: 8,
296
+ }, children: [_jsxs("span", { style: {
297
+ fontSize: 14,
298
+ fontWeight: 600,
299
+ display: 'flex',
300
+ alignItems: 'center',
301
+ gap: 6,
302
+ }, children: [icon !== undefined && _jsx("span", { children: icon }), children] }), actions] }));
303
+ }
304
+ // ─── Table helpers ─────────────────────────────────────────────
305
+ export const tableStyles = {
306
+ table: {
307
+ borderCollapse: 'collapse',
308
+ width: '100%',
309
+ },
310
+ th: {
311
+ textAlign: 'left',
312
+ padding: '8px 12px 8px 0',
313
+ fontWeight: 600,
314
+ fontSize: 12,
315
+ color: T.muted,
316
+ borderBottom: `1px solid ${T.border}`,
317
+ },
318
+ td: {
319
+ padding: '8px 12px 8px 0',
320
+ fontSize: 13,
321
+ borderBottom: '1px solid rgba(128,128,128,0.08)',
322
+ },
323
+ clickRow: { cursor: 'pointer' },
324
+ };
325
+ // ─── CodeBlock ─────────────────────────────────────────────────
326
+ export function CodeBlock({ children, maxHeight = 200, style, }) {
327
+ return (_jsx("pre", { style: {
328
+ whiteSpace: 'pre-wrap',
329
+ wordBreak: 'break-word',
330
+ fontSize: 12,
331
+ maxHeight,
332
+ overflow: 'auto',
333
+ padding: 10,
334
+ borderRadius: 7,
335
+ background: 'rgba(128,128,128,0.06)',
336
+ border: `1px solid ${T.border}`,
337
+ margin: 0,
338
+ ...style,
339
+ }, children: children }));
340
+ }
341
+ // ─── ProgressBar ───────────────────────────────────────────────
342
+ export function ProgressBar({ pct, color, height = 18, }) {
343
+ return (_jsx("div", { style: {
344
+ width: '100%',
345
+ height,
346
+ background: 'rgba(128,128,128,0.12)',
347
+ borderRadius: 5,
348
+ overflow: 'hidden',
349
+ position: 'relative',
350
+ }, children: _jsx("div", { style: {
351
+ width: `${Math.min(pct, 100)}%`,
352
+ height: '100%',
353
+ background: color ?? T.accent,
354
+ borderRadius: 5,
355
+ transition: 'width 0.3s ease',
356
+ } }) }));
357
+ }
358
+ // ─── Field label ───────────────────────────────────────────────
359
+ export function Field({ label, children, hint, }) {
360
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsx("label", { style: { fontSize: 12, fontWeight: 600, color: T.text }, children: label }), children, hint !== undefined && (_jsx("span", { style: { fontSize: 11, color: T.muted }, children: hint }))] }));
361
+ }
362
+ // ─── usePanel hook (shared fetch logic) ────────────────────────
363
+ export function usePanel(path) {
364
+ const [payload, setPayload] = useState(null);
365
+ const [error, setError] = useState(null);
366
+ const [tick, setTick] = useState(0);
367
+ const reload = useCallback(() => setTick((v) => v + 1), []);
368
+ useEffect(() => {
369
+ const c = new AbortController();
370
+ setError(null);
371
+ fetch(path, { signal: c.signal })
372
+ .then(async (r) => {
373
+ if (!r.ok)
374
+ throw new Error(String(r.status));
375
+ return r.json();
376
+ })
377
+ .then((p) => {
378
+ if (!c.signal.aborted)
379
+ setPayload(p);
380
+ })
381
+ .catch((e) => {
382
+ if (!c.signal.aborted)
383
+ setPayload(null), setError(e instanceof Error ? e.message : String(e));
384
+ });
385
+ return () => c.abort();
386
+ }, [tick, path]);
387
+ return useMemo(() => ({ payload, error, reload }), [payload, error, reload]);
388
+ }
@@ -1,24 +1,10 @@
1
1
  /**
2
- * Pure rendering half of the modelArena page.
3
- *
4
- * Separate from `index.tsx` so a static render can assert in Node what the page draws —
5
- * the shipped bundle is a loader factory only a browser can run. Every user-visible
6
- * string comes from the `t` seat the renderer binds from this plugin's namespace, so the
7
- * page follows the UI language; no copy is hardcoded here.
8
- *
2
+ * Pure rendering half of the modelArena page. Uses shared UI kit.
9
3
  * @module client/view
10
4
  */
11
- import type { PanelPayload } from '../types.js';
5
+ import type { ReactNode } from 'react';
12
6
  export type Translate = (key: string, params?: Record<string, unknown>) => string;
13
7
  export interface PanelProps {
14
8
  t: Translate;
15
9
  }
16
- interface PanelState {
17
- payload: PanelPayload | null;
18
- error: string | null;
19
- }
20
- export declare function usePanel(): PanelState & {
21
- reload: () => void;
22
- };
23
- export declare function ArenaPanel({ t }: PanelProps): import("react").JSX.Element;
24
- export {};
10
+ export declare function ArenaPanel({ t }: PanelProps): ReactNode;
@@ -1,117 +1,57 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * Pure rendering half of the modelArena page.
4
- *
5
- * Separate from `index.tsx` so a static render can assert in Node what the page draws —
6
- * the shipped bundle is a loader factory only a browser can run. Every user-visible
7
- * string comes from the `t` seat the renderer binds from this plugin's namespace, so the
8
- * page follows the UI language; no copy is hardcoded here.
9
- *
3
+ * Pure rendering half of the modelArena page. Uses shared UI kit.
10
4
  * @module client/view
11
5
  */
12
- import { useCallback, useEffect, useState } from 'react';
6
+ import { useCallback, useState } from 'react';
7
+ import { Badge, Button, Card, ConfirmDialog, EmptyState, Modal, SectionTitle, Spinner, StatCard, ToastProvider, tableStyles, usePanel, useToast, } from './ui.js';
13
8
  const PANEL_PATH = '/api/arena.panel';
14
9
  const RUN_PATH = '/api/arena.run';
15
10
  const RATE_PATH = '/api/arena.rate';
16
11
  const DELETE_PATH = '/api/arena.delete';
17
- // --- Styles ---
18
- const wrap = { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 820, fontFamily: 'inherit' };
19
- const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
20
- const section = { marginTop: 8 };
21
- const sectionTitle = { fontSize: 13, fontWeight: 600, marginBottom: 6 };
22
- const muted = { fontSize: 12, opacity: 0.75 };
23
- const table = { borderCollapse: 'collapse', width: '100%' };
24
- const th = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' };
25
- const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
26
- const card = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', background: 'rgba(128,128,128,0.04)', fontSize: 13 };
27
- const badge = { display: 'inline-block', padding: '1px 6px', borderRadius: 9, fontSize: 11, fontWeight: 600 };
28
- const statRow = { display: 'flex', gap: 16, flexWrap: 'wrap' };
29
- const statBox = { ...card, flex: 1, minWidth: 100, textAlign: 'center' };
30
- const starBtn = { cursor: 'pointer', background: 'none', border: 'none', fontSize: 16, padding: '0 1px' };
31
- const clickRow = { cursor: 'pointer' };
32
- const preBlock = { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, maxHeight: 200, overflow: 'auto', padding: 8, borderRadius: 4, background: 'rgba(128,128,128,0.06)', border: '1px solid rgba(128,128,128,0.15)' };
33
- const dangerBtn = { fontSize: 11, color: '#e55', background: 'none', border: '1px solid #e55', borderRadius: 4, padding: '2px 8px', cursor: 'pointer' };
34
- export function usePanel() {
35
- const [state, setState] = useState({ payload: null, error: null });
36
- const [tick, setTick] = useState(0);
37
- const reload = useCallback(() => setTick((v) => v + 1), []);
38
- useEffect(() => {
39
- const c = new AbortController();
40
- setState((p) => ({ ...p, error: null }));
41
- fetch(PANEL_PATH, { signal: c.signal })
42
- .then(async (r) => {
43
- if (!r.ok)
44
- throw new Error(String(r.status));
45
- return r.json();
46
- })
47
- .then((payload) => { if (!c.signal.aborted)
48
- setState({ payload, error: null }); })
49
- .catch((e) => { if (!c.signal.aborted)
50
- setState({ payload: null, error: e instanceof Error ? e.message : String(e) }); });
51
- return () => c.abort();
52
- }, [tick]);
53
- return { ...state, reload };
54
- }
55
- // --- Stars ---
56
12
  function Stars({ rating, onRate }) {
57
- return (_jsx("span", { children: [1, 2, 3, 4, 5].map((n) => (_jsx("button", { type: "button", style: starBtn, onClick: () => onRate(n), title: `${n}/5`, children: n <= rating ? '★' : '☆' }, n))) }));
58
- }
59
- // --- Leaderboard ---
60
- function Leaderboard({ entries, t }) {
61
- if (entries.length === 0)
62
- return null;
63
- return (_jsxs("div", { style: section, children: [_jsxs("div", { style: sectionTitle, children: ["\uD83C\uDFC6 ", t('leaderboard')] }), _jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: "#" }), _jsx("th", { style: th, children: t('model') }), _jsx("th", { style: th, children: "Elo" }), _jsx("th", { style: th, children: t('wins') }), _jsx("th", { style: th, children: t('avgRating') }), _jsx("th", { style: th, children: t('runs') })] }) }), _jsx("tbody", { children: entries.map((e, i) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: i + 1 }), _jsx("td", { style: td, children: _jsx("strong", { children: e.model }) }), _jsx("td", { style: td, children: e.elo }), _jsx("td", { style: td, children: e.wins }), _jsx("td", { style: td, children: e.avgRating > 0 ? e.avgRating.toFixed(1) : '—' }), _jsx("td", { style: td, children: e.totalRuns })] }, e.model))) })] })] }));
13
+ return (_jsx("span", { style: { display: 'inline-flex', gap: 1 }, children: [1, 2, 3, 4, 5].map((n) => (_jsx("button", { type: "button", style: {
14
+ background: 'none', border: 'none', cursor: 'pointer', fontSize: 18,
15
+ color: n <= rating ? 'var(--warning, #ed6c02)' : 'rgba(128,128,128,0.3)',
16
+ padding: '0 2px',
17
+ }, onClick: () => onRate(n), title: `${n}/5`, children: "\u2605" }, n))) }));
64
18
  }
65
- // --- Stats summary ---
66
- function StatsSummary({ stats, t }) {
67
- return (_jsxs("div", { style: statRow, children: [_jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 20, fontWeight: 700 }, children: stats.totalRuns }), _jsx("div", { style: muted, children: t('totalRuns') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 20, fontWeight: 700 }, children: stats.uniqueModels }), _jsx("div", { style: muted, children: t('uniqueModels') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 20, fontWeight: 700 }, children: stats.totalRatings }), _jsx("div", { style: muted, children: t('totalRatings') })] })] }));
68
- }
69
- // --- Run detail expansion ---
70
- function RunDetail({ runId, t, onDelete }) {
19
+ function RunModal({ runId, t, onClose, onDeleted }) {
20
+ const toast = useToast();
71
21
  const [run, setRun] = useState(null);
72
22
  const [loading, setLoading] = useState(true);
73
- useEffect(() => {
23
+ const [confirmDel, setConfirmDel] = useState(false);
24
+ useCallback(() => {
74
25
  setLoading(true);
75
- fetch(`${RUN_PATH}?id=${runId}`)
76
- .then((r) => r.json())
77
- .then(setRun)
78
- .catch(() => setRun(null))
79
- .finally(() => setLoading(false));
80
- }, [runId]);
26
+ fetch(`${RUN_PATH}?id=${runId}`).then(async (r) => r.json())
27
+ .then(setRun).catch(() => setRun(null)).finally(() => setLoading(false));
28
+ }, [runId])();
81
29
  const handleRate = useCallback((model, rating) => {
82
- fetch(RATE_PATH, {
83
- method: 'POST',
84
- headers: { 'Content-Type': 'application/json' },
85
- body: JSON.stringify({ runId, model, rating }),
86
- })
87
- .then((r) => r.json())
88
- .then(setRun)
89
- .catch(() => { });
30
+ fetch(RATE_PATH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId, model, rating }) })
31
+ .then(async (r) => r.json()).then(setRun).catch(() => { });
90
32
  }, [runId]);
91
- const handleDelete = useCallback(() => {
92
- if (!confirm(t('confirmDelete')))
93
- return;
94
- fetch(DELETE_PATH, {
95
- method: 'POST',
96
- headers: { 'Content-Type': 'application/json' },
97
- body: JSON.stringify({ runId }),
98
- }).then(() => onDelete()).catch(() => { });
99
- }, [runId, t, onDelete]);
100
- if (loading)
101
- return _jsx("p", { style: muted, children: t('loading') });
102
- if (!run)
103
- return _jsx("p", { style: muted, children: t('notFound') });
104
- return (_jsxs("div", { style: { ...card, marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsxs("strong", { children: [t('prompt'), ": "] }), _jsx("button", { type: "button", style: dangerBtn, onClick: handleDelete, children: t('delete') })] }), _jsx("div", { style: preBlock, children: run.prompt }), run.reference && (_jsxs("div", { style: { marginTop: 6 }, children: [_jsxs("div", { style: { fontSize: 12, fontWeight: 600, marginBottom: 2 }, children: [t('reference'), ":"] }), _jsx("div", { style: preBlock, children: run.reference })] })), _jsx("div", { style: { marginTop: 8 }, children: run.results.map((r) => (_jsxs("div", { style: { ...card, marginTop: 6 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [_jsx("strong", { children: r.model }), _jsx("span", { children: r.error ? (_jsx("span", { style: { ...badge, background: 'rgba(255,80,80,0.15)', color: '#e55' }, children: t('error') })) : (_jsxs("span", { style: { ...badge, background: 'rgba(80,200,120,0.15)', color: '#4a4' }, children: [r.latencyMs, "ms"] })) })] }), _jsx("div", { style: preBlock, children: r.error ?? r.output }), _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 }, children: [_jsxs("span", { style: muted, children: [r.promptTokens + r.completionTokens, " tokens (", r.promptTokens, "\u2191 ", r.completionTokens, "\u2193)"] }), _jsx(Stars, { rating: r.rating ?? 0, onRate: (n) => handleRate(r.model, n) })] })] }, r.model))) })] }));
33
+ const handleDelete = useCallback(async () => {
34
+ const r = await fetch(DELETE_PATH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId }) });
35
+ if (r.ok) {
36
+ toast('success', t('deleted'));
37
+ onDeleted();
38
+ onClose();
39
+ }
40
+ }, [runId, t, toast, onDeleted, onClose]);
41
+ return (_jsxs(Modal, { title: `${t('run')} ${runId.slice(0, 8)}…`, onClose: onClose, width: 720, footer: _jsxs(Button, { variant: "danger", size: "sm", onClick: () => setConfirmDel(true), children: ["\uD83D\uDDD1 ", t('delete')] }), children: [loading ? _jsx("div", { style: { display: 'flex', justifyContent: 'center', padding: 20 }, children: _jsx(Spinner, { size: 24 }) })
42
+ : run === null ? _jsx(EmptyState, { message: t('notFound') })
43
+ : (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 }, children: [_jsx(Card, { title: t('prompt'), children: _jsx("pre", { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, margin: 0 }, children: run.prompt }) }), run.reference && _jsx(Card, { title: t('reference'), children: _jsx("pre", { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, margin: 0 }, children: run.reference }) }), run.results.map((r) => (_jsxs(Card, { children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("strong", { style: { fontSize: 13 }, children: r.model }), r.error ? _jsx(Badge, { color: "error", children: t('error') }) : _jsxs(Badge, { color: "success", children: [r.latencyMs, "ms"] })] }), _jsx("pre", { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, margin: 0, padding: 8, borderRadius: 6, background: 'rgba(128,128,128,0.06)', maxHeight: 150, overflow: 'auto' }, children: r.error ?? r.output }), _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 8 }, children: [_jsxs("span", { style: { fontSize: 11, opacity: 0.6 }, children: [r.promptTokens + r.completionTokens, " tokens"] }), _jsx(Stars, { rating: r.rating ?? 0, onRate: (n) => handleRate(r.model, n) })] })] }, r.model)))] })), confirmDel && _jsx(ConfirmDialog, { title: t('delete'), message: t('confirmDelete'), confirmLabel: t('delete'), danger: true, onConfirm: handleDelete, onClose: () => setConfirmDel(false) })] }));
105
44
  }
106
- // --- Main Panel ---
107
- export function ArenaPanel({ t }) {
108
- const { payload, error, reload } = usePanel();
109
- const [expandedRun, setExpandedRun] = useState(null);
110
- const header = (_jsxs("header", { style: head, children: [_jsxs("strong", { style: { fontSize: 14 }, children: ["\u2694\uFE0F ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsx("button", { type: "button", onClick: reload, style: { fontSize: 12 }, children: t('refresh') })] }));
111
- if (error !== null) {
112
- return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { alignSelf: 'flex-start', fontSize: 12 }, children: t('retry') })] }));
113
- }
45
+ function ArenaPanelInner({ t }) {
46
+ const { payload, error, reload } = usePanel(PANEL_PATH);
47
+ const [runModal, setRunModal] = useState(null);
48
+ const header = (_jsxs("header", { style: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }, children: [_jsxs("strong", { style: { fontSize: 15 }, children: ["\u2694\uFE0F ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsx(Button, { variant: "secondary", size: "sm", onClick: reload, children: t('refresh') })] }));
49
+ if (error !== null)
50
+ return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx(Card, { children: _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13, color: 'var(--error, #e53935)' }, children: [t('failed'), ": ", error] }) })] });
114
51
  if (payload === null)
115
- return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
116
- return (_jsxs("div", { style: wrap, children: [header, _jsx(StatsSummary, { stats: payload.stats, t: t }), _jsx(Leaderboard, { entries: payload.leaderboard, t: t }), _jsxs("div", { style: section, children: [_jsxs("div", { style: sectionTitle, children: ["\uD83D\uDCCB ", t('recentRuns')] }), payload.recentRuns.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: t('run') }), _jsx("th", { style: th, children: t('models') }), _jsx("th", { style: th, children: t('winner') }), _jsx("th", { style: th, children: t('date') })] }) }), _jsx("tbody", { children: payload.recentRuns.map((row) => (_jsxs(_Fragment, { children: [_jsxs("tr", { style: clickRow, onClick: () => setExpandedRun(expandedRun === row.id ? null : row.id), children: [_jsxs("td", { style: td, children: [_jsx("code", { style: { fontSize: 11 }, children: row.id }), _jsx("span", { style: { marginLeft: 6, fontSize: 10 }, children: expandedRun === row.id ? '▼' : '▶' })] }), _jsx("td", { style: td, children: row.modelCount }), _jsx("td", { style: td, children: row.winner ?? '—' }), _jsx("td", { style: td, children: new Date(row.timestamp).toLocaleString() })] }, row.id), expandedRun === row.id && (_jsx("tr", { children: _jsx("td", { colSpan: 4, style: { padding: '4px 0' }, children: _jsx(RunDetail, { runId: row.id, t: t, onDelete: () => { setExpandedRun(null); reload(); } }) }) }, `${row.id}-detail`))] }))) })] }))] })] }));
52
+ return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx("div", { style: { display: 'flex', justifyContent: 'center', padding: 40 }, children: _jsx(Spinner, { size: 28 }) })] });
53
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 820 }, children: [header, _jsxs("div", { style: { display: 'flex', gap: 10, flexWrap: 'wrap' }, children: [_jsx(StatCard, { value: payload.stats.totalRuns, label: t('totalRuns') }), _jsx(StatCard, { value: payload.stats.uniqueModels, label: t('uniqueModels') }), _jsx(StatCard, { value: payload.stats.totalRatings, label: t('totalRatings') })] }), payload.leaderboard.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\uD83C\uDFC6", children: t('leaderboard') }), _jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: "#" }), _jsx("th", { style: tableStyles.th, children: t('model') }), _jsx("th", { style: tableStyles.th, children: "Elo" }), _jsx("th", { style: tableStyles.th, children: t('wins') }), _jsx("th", { style: tableStyles.th, children: t('avgRating') }), _jsx("th", { style: tableStyles.th, children: t('runs') })] }) }), _jsx("tbody", { children: payload.leaderboard.map((e, i) => (_jsxs("tr", { children: [_jsx("td", { style: tableStyles.td, children: i + 1 }), _jsx("td", { style: tableStyles.td, children: _jsx("strong", { children: e.model }) }), _jsx("td", { style: tableStyles.td, children: e.elo }), _jsx("td", { style: tableStyles.td, children: e.wins }), _jsx("td", { style: tableStyles.td, children: e.avgRating > 0 ? e.avgRating.toFixed(1) : '—' }), _jsx("td", { style: tableStyles.td, children: e.totalRuns })] }, e.model))) })] }) })] })), _jsx(SectionTitle, { icon: "\uD83D\uDCCB", children: t('recentRuns') }), payload.recentRuns.length === 0 ? _jsx(EmptyState, { icon: "\uD83D\uDCED", message: t('empty') }) : (_jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: t('run') }), _jsx("th", { style: tableStyles.th, children: t('models') }), _jsx("th", { style: tableStyles.th, children: t('winner') }), _jsx("th", { style: tableStyles.th, children: t('date') })] }) }), _jsx("tbody", { children: payload.recentRuns.map((row) => (_jsxs("tr", { style: tableStyles.clickRow, onClick: () => setRunModal(row.id), children: [_jsx("td", { style: tableStyles.td, children: _jsx("code", { style: { fontSize: 11 }, children: row.id.slice(0, 8) }) }), _jsx("td", { style: tableStyles.td, children: row.modelCount }), _jsx("td", { style: tableStyles.td, children: row.winner ?? '—' }), _jsx("td", { style: tableStyles.td, children: new Date(row.timestamp).toLocaleString() })] }, row.id))) })] }) })), runModal !== null && _jsx(RunModal, { runId: runModal, t: t, onClose: () => setRunModal(null), onDeleted: reload })] }));
54
+ }
55
+ export function ArenaPanel({ t }) {
56
+ return _jsx(ToastProvider, { children: _jsx(ArenaPanelInner, { t: t }) });
117
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-arena",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Run the same prompt through every model you have, and see the difference without squinting.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",