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.
- package/CHANGELOG.md +10 -1798
- package/README.md +4 -1
- package/bin/free-coding-models.js +1 -1
- package/package.json +11 -2
- package/src/app.js +3 -0
- package/src/cli-help.js +2 -0
- package/src/command-palette.js +3 -0
- package/src/endpoint-installer.js +1 -1
- package/src/tool-bootstrap.js +34 -0
- package/src/tool-launchers.js +137 -1
- package/src/tool-metadata.js +9 -0
- package/src/utils.js +8 -2
- package/web/index.html +2 -300
- package/web/server.js +80 -15
- package/web/src/App.jsx +150 -0
- package/web/src/components/analytics/AnalyticsView.jsx +109 -0
- package/web/src/components/analytics/AnalyticsView.module.css +186 -0
- package/web/src/components/atoms/Sparkline.jsx +44 -0
- package/web/src/components/atoms/StabilityCell.jsx +18 -0
- package/web/src/components/atoms/StabilityCell.module.css +8 -0
- package/web/src/components/atoms/StatusDot.jsx +10 -0
- package/web/src/components/atoms/StatusDot.module.css +17 -0
- package/web/src/components/atoms/TierBadge.jsx +10 -0
- package/web/src/components/atoms/TierBadge.module.css +18 -0
- package/web/src/components/atoms/Toast.jsx +25 -0
- package/web/src/components/atoms/Toast.module.css +35 -0
- package/web/src/components/atoms/ToastContainer.jsx +16 -0
- package/web/src/components/atoms/ToastContainer.module.css +10 -0
- package/web/src/components/atoms/VerdictBadge.jsx +13 -0
- package/web/src/components/atoms/VerdictBadge.module.css +19 -0
- package/web/src/components/dashboard/DetailPanel.jsx +131 -0
- package/web/src/components/dashboard/DetailPanel.module.css +99 -0
- package/web/src/components/dashboard/ExportModal.jsx +79 -0
- package/web/src/components/dashboard/ExportModal.module.css +99 -0
- package/web/src/components/dashboard/FilterBar.jsx +73 -0
- package/web/src/components/dashboard/FilterBar.module.css +43 -0
- package/web/src/components/dashboard/ModelTable.jsx +86 -0
- package/web/src/components/dashboard/ModelTable.module.css +46 -0
- package/web/src/components/dashboard/StatsBar.jsx +40 -0
- package/web/src/components/dashboard/StatsBar.module.css +28 -0
- package/web/src/components/layout/Footer.jsx +19 -0
- package/web/src/components/layout/Footer.module.css +10 -0
- package/web/src/components/layout/Header.jsx +38 -0
- package/web/src/components/layout/Header.module.css +73 -0
- package/web/src/components/layout/Sidebar.jsx +41 -0
- package/web/src/components/layout/Sidebar.module.css +76 -0
- package/web/src/components/settings/SettingsView.jsx +264 -0
- package/web/src/components/settings/SettingsView.module.css +377 -0
- package/web/src/global.css +199 -0
- package/web/src/hooks/useFilter.js +83 -0
- package/web/src/hooks/useSSE.js +49 -0
- package/web/src/hooks/useTheme.js +27 -0
- package/web/src/main.jsx +15 -0
- package/web/src/utils/download.js +15 -0
- package/web/src/utils/format.js +42 -0
- package/web/src/utils/ranks.js +37 -0
- /package/web/{app.js → app.legacy.js} +0 -0
- /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
|
-
|
|
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">×</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">×</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
|
-
|
|
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
|
-
|
|
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(` 🌐
|
|
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)
|
package/web/src/App.jsx
ADDED
|
@@ -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
|
+
}
|