free-coding-models 0.3.36 → 0.3.40

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 (58) hide show
  1. package/CHANGELOG.md +10 -1798
  2. package/README.md +4 -1
  3. package/bin/free-coding-models.js +1 -1
  4. package/package.json +11 -2
  5. package/src/app.js +3 -0
  6. package/src/cli-help.js +2 -0
  7. package/src/command-palette.js +3 -0
  8. package/src/endpoint-installer.js +1 -1
  9. package/src/tool-bootstrap.js +34 -0
  10. package/src/tool-launchers.js +137 -1
  11. package/src/tool-metadata.js +9 -0
  12. package/src/utils.js +8 -2
  13. package/web/index.html +2 -300
  14. package/web/server.js +80 -15
  15. package/web/src/App.jsx +150 -0
  16. package/web/src/components/analytics/AnalyticsView.jsx +109 -0
  17. package/web/src/components/analytics/AnalyticsView.module.css +186 -0
  18. package/web/src/components/atoms/Sparkline.jsx +44 -0
  19. package/web/src/components/atoms/StabilityCell.jsx +18 -0
  20. package/web/src/components/atoms/StabilityCell.module.css +8 -0
  21. package/web/src/components/atoms/StatusDot.jsx +10 -0
  22. package/web/src/components/atoms/StatusDot.module.css +17 -0
  23. package/web/src/components/atoms/TierBadge.jsx +10 -0
  24. package/web/src/components/atoms/TierBadge.module.css +18 -0
  25. package/web/src/components/atoms/Toast.jsx +25 -0
  26. package/web/src/components/atoms/Toast.module.css +35 -0
  27. package/web/src/components/atoms/ToastContainer.jsx +16 -0
  28. package/web/src/components/atoms/ToastContainer.module.css +10 -0
  29. package/web/src/components/atoms/VerdictBadge.jsx +13 -0
  30. package/web/src/components/atoms/VerdictBadge.module.css +19 -0
  31. package/web/src/components/dashboard/DetailPanel.jsx +131 -0
  32. package/web/src/components/dashboard/DetailPanel.module.css +99 -0
  33. package/web/src/components/dashboard/ExportModal.jsx +79 -0
  34. package/web/src/components/dashboard/ExportModal.module.css +99 -0
  35. package/web/src/components/dashboard/FilterBar.jsx +73 -0
  36. package/web/src/components/dashboard/FilterBar.module.css +43 -0
  37. package/web/src/components/dashboard/ModelTable.jsx +86 -0
  38. package/web/src/components/dashboard/ModelTable.module.css +46 -0
  39. package/web/src/components/dashboard/StatsBar.jsx +40 -0
  40. package/web/src/components/dashboard/StatsBar.module.css +28 -0
  41. package/web/src/components/layout/Footer.jsx +19 -0
  42. package/web/src/components/layout/Footer.module.css +10 -0
  43. package/web/src/components/layout/Header.jsx +38 -0
  44. package/web/src/components/layout/Header.module.css +73 -0
  45. package/web/src/components/layout/Sidebar.jsx +41 -0
  46. package/web/src/components/layout/Sidebar.module.css +76 -0
  47. package/web/src/components/settings/SettingsView.jsx +264 -0
  48. package/web/src/components/settings/SettingsView.module.css +377 -0
  49. package/web/src/global.css +199 -0
  50. package/web/src/hooks/useFilter.js +83 -0
  51. package/web/src/hooks/useSSE.js +49 -0
  52. package/web/src/hooks/useTheme.js +27 -0
  53. package/web/src/main.jsx +15 -0
  54. package/web/src/utils/download.js +15 -0
  55. package/web/src/utils/format.js +42 -0
  56. package/web/src/utils/ranks.js +37 -0
  57. /package/web/{app.js → app.legacy.js} +0 -0
  58. /package/web/{styles.css → styles.legacy.css} +0 -0
package/web/index.html CHANGED
@@ -8,311 +8,13 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="/styles.css">
12
11
  </head>
13
12
  <body>
14
- <!-- ─── Background Effects ─── -->
15
13
  <div class="bg-grid"></div>
16
14
  <div class="bg-glow bg-glow--1"></div>
17
15
  <div class="bg-glow bg-glow--2"></div>
18
16
  <div class="bg-glow bg-glow--3"></div>
19
-
20
- <!-- ─── Toast Container ─── -->
21
- <div class="toast-container" id="toast-container"></div>
22
-
23
- <!-- ─── Sidebar ─── -->
24
- <aside class="sidebar" id="sidebar">
25
- <div class="sidebar__logo">
26
- <span class="sidebar__logo-icon">⚡</span>
27
- <span class="sidebar__logo-text">FCM</span>
28
- </div>
29
- <nav class="sidebar__nav">
30
- <button class="sidebar__nav-item sidebar__nav-item--active" data-view="dashboard" id="nav-dashboard" title="Dashboard">
31
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
32
- <span class="sidebar__nav-label">Dashboard</span>
33
- </button>
34
- <button class="sidebar__nav-item" data-view="settings" id="nav-settings" title="Settings">
35
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
36
- <span class="sidebar__nav-label">Settings</span>
37
- </button>
38
- <button class="sidebar__nav-item" data-view="analytics" id="nav-analytics" title="Analytics">
39
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
40
- <span class="sidebar__nav-label">Analytics</span>
41
- </button>
42
- </nav>
43
- <div class="sidebar__bottom">
44
- <button class="sidebar__nav-item" id="sidebar-theme-toggle" title="Toggle Theme">
45
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
46
- <span class="sidebar__nav-label">Theme</span>
47
- </button>
48
- </div>
49
- </aside>
50
-
51
- <!-- ─── Main Content ─── -->
52
- <div class="app-content" id="app-content">
53
-
54
- <!-- ═══════ DASHBOARD VIEW ═══════ -->
55
- <div class="view" id="view-dashboard">
56
-
57
- <!-- Header -->
58
- <header class="header" id="header">
59
- <div class="header__left">
60
- <button class="btn btn--icon sidebar-toggle" id="sidebar-toggle" title="Toggle Sidebar">
61
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
62
- </button>
63
- <div class="logo">
64
- <span class="logo__icon">⚡</span>
65
- <span class="logo__text">free-coding-models</span>
66
- </div>
67
- <span class="header__version" id="version-badge">v0.3.35</span>
68
- </div>
69
- <div class="header__center">
70
- <div class="search-bar" id="search-bar">
71
- <svg class="search-bar__icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
72
- <input type="text" class="search-bar__input" id="search-input" placeholder="Search models, providers, tiers..." autocomplete="off">
73
- <kbd class="search-bar__kbd">Ctrl+K</kbd>
74
- </div>
75
- </div>
76
- <div class="header__right">
77
- <button class="btn btn--icon" id="theme-toggle" title="Toggle theme">
78
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
79
- </button>
80
- <button class="btn btn--icon" id="export-btn" title="Export Data">
81
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
82
- </button>
83
- <button class="btn btn--primary" id="settings-btn">
84
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
85
- Settings
86
- </button>
87
- </div>
88
- </header>
89
-
90
- <!-- Stats Bar -->
91
- <section class="stats-bar" id="stats-bar">
92
- <div class="stat-card" id="stat-total">
93
- <div class="stat-card__icon">📊</div>
94
- <div class="stat-card__body">
95
- <div class="stat-card__value" id="stat-total-value">0</div>
96
- <div class="stat-card__label">Total Models</div>
97
- </div>
98
- </div>
99
- <div class="stat-card" id="stat-online">
100
- <div class="stat-card__icon">🟢</div>
101
- <div class="stat-card__body">
102
- <div class="stat-card__value" id="stat-online-value">0</div>
103
- <div class="stat-card__label">Online</div>
104
- </div>
105
- </div>
106
- <div class="stat-card" id="stat-avg">
107
- <div class="stat-card__icon">⚡</div>
108
- <div class="stat-card__body">
109
- <div class="stat-card__value" id="stat-avg-value">—</div>
110
- <div class="stat-card__label">Avg Latency</div>
111
- </div>
112
- </div>
113
- <div class="stat-card" id="stat-best">
114
- <div class="stat-card__icon">🏆</div>
115
- <div class="stat-card__body">
116
- <div class="stat-card__value" id="stat-best-value">—</div>
117
- <div class="stat-card__label">Fastest Model</div>
118
- </div>
119
- </div>
120
- <div class="stat-card" id="stat-providers">
121
- <div class="stat-card__icon">🌐</div>
122
- <div class="stat-card__body">
123
- <div class="stat-card__value" id="stat-providers-value">0</div>
124
- <div class="stat-card__label">Providers</div>
125
- </div>
126
- </div>
127
- </section>
128
-
129
- <!-- Filters -->
130
- <section class="filters" id="filters">
131
- <div class="filters__group">
132
- <label class="filter-label">Tier</label>
133
- <div class="tier-filters" id="tier-filters">
134
- <button class="tier-btn tier-btn--active" data-tier="all">All</button>
135
- <button class="tier-btn" data-tier="S+">S+</button>
136
- <button class="tier-btn" data-tier="S">S</button>
137
- <button class="tier-btn" data-tier="A+">A+</button>
138
- <button class="tier-btn" data-tier="A">A</button>
139
- <button class="tier-btn" data-tier="A-">A-</button>
140
- <button class="tier-btn" data-tier="B+">B+</button>
141
- <button class="tier-btn" data-tier="B">B</button>
142
- <button class="tier-btn" data-tier="C">C</button>
143
- </div>
144
- </div>
145
- <div class="filters__group">
146
- <label class="filter-label">Status</label>
147
- <div class="status-filters" id="status-filters">
148
- <button class="status-btn status-btn--active" data-status="all">All</button>
149
- <button class="status-btn" data-status="up">Online</button>
150
- <button class="status-btn" data-status="down">Offline</button>
151
- <button class="status-btn" data-status="pending">Pending</button>
152
- </div>
153
- </div>
154
- <div class="filters__group">
155
- <label class="filter-label">Provider</label>
156
- <select class="provider-select" id="provider-filter">
157
- <option value="all">All Providers</option>
158
- </select>
159
- </div>
160
- <div class="filters__spacer"></div>
161
- <div class="filters__group">
162
- <div class="live-indicator" id="live-indicator">
163
- <span class="live-dot"></span>
164
- <span class="live-text">LIVE</span>
165
- </div>
166
- </div>
167
- </section>
168
-
169
- <!-- Main Table -->
170
- <main class="main" id="main">
171
- <div class="table-container" id="table-container">
172
- <table class="models-table" id="models-table">
173
- <thead>
174
- <tr>
175
- <th class="th--rank sortable" data-sort="idx">#</th>
176
- <th class="th--tier sortable" data-sort="tier">Tier</th>
177
- <th class="th--model sortable" data-sort="label">Model</th>
178
- <th class="th--provider sortable" data-sort="origin">Provider</th>
179
- <th class="th--swe sortable" data-sort="sweScore">SWE %</th>
180
- <th class="th--ctx sortable" data-sort="ctx">Context</th>
181
- <th class="th--ping sortable" data-sort="latestPing">Ping</th>
182
- <th class="th--avg sortable" data-sort="avg">Avg</th>
183
- <th class="th--stability sortable" data-sort="stability">Stability</th>
184
- <th class="th--verdict sortable" data-sort="verdict">Verdict</th>
185
- <th class="th--uptime sortable" data-sort="uptime">Uptime</th>
186
- <th class="th--sparkline">Trend</th>
187
- </tr>
188
- </thead>
189
- <tbody id="table-body">
190
- <tr class="loading-row">
191
- <td colspan="12">
192
- <div class="loading-spinner">
193
- <div class="spinner"></div>
194
- <span>Connecting to ping engine...</span>
195
- </div>
196
- </td>
197
- </tr>
198
- </tbody>
199
- </table>
200
- </div>
201
- </main>
202
- </div>
203
-
204
- <!-- ═══════ SETTINGS VIEW ═══════ -->
205
- <div class="view view--hidden" id="view-settings">
206
- <div class="settings-page">
207
- <div class="settings-page__header">
208
- <h1 class="settings-page__title">⚙️ Provider Settings</h1>
209
- <p class="settings-page__subtitle">Manage your API keys and provider configurations. Keys are stored locally in <code>~/.free-coding-models.json</code></p>
210
- </div>
211
-
212
- <!-- Settings Toolbar -->
213
- <div class="settings-toolbar" id="settings-toolbar">
214
- <div class="settings-toolbar__search">
215
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
216
- <input type="text" id="settings-search" placeholder="Search providers..." autocomplete="off">
217
- </div>
218
- <div class="settings-toolbar__actions">
219
- <button class="btn" id="settings-expand-all">Expand All</button>
220
- <button class="btn" id="settings-collapse-all">Collapse All</button>
221
- </div>
222
- </div>
223
-
224
- <!-- Provider Cards Container -->
225
- <div class="settings-providers" id="settings-providers">
226
- <!-- Populated by JS -->
227
- </div>
228
- </div>
229
- </div>
230
-
231
- <!-- ═══════ ANALYTICS VIEW ═══════ -->
232
- <div class="view view--hidden" id="view-analytics">
233
- <div class="analytics-page">
234
- <div class="analytics-page__header">
235
- <h1 class="analytics-page__title">📊 Analytics</h1>
236
- <p class="analytics-page__subtitle">Real-time insights across all providers and models</p>
237
- </div>
238
- <div class="analytics-grid" id="analytics-grid">
239
- <!-- Provider Health Overview -->
240
- <div class="analytics-card analytics-card--wide" id="analytics-provider-health">
241
- <h3 class="analytics-card__title">Provider Health Overview</h3>
242
- <div class="analytics-card__body" id="provider-health-body">
243
- <!-- Populated by JS -->
244
- </div>
245
- </div>
246
- <!-- Top 10 Fastest -->
247
- <div class="analytics-card" id="analytics-leaderboard">
248
- <h3 class="analytics-card__title">🏆 Fastest Models</h3>
249
- <div class="analytics-card__body" id="leaderboard-body">
250
- <!-- Populated by JS -->
251
- </div>
252
- </div>
253
- <!-- Tier Distribution -->
254
- <div class="analytics-card" id="analytics-tier-dist">
255
- <h3 class="analytics-card__title">Tier Distribution</h3>
256
- <div class="analytics-card__body" id="tier-dist-body">
257
- <!-- Populated by JS -->
258
- </div>
259
- </div>
260
- </div>
261
- </div>
262
- </div>
263
-
264
- <!-- ─── Footer ─── -->
265
- <footer class="footer">
266
- <div class="footer__left">
267
- <span>Made with ❤️ by <a href="https://vavanessa.dev" target="_blank">Vava-Nessa</a></span>
268
- </div>
269
- <div class="footer__right">
270
- <a href="https://github.com/vava-nessa/free-coding-models" target="_blank">GitHub</a>
271
- <a href="https://discord.gg/ZTNFHvvCkU" target="_blank">Discord</a>
272
- </div>
273
- </footer>
274
- </div>
275
-
276
- <!-- ─── Model Detail Panel ─── -->
277
- <div class="detail-panel" id="detail-panel" hidden>
278
- <div class="detail-panel__header">
279
- <h3 class="detail-panel__title" id="detail-title">Model Details</h3>
280
- <button class="detail-panel__close" id="detail-close">&times;</button>
281
- </div>
282
- <div class="detail-panel__body" id="detail-body">
283
- <!-- Populated by JS -->
284
- </div>
285
- </div>
286
-
287
- <!-- ─── Export Modal ─── -->
288
- <div class="modal-overlay" id="export-modal" hidden>
289
- <div class="modal">
290
- <div class="modal__header">
291
- <h2 class="modal__title">📤 Export Data</h2>
292
- <button class="modal__close" id="export-close">&times;</button>
293
- </div>
294
- <div class="modal__body">
295
- <div class="export-options">
296
- <button class="export-option" id="export-json">
297
- <span class="export-option__icon">{ }</span>
298
- <span class="export-option__label">Export as JSON</span>
299
- <span class="export-option__desc">Full model data with all metrics</span>
300
- </button>
301
- <button class="export-option" id="export-csv">
302
- <span class="export-option__icon">📊</span>
303
- <span class="export-option__label">Export as CSV</span>
304
- <span class="export-option__desc">Spreadsheet-compatible format</span>
305
- </button>
306
- <button class="export-option" id="export-clipboard">
307
- <span class="export-option__icon">📋</span>
308
- <span class="export-option__label">Copy to Clipboard</span>
309
- <span class="export-option__desc">Copy model summary as text</span>
310
- </button>
311
- </div>
312
- </div>
313
- </div>
314
- </div>
315
-
316
- <script src="/app.js" type="module"></script>
17
+ <div id="root"></div>
18
+ <script type="module" src="/src/main.jsx"></script>
317
19
  </body>
318
20
  </html>
package/web/server.js CHANGED
@@ -18,9 +18,10 @@
18
18
  */
19
19
 
20
20
  import { createServer } from 'node:http'
21
- import { readFileSync } from 'node:fs'
22
- import { join, dirname } from 'node:path'
21
+ import { readFileSync, existsSync } from 'node:fs'
22
+ import { join, dirname, extname } from 'node:path'
23
23
  import { fileURLToPath } from 'node:url'
24
+ import { exec } from 'node:child_process'
24
25
 
25
26
  import { sources, MODELS } from '../sources.js'
26
27
  import { loadConfig, getApiKey, saveConfig, isProviderEnabled } from '../src/config.js'
@@ -174,6 +175,16 @@ function maskApiKey(key) {
174
175
 
175
176
  // ─── HTTP Server ─────────────────────────────────────────────────────────────
176
177
 
178
+ const MIME_TYPES = {
179
+ '.html': 'text/html; charset=utf-8',
180
+ '.css': 'text/css; charset=utf-8',
181
+ '.js': 'application/javascript; charset=utf-8',
182
+ '.json': 'application/json; charset=utf-8',
183
+ '.svg': 'image/svg+xml',
184
+ '.png': 'image/png',
185
+ '.ico': 'image/x-icon',
186
+ }
187
+
177
188
  function serveFile(res, filename, contentType) {
178
189
  try {
179
190
  const content = readFileSync(join(__dirname, filename), 'utf8')
@@ -185,6 +196,24 @@ function serveFile(res, filename, contentType) {
185
196
  }
186
197
  }
187
198
 
199
+ function serveDistFile(res, pathname) {
200
+ const filePath = join(__dirname, 'dist', pathname === '/' ? 'index.html' : pathname)
201
+ if (!existsSync(filePath)) {
202
+ serveFile(res, 'dist/index.html', 'text/html; charset=utf-8')
203
+ return
204
+ }
205
+ const ext = extname(filePath)
206
+ const ct = MIME_TYPES[ext] || 'application/octet-stream'
207
+ try {
208
+ const content = readFileSync(filePath)
209
+ res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable' })
210
+ res.end(content)
211
+ } catch {
212
+ res.writeHead(404)
213
+ res.end('Not Found')
214
+ }
215
+ }
216
+
188
217
  function handleRequest(req, res) {
189
218
  // CORS for local dev
190
219
  res.setHeader('Access-Control-Allow-Origin', '*')
@@ -211,17 +240,26 @@ function handleRequest(req, res) {
211
240
 
212
241
  switch (url.pathname) {
213
242
  case '/':
214
- serveFile(res, 'index.html', 'text/html; charset=utf-8')
243
+ serveDistFile(res, '/')
215
244
  break
216
245
 
217
246
  case '/styles.css':
218
- serveFile(res, 'styles.css', 'text/css; charset=utf-8')
219
- break
220
-
221
247
  case '/app.js':
222
- serveFile(res, 'app.js', 'application/javascript; charset=utf-8')
248
+ serveDistFile(res, url.pathname)
223
249
  break
224
250
 
251
+ default:
252
+ if (url.pathname.startsWith('/assets/') || url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) {
253
+ serveDistFile(res, url.pathname)
254
+ break
255
+ }
256
+
257
+ if (!url.pathname.startsWith('/api/')) {
258
+ res.writeHead(404)
259
+ res.end('Not Found')
260
+ break
261
+ }
262
+
225
263
  case '/api/models':
226
264
  res.writeHead(200, { 'Content-Type': 'application/json' })
227
265
  res.end(JSON.stringify(getModelsPayload()))
@@ -283,30 +321,57 @@ function handleRequest(req, res) {
283
321
  res.end('Method Not Allowed')
284
322
  }
285
323
  break
286
-
287
- default:
288
- res.writeHead(404)
289
- res.end('Not Found')
290
324
  }
291
325
  }
292
326
 
327
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
328
+
329
+ function checkPortInUse(port) {
330
+ return new Promise((resolve) => {
331
+ const s = createServer()
332
+ s.once('error', (err) => { if (err.code === 'EADDRINUSE') resolve(true); else resolve(false) })
333
+ s.once('listening', () => { s.close(); resolve(false) })
334
+ s.listen(port)
335
+ })
336
+ }
337
+
338
+ function openBrowser(url) {
339
+ const cmd = process.platform === 'darwin' ? 'open'
340
+ : process.platform === 'win32' ? 'start'
341
+ : 'xdg-open'
342
+ exec(`${cmd} "${url}"`, (err) => {
343
+ if (err) console.log(` 💡 Open manually: ${url}`)
344
+ })
345
+ }
346
+
293
347
  // ─── Exports ─────────────────────────────────────────────────────────────────
294
348
 
295
- export async function startWebServer(port = 3333) {
349
+ export async function startWebServer(port = 3333, { open = true } = {}) {
350
+ const alreadyRunning = await checkPortInUse(port)
351
+ const url = `http://localhost:${port}`
352
+
353
+ if (alreadyRunning) {
354
+ console.log()
355
+ console.log(` ⚡ free-coding-models Web Dashboard already running`)
356
+ console.log(` 🌐 ${url}`)
357
+ console.log()
358
+ if (open) openBrowser(url)
359
+ return null
360
+ }
361
+
296
362
  const server = createServer(handleRequest)
297
363
 
298
364
  server.listen(port, () => {
299
365
  console.log()
300
366
  console.log(` ⚡ free-coding-models Web Dashboard`)
301
- console.log(` 🌐 http://localhost:${port}`)
367
+ console.log(` 🌐 ${url}`)
302
368
  console.log(` 📊 Monitoring ${results.filter(r => !r.cliOnly).length} models across ${Object.keys(sources).length} providers`)
303
369
  console.log()
304
370
  console.log(` Press Ctrl+C to stop`)
305
371
  console.log()
372
+ if (open) openBrowser(url)
306
373
  })
307
374
 
308
- // P1 fix: serialize ping rounds — each round starts only after the
309
- // previous one finishes, preventing overlapping concurrent mutations.
310
375
  async function schedulePingLoop() {
311
376
  await pingAllModels()
312
377
  setTimeout(schedulePingLoop, 10_000)
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @file web/src/App.jsx
3
+ * @description Root application component — orchestrates all views, layout, SSE connection, and global state.
4
+ * 📖 Manages current view (dashboard/settings/analytics), theme toggle, search, filters,
5
+ * selected model for detail panel, export modal, and toast notifications.
6
+ * Uses useSSE for live data, useFilter for model filtering/sorting, useTheme for dark/light.
7
+ * @functions App → root component with all state and layout composition
8
+ */
9
+ import { useState, useCallback, useEffect } from 'react'
10
+ import { useSSE } from './hooks/useSSE.js'
11
+ import { useFilter } from './hooks/useFilter.js'
12
+ import { useTheme } from './hooks/useTheme.js'
13
+ import Header from './components/layout/Header.jsx'
14
+ import Sidebar from './components/layout/Sidebar.jsx'
15
+ import Footer from './components/layout/Footer.jsx'
16
+ import StatsBar from './components/dashboard/StatsBar.jsx'
17
+ import FilterBar from './components/dashboard/FilterBar.jsx'
18
+ import ModelTable from './components/dashboard/ModelTable.jsx'
19
+ import DetailPanel from './components/dashboard/DetailPanel.jsx'
20
+ import ExportModal from './components/dashboard/ExportModal.jsx'
21
+ import SettingsView from './components/settings/SettingsView.jsx'
22
+ import AnalyticsView from './components/analytics/AnalyticsView.jsx'
23
+ import ToastContainer from './components/atoms/ToastContainer.jsx'
24
+
25
+ let toastIdCounter = 0
26
+
27
+ export default function App() {
28
+ const { models, connected } = useSSE('/api/events')
29
+ const { theme, toggle: toggleTheme } = useTheme()
30
+ const [currentView, setCurrentView] = useState('dashboard')
31
+ const [selectedModel, setSelectedModel] = useState(null)
32
+ const [exportOpen, setExportOpen] = useState(false)
33
+ const [toasts, setToasts] = useState([])
34
+
35
+ const {
36
+ filtered,
37
+ filterTier, setFilterTier,
38
+ filterStatus, setFilterStatus,
39
+ filterProvider, setFilterProvider,
40
+ searchQuery, setSearchQuery,
41
+ sortColumn, sortDirection, toggleSort,
42
+ } = useFilter(models)
43
+
44
+ const providers = (() => {
45
+ const map = {}
46
+ models.forEach((m) => {
47
+ if (!map[m.providerKey]) map[m.providerKey] = { key: m.providerKey, name: m.origin, count: 0 }
48
+ map[m.providerKey].count++
49
+ })
50
+ return Object.values(map).sort((a, b) => a.name.localeCompare(b.name))
51
+ })()
52
+
53
+ const addToast = useCallback((message, type = 'info') => {
54
+ const id = ++toastIdCounter
55
+ setToasts((prev) => [...prev, { id, message, type }])
56
+ }, [])
57
+
58
+ const dismissToast = useCallback((id) => {
59
+ setToasts((prev) => prev.filter((t) => t.id !== id))
60
+ }, [])
61
+
62
+ const handleSelectModel = useCallback((modelId) => {
63
+ const model = models.find((m) => m.modelId === modelId)
64
+ if (model) setSelectedModel(model)
65
+ }, [models])
66
+
67
+ const handleCloseDetail = useCallback(() => setSelectedModel(null), [])
68
+
69
+ useEffect(() => {
70
+ const handler = (e) => {
71
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
72
+ e.preventDefault()
73
+ if (currentView !== 'dashboard') setCurrentView('dashboard')
74
+ }
75
+ if (e.key === 'Escape') {
76
+ setSelectedModel(null)
77
+ setExportOpen(false)
78
+ }
79
+ }
80
+ window.addEventListener('keydown', handler)
81
+ return () => window.removeEventListener('keydown', handler)
82
+ }, [currentView])
83
+
84
+ return (
85
+ <>
86
+ <Sidebar
87
+ currentView={currentView}
88
+ onNavigate={setCurrentView}
89
+ onToggleTheme={toggleTheme}
90
+ />
91
+
92
+ <div className="app-content">
93
+ {currentView === 'dashboard' && (
94
+ <div className="view">
95
+ <Header
96
+ searchQuery={searchQuery}
97
+ onSearchChange={setSearchQuery}
98
+ onToggleTheme={toggleTheme}
99
+ onOpenSettings={() => setCurrentView('settings')}
100
+ onOpenExport={() => setExportOpen(true)}
101
+ />
102
+ <StatsBar models={models} />
103
+ <FilterBar
104
+ filterTier={filterTier}
105
+ setFilterTier={setFilterTier}
106
+ filterStatus={filterStatus}
107
+ setFilterStatus={setFilterStatus}
108
+ filterProvider={filterProvider}
109
+ setFilterProvider={setFilterProvider}
110
+ providers={providers}
111
+ />
112
+ <ModelTable
113
+ filtered={filtered}
114
+ onSelectModel={handleSelectModel}
115
+ />
116
+ </div>
117
+ )}
118
+
119
+ {currentView === 'settings' && (
120
+ <div className="view">
121
+ <SettingsView onToast={addToast} />
122
+ </div>
123
+ )}
124
+
125
+ {currentView === 'analytics' && (
126
+ <div className="view">
127
+ <AnalyticsView models={models} />
128
+ </div>
129
+ )}
130
+
131
+ <Footer />
132
+ </div>
133
+
134
+ <DetailPanel
135
+ model={selectedModel}
136
+ onClose={handleCloseDetail}
137
+ />
138
+
139
+ {exportOpen && (
140
+ <ExportModal
141
+ models={filtered}
142
+ onClose={() => setExportOpen(false)}
143
+ onToast={addToast}
144
+ />
145
+ )}
146
+
147
+ <ToastContainer toasts={toasts} dismissToast={dismissToast} />
148
+ </>
149
+ )
150
+ }