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.
Files changed (51) hide show
  1. package/README.md +9 -1
  2. package/bin/free-coding-models.js +10 -0
  3. package/changelog/v0.5.1.md +24 -0
  4. package/package.json +7 -2
  5. package/src/core/router-daemon.js +166 -1
  6. package/src/core/utils.js +2 -0
  7. package/src/tui/cli-help.js +2 -0
  8. package/src/tui/render-table.js +1 -1
  9. package/web/README.md +8 -5
  10. package/web/dist/assets/index-ByGf4Kq-.js +14 -0
  11. package/web/dist/assets/index-Ds7wmHBv.css +1 -0
  12. package/web/dist/index.html +3 -6
  13. package/web/index.html +1 -4
  14. package/web/package.json +11 -0
  15. package/web/server.js +606 -211
  16. package/web/src/App.jsx +54 -12
  17. package/web/src/components/analytics/AnalyticsView.jsx +10 -4
  18. package/web/src/components/atoms/AILatencyCell.jsx +38 -0
  19. package/web/src/components/atoms/AILatencyCell.module.css +43 -0
  20. package/web/src/components/atoms/HealthCell.jsx +53 -0
  21. package/web/src/components/atoms/HealthCell.module.css +15 -0
  22. package/web/src/components/atoms/LastPingCell.jsx +35 -0
  23. package/web/src/components/atoms/LastPingCell.module.css +35 -0
  24. package/web/src/components/atoms/MoodCell.jsx +25 -0
  25. package/web/src/components/atoms/MoodCell.module.css +6 -0
  26. package/web/src/components/atoms/RankCell.jsx +9 -0
  27. package/web/src/components/atoms/RankCell.module.css +9 -0
  28. package/web/src/components/atoms/TPSCell.jsx +36 -0
  29. package/web/src/components/atoms/TPSCell.module.css +38 -0
  30. package/web/src/components/atoms/VerdictBadge.jsx +30 -7
  31. package/web/src/components/atoms/VerdictBadge.module.css +24 -15
  32. package/web/src/components/dashboard/ExportModal.jsx +9 -4
  33. package/web/src/components/dashboard/FilterBar.jsx +112 -10
  34. package/web/src/components/dashboard/FilterBar.module.css +86 -1
  35. package/web/src/components/dashboard/ModelTable.jsx +293 -52
  36. package/web/src/components/dashboard/ModelTable.module.css +131 -33
  37. package/web/src/components/dashboard/StatsBar.jsx +7 -5
  38. package/web/src/components/layout/Footer.jsx +1 -1
  39. package/web/src/components/layout/Header.jsx +43 -9
  40. package/web/src/components/layout/Header.module.css +38 -4
  41. package/web/src/components/layout/Sidebar.jsx +19 -11
  42. package/web/src/components/layout/Sidebar.module.css +15 -5
  43. package/web/src/components/settings/SettingsView.jsx +24 -6
  44. package/web/src/components/settings/SettingsView.module.css +0 -1
  45. package/web/src/global.css +70 -73
  46. package/web/src/hooks/useFilter.js +117 -25
  47. package/web/src/hooks/useSSE.js +33 -9
  48. package/web/src/hooks/useSocket.js +200 -0
  49. package/web/vite.config.js +41 -0
  50. package/web/dist/assets/index-CGN-0_A0.css +0 -1
  51. 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}>πŸ“€ Export Data</h2>
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}>&times;</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}>{'{ }'}</span>
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}>πŸ“Š</span>
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}>πŸ“‹</span>
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, and search + live indicator.
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 = ['All', 'S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
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
- <div className={styles.live}>
67
- <span className={styles.liveDot} />
68
- <span>LIVE</span>
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: #000 !important;
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
+ }