free-coding-models 0.5.0 β 0.5.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/README.md +9 -1
- package/bin/free-coding-models.js +10 -0
- package/changelog/v0.5.1.md +24 -0
- package/package.json +7 -2
- package/src/core/router-daemon.js +166 -1
- package/src/core/utils.js +2 -0
- package/src/tui/cli-help.js +2 -0
- package/src/tui/render-table.js +1 -1
- package/web/README.md +8 -5
- package/web/dist/assets/index-ByGf4Kq-.js +14 -0
- package/web/dist/assets/index-Ds7wmHBv.css +1 -0
- package/web/dist/index.html +3 -6
- package/web/index.html +1 -4
- package/web/package.json +11 -0
- package/web/server.js +606 -211
- package/web/src/App.jsx +54 -12
- package/web/src/components/analytics/AnalyticsView.jsx +10 -4
- package/web/src/components/atoms/AILatencyCell.jsx +38 -0
- package/web/src/components/atoms/AILatencyCell.module.css +43 -0
- package/web/src/components/atoms/HealthCell.jsx +53 -0
- package/web/src/components/atoms/HealthCell.module.css +15 -0
- package/web/src/components/atoms/LastPingCell.jsx +35 -0
- package/web/src/components/atoms/LastPingCell.module.css +35 -0
- package/web/src/components/atoms/MoodCell.jsx +25 -0
- package/web/src/components/atoms/MoodCell.module.css +6 -0
- package/web/src/components/atoms/RankCell.jsx +9 -0
- package/web/src/components/atoms/RankCell.module.css +9 -0
- package/web/src/components/atoms/TPSCell.jsx +36 -0
- package/web/src/components/atoms/TPSCell.module.css +38 -0
- package/web/src/components/atoms/VerdictBadge.jsx +30 -7
- package/web/src/components/atoms/VerdictBadge.module.css +24 -15
- package/web/src/components/dashboard/ExportModal.jsx +9 -4
- package/web/src/components/dashboard/FilterBar.jsx +112 -10
- package/web/src/components/dashboard/FilterBar.module.css +86 -1
- package/web/src/components/dashboard/ModelTable.jsx +293 -52
- package/web/src/components/dashboard/ModelTable.module.css +131 -33
- package/web/src/components/dashboard/StatsBar.jsx +7 -5
- package/web/src/components/layout/Footer.jsx +1 -1
- package/web/src/components/layout/Header.jsx +43 -9
- package/web/src/components/layout/Header.module.css +38 -4
- package/web/src/components/layout/Sidebar.jsx +19 -11
- package/web/src/components/layout/Sidebar.module.css +15 -5
- package/web/src/components/settings/SettingsView.jsx +24 -6
- package/web/src/components/settings/SettingsView.module.css +0 -1
- package/web/src/global.css +70 -73
- package/web/src/hooks/useFilter.js +117 -25
- package/web/src/hooks/useSSE.js +33 -9
- package/web/src/hooks/useSocket.js +200 -0
- package/web/vite.config.js +41 -0
- package/web/dist/assets/index-CGN-0_A0.css +0 -1
- package/web/dist/assets/index-CvMUM9Jr.js +0 -11
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `onClose` callback, and `onToast` for feedback notifications.
|
|
6
6
|
* @functions ExportModal β renders the modal with three export option buttons
|
|
7
7
|
*/
|
|
8
|
+
import { IconUpload, IconBraces, IconFileSpreadsheet, IconClipboard } from '@tabler/icons-react'
|
|
8
9
|
import { downloadFile } from '../../utils/download.js'
|
|
9
10
|
import styles from './ExportModal.module.css'
|
|
10
11
|
|
|
@@ -45,27 +46,30 @@ export default function ExportModal({ models, onClose, onToast }) {
|
|
|
45
46
|
<div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
|
|
46
47
|
<div className={styles.modal}>
|
|
47
48
|
<div className={styles.modalHeader}>
|
|
48
|
-
<h2 className={styles.modalTitle}
|
|
49
|
+
<h2 className={styles.modalTitle}>
|
|
50
|
+
<IconUpload size={20} stroke={1.5} style={{ marginRight: 8, verticalAlign: 'middle' }} />
|
|
51
|
+
Export Data
|
|
52
|
+
</h2>
|
|
49
53
|
<button className={styles.modalClose} onClick={onClose}>×</button>
|
|
50
54
|
</div>
|
|
51
55
|
<div className={styles.modalBody}>
|
|
52
56
|
<div className={styles.options}>
|
|
53
57
|
<button className={styles.option} onClick={handleJson}>
|
|
54
|
-
<span className={styles.optionIcon}
|
|
58
|
+
<span className={styles.optionIcon}><IconBraces size={20} stroke={1.5} /></span>
|
|
55
59
|
<div>
|
|
56
60
|
<span className={styles.optionLabel}>Export as JSON</span>
|
|
57
61
|
<span className={styles.optionDesc}>Full model data with all metrics</span>
|
|
58
62
|
</div>
|
|
59
63
|
</button>
|
|
60
64
|
<button className={styles.option} onClick={handleCsv}>
|
|
61
|
-
<span className={styles.optionIcon}
|
|
65
|
+
<span className={styles.optionIcon}><IconFileSpreadsheet size={20} stroke={1.5} /></span>
|
|
62
66
|
<div>
|
|
63
67
|
<span className={styles.optionLabel}>Export as CSV</span>
|
|
64
68
|
<span className={styles.optionDesc}>Spreadsheet-compatible format</span>
|
|
65
69
|
</div>
|
|
66
70
|
</button>
|
|
67
71
|
<button className={styles.option} onClick={handleClipboard}>
|
|
68
|
-
<span className={styles.optionIcon}
|
|
72
|
+
<span className={styles.optionIcon}><IconClipboard size={20} stroke={1.5} /></span>
|
|
69
73
|
<div>
|
|
70
74
|
<span className={styles.optionLabel}>Copy to Clipboard</span>
|
|
71
75
|
<span className={styles.optionDesc}>Copy model summary as text</span>
|
|
@@ -77,3 +81,4 @@ export default function ExportModal({ models, onClose, onToast }) {
|
|
|
77
81
|
</div>
|
|
78
82
|
)
|
|
79
83
|
}
|
|
84
|
+
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/components/dashboard/FilterBar.jsx
|
|
3
|
-
* @description Filter controls for tier, status, provider,
|
|
3
|
+
* @description Filter controls for tier, status, provider, ping interval, and live indicator.
|
|
4
|
+
* π Shows "Next ping in Xs" countdown matching TUI behavior. Ping mode selector (Speed/Normal/Slow/Forced).
|
|
4
5
|
*/
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
5
7
|
import styles from './FilterBar.module.css'
|
|
6
8
|
|
|
7
|
-
const TIERS = [
|
|
9
|
+
const TIERS = [
|
|
10
|
+
{ key: 'all', label: 'All' },
|
|
11
|
+
{ key: 'S+', label: 'S+' },
|
|
12
|
+
{ key: 'S', label: 'S' },
|
|
13
|
+
{ key: 'A+', label: 'A+' },
|
|
14
|
+
{ key: 'A', label: 'A' },
|
|
15
|
+
{ key: 'A-', label: 'A-' },
|
|
16
|
+
{ key: 'B+', label: 'B+' },
|
|
17
|
+
{ key: 'B', label: 'B' },
|
|
18
|
+
{ key: 'C', label: 'C' },
|
|
19
|
+
]
|
|
8
20
|
const STATUSES = [
|
|
9
21
|
{ key: 'all', label: 'All' },
|
|
10
22
|
{ key: 'up', label: 'Online' },
|
|
@@ -12,24 +24,82 @@ const STATUSES = [
|
|
|
12
24
|
{ key: 'pending', label: 'Pending' },
|
|
13
25
|
]
|
|
14
26
|
|
|
27
|
+
const PING_MODES = [
|
|
28
|
+
{ key: 'speed', label: 'β‘ Speed', interval: '2s', color: '#00ff88' },
|
|
29
|
+
{ key: 'normal', label: 'β Normal', interval: '10s', color: '#ffaa00' },
|
|
30
|
+
{ key: 'slow', label: 'π’ Slow', interval: '30s', color: '#ff6644' },
|
|
31
|
+
{ key: 'forced', label: 'π₯ Forced', interval: '4s', color: '#ff4466' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
function formatCountdown(ms) {
|
|
35
|
+
if (ms == null) return null
|
|
36
|
+
const s = Math.max(0, Math.ceil(ms / 1000))
|
|
37
|
+
if (s === 1) return '1s'
|
|
38
|
+
if (s < 60) return `${s}s`
|
|
39
|
+
const m = Math.floor(s / 60)
|
|
40
|
+
const rem = s % 60
|
|
41
|
+
return `${m}m${rem > 0 ? rem + 's' : ''}`
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
export default function FilterBar({
|
|
16
45
|
filterTier, setFilterTier,
|
|
17
46
|
filterStatus, setFilterStatus,
|
|
18
47
|
filterProvider, setFilterProvider,
|
|
19
48
|
providers,
|
|
49
|
+
pingMode, setPingMode,
|
|
50
|
+
nextPingAt,
|
|
51
|
+
isPinging,
|
|
52
|
+
globalBenchmarkRunning,
|
|
53
|
+
globalBenchmarkTotal,
|
|
54
|
+
globalBenchmarkCompleted,
|
|
20
55
|
}) {
|
|
56
|
+
const [countdown, setCountdown] = useState(null)
|
|
57
|
+
|
|
58
|
+
// Update countdown every second when nextPingAt is set
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (nextPingAt == null) return
|
|
61
|
+
const tick = () => {
|
|
62
|
+
const rem = nextPingAt - Date.now()
|
|
63
|
+
setCountdown(rem > 0 ? rem : 0)
|
|
64
|
+
}
|
|
65
|
+
tick()
|
|
66
|
+
const id = setInterval(tick, 1000)
|
|
67
|
+
return () => clearInterval(id)
|
|
68
|
+
}, [nextPingAt])
|
|
69
|
+
|
|
70
|
+
const countdownDisplay = countdown !== null ? formatCountdown(countdown) : null
|
|
71
|
+
|
|
72
|
+
// Global benchmark progress percentage
|
|
73
|
+
const benchmarkPct = globalBenchmarkRunning && globalBenchmarkTotal > 0
|
|
74
|
+
? Math.round((globalBenchmarkCompleted / globalBenchmarkTotal) * 100)
|
|
75
|
+
: 0
|
|
76
|
+
|
|
21
77
|
return (
|
|
22
78
|
<section className={styles.filters}>
|
|
79
|
+
{/* ββ Global benchmark progress bar (Ctrl+U) ββ */}
|
|
80
|
+
{globalBenchmarkRunning && (
|
|
81
|
+
<div className={styles.benchmarkBar}>
|
|
82
|
+
<div className={styles.benchmarkLabel}>
|
|
83
|
+
<span className={styles.benchmarkSpinner} />
|
|
84
|
+
<span>AI Speed Test</span>
|
|
85
|
+
<span className={styles.benchmarkCount}>{globalBenchmarkCompleted}/{globalBenchmarkTotal}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div className={styles.benchmarkTrack}>
|
|
88
|
+
<div className={styles.benchmarkFill} style={{ width: `${benchmarkPct}%` }} />
|
|
89
|
+
</div>
|
|
90
|
+
<span className={styles.benchmarkPct}>{benchmarkPct}%</span>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
23
93
|
<div className={styles.group}>
|
|
24
94
|
<label className={styles.filterLabel}>Tier</label>
|
|
25
95
|
<div className={styles.tierRow}>
|
|
26
96
|
{TIERS.map(t => (
|
|
27
97
|
<button
|
|
28
|
-
key={t}
|
|
29
|
-
className={`${styles.tierBtn} ${filterTier === t ? styles.active : ''}`}
|
|
30
|
-
onClick={() => setFilterTier(t)}
|
|
98
|
+
key={t.key}
|
|
99
|
+
className={`${styles.tierBtn} ${filterTier === t.key ? styles.active : ''}`}
|
|
100
|
+
onClick={() => setFilterTier(t.key)}
|
|
31
101
|
>
|
|
32
|
-
{t}
|
|
102
|
+
{t.label}
|
|
33
103
|
</button>
|
|
34
104
|
))}
|
|
35
105
|
</div>
|
|
@@ -62,12 +132,44 @@ export default function FilterBar({
|
|
|
62
132
|
</select>
|
|
63
133
|
</div>
|
|
64
134
|
<div className={styles.spacer} />
|
|
135
|
+
|
|
136
|
+
{/* ββ Ping interval selector ββ */}
|
|
65
137
|
<div className={styles.group}>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
138
|
+
<label className={styles.filterLabel}>Ping</label>
|
|
139
|
+
<div className={styles.pingRow}>
|
|
140
|
+
{PING_MODES.map(m => (
|
|
141
|
+
<button
|
|
142
|
+
key={m.key}
|
|
143
|
+
className={`${styles.pingBtn} ${pingMode === m.key ? styles.pingBtnActive : ''}`}
|
|
144
|
+
style={pingMode === m.key ? { '--ping-active-color': m.color } : {}}
|
|
145
|
+
onClick={() => setPingMode(m.key)}
|
|
146
|
+
title={`${m.interval} interval`}
|
|
147
|
+
>
|
|
148
|
+
{m.label}
|
|
149
|
+
</button>
|
|
150
|
+
))}
|
|
69
151
|
</div>
|
|
70
152
|
</div>
|
|
153
|
+
|
|
154
|
+
{/* ββ Next ping countdown + LIVE ββ */}
|
|
155
|
+
<div className={styles.group}>
|
|
156
|
+
{isPinging ? (
|
|
157
|
+
<div className={styles.nextPing} title="Pinging nowβ¦">
|
|
158
|
+
<span className={styles.pingingDot} />
|
|
159
|
+
<span className={styles.pingingText}>Pingingβ¦</span>
|
|
160
|
+
</div>
|
|
161
|
+
) : countdownDisplay ? (
|
|
162
|
+
<div className={styles.nextPing} title="Next ping countdown">
|
|
163
|
+
<span className={styles.nextPingLabel}>Next</span>
|
|
164
|
+
<span className={styles.nextPingTime}>{countdownDisplay}</span>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<div className={styles.live}>
|
|
168
|
+
<span className={styles.liveDot} />
|
|
169
|
+
<span>LIVE</span>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
71
173
|
</section>
|
|
72
174
|
)
|
|
73
|
-
}
|
|
175
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
.tierBtn:hover { background: var(--color-bg-hover); color: var(--color-text); }
|
|
19
19
|
.active {
|
|
20
|
-
background: var(--color-accent) !important; color:
|
|
20
|
+
background: var(--color-accent) !important; color: var(--color-bg) !important;
|
|
21
21
|
border-color: var(--color-accent) !important; font-weight: 700;
|
|
22
22
|
}
|
|
23
23
|
.providerSelect {
|
|
@@ -37,7 +37,92 @@
|
|
|
37
37
|
width: 8px; height: 8px; border-radius: 50%;
|
|
38
38
|
background: var(--color-accent); animation: pulseDot 2s ease-in-out infinite;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/* ββ Next ping countdown ββ */
|
|
42
|
+
.nextPing {
|
|
43
|
+
display: flex; align-items: center; gap: 6px;
|
|
44
|
+
font-size: 12px; font-family: var(--font-mono);
|
|
45
|
+
}
|
|
46
|
+
.nextPingLabel {
|
|
47
|
+
font-size: 11px; font-weight: 500; color: var(--color-text-muted);
|
|
48
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
49
|
+
}
|
|
50
|
+
.nextPingTime {
|
|
51
|
+
font-size: 14px; font-weight: 700; color: var(--color-text);
|
|
52
|
+
min-width: 32px;
|
|
53
|
+
}
|
|
54
|
+
.pingingDot {
|
|
55
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
56
|
+
background: #ff4466; animation: pulseDot 0.6s ease-in-out infinite;
|
|
57
|
+
}
|
|
58
|
+
.pingingText {
|
|
59
|
+
font-size: 12px; font-weight: 600; color: #ff4466;
|
|
60
|
+
font-family: var(--font-mono);
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
@keyframes pulseDot {
|
|
41
64
|
0%, 100% { box-shadow: 0 0 0 0 var(--color-accent-glow); }
|
|
42
65
|
50% { box-shadow: 0 0 0 6px transparent; }
|
|
43
66
|
}
|
|
67
|
+
|
|
68
|
+
/* ββ Ping interval selector ββ */
|
|
69
|
+
.pingRow { display: flex; gap: 4px; }
|
|
70
|
+
.pingBtn {
|
|
71
|
+
font-size: 12px; font-weight: 600; font-family: var(--font-mono);
|
|
72
|
+
padding: 4px 10px; border-radius: 6px;
|
|
73
|
+
border: 1px solid var(--color-border); background: var(--color-surface);
|
|
74
|
+
color: var(--color-text-muted); cursor: pointer; transition: all 150ms;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
.pingBtn:hover { background: var(--color-bg-hover); color: var(--color-text); border-color: var(--color-text-muted); }
|
|
78
|
+
.pingBtnActive {
|
|
79
|
+
background: var(--color-bg-hover) !important;
|
|
80
|
+
color: var(--ping-active-color, var(--color-accent)) !important;
|
|
81
|
+
border-color: var(--ping-active-color, var(--color-accent)) !important;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ββ Global benchmark progress bar (Ctrl+U) ββ */
|
|
86
|
+
.benchmarkBar {
|
|
87
|
+
display: flex; align-items: center; gap: 12px;
|
|
88
|
+
background: rgba(180, 0, 255, 0.12);
|
|
89
|
+
border: 1px solid rgba(180, 0, 255, 0.4);
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
padding: 8px 14px;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
}
|
|
94
|
+
.benchmarkLabel {
|
|
95
|
+
display: flex; align-items: center; gap: 6px;
|
|
96
|
+
font-size: 12px; font-weight: 700;
|
|
97
|
+
color: #b400ff; font-family: var(--font-mono);
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
}
|
|
100
|
+
.benchmarkSpinner {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
animation: spinSp 0.8s linear infinite;
|
|
103
|
+
font-size: 14px; line-height: 1;
|
|
104
|
+
}
|
|
105
|
+
.benchmarkCount {
|
|
106
|
+
font-size: 11px; opacity: 0.8;
|
|
107
|
+
}
|
|
108
|
+
.benchmarkTrack {
|
|
109
|
+
width: 120px; height: 6px;
|
|
110
|
+
background: rgba(255,255,255,0.1);
|
|
111
|
+
border-radius: 3px; overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.benchmarkFill {
|
|
114
|
+
height: 100%;
|
|
115
|
+
background: linear-gradient(90deg, #b400ff, #ff00ff);
|
|
116
|
+
border-radius: 3px;
|
|
117
|
+
transition: width 0.5s ease;
|
|
118
|
+
}
|
|
119
|
+
.benchmarkPct {
|
|
120
|
+
font-size: 12px; font-weight: 700;
|
|
121
|
+
color: #b400ff; font-family: var(--font-mono);
|
|
122
|
+
min-width: 36px; text-align: right;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@keyframes spinSp {
|
|
126
|
+
from { transform: rotate(0deg); }
|
|
127
|
+
to { transform: rotate(360deg); }
|
|
128
|
+
}
|