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.
- package/lib/arena.web.js +597 -183
- package/lib/arena.web.js.map +4 -4
- package/lib/client/index.js +1 -1
- package/lib/client/ui.d.ts +124 -0
- package/lib/client/ui.js +388 -0
- package/lib/client/view.d.ts +3 -17
- package/lib/client/view.js +39 -99
- package/package.json +1 -1
package/lib/client/ui.js
ADDED
|
@@ -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
|
+
}
|
package/lib/client/view.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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;
|
package/lib/client/view.js
CHANGED
|
@@ -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,
|
|
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:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
23
|
+
const [confirmDel, setConfirmDel] = useState(false);
|
|
24
|
+
useCallback(() => {
|
|
74
25
|
setLoading(true);
|
|
75
|
-
fetch(`${RUN_PATH}?id=${runId}`)
|
|
76
|
-
.then((
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}, [
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const [
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
116
|
-
return (_jsxs("div", { style:
|
|
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
|
}
|