free-coding-models 0.3.37 → 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 -1794
  2. package/README.md +4 -1
  3. package/bin/free-coding-models.js +8 -0
  4. package/package.json +13 -3
  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 +10 -0
  13. package/web/app.legacy.js +900 -0
  14. package/web/index.html +20 -0
  15. package/web/server.js +382 -0
  16. package/web/src/App.jsx +150 -0
  17. package/web/src/components/analytics/AnalyticsView.jsx +109 -0
  18. package/web/src/components/analytics/AnalyticsView.module.css +186 -0
  19. package/web/src/components/atoms/Sparkline.jsx +44 -0
  20. package/web/src/components/atoms/StabilityCell.jsx +18 -0
  21. package/web/src/components/atoms/StabilityCell.module.css +8 -0
  22. package/web/src/components/atoms/StatusDot.jsx +10 -0
  23. package/web/src/components/atoms/StatusDot.module.css +17 -0
  24. package/web/src/components/atoms/TierBadge.jsx +10 -0
  25. package/web/src/components/atoms/TierBadge.module.css +18 -0
  26. package/web/src/components/atoms/Toast.jsx +25 -0
  27. package/web/src/components/atoms/Toast.module.css +35 -0
  28. package/web/src/components/atoms/ToastContainer.jsx +16 -0
  29. package/web/src/components/atoms/ToastContainer.module.css +10 -0
  30. package/web/src/components/atoms/VerdictBadge.jsx +13 -0
  31. package/web/src/components/atoms/VerdictBadge.module.css +19 -0
  32. package/web/src/components/dashboard/DetailPanel.jsx +131 -0
  33. package/web/src/components/dashboard/DetailPanel.module.css +99 -0
  34. package/web/src/components/dashboard/ExportModal.jsx +79 -0
  35. package/web/src/components/dashboard/ExportModal.module.css +99 -0
  36. package/web/src/components/dashboard/FilterBar.jsx +73 -0
  37. package/web/src/components/dashboard/FilterBar.module.css +43 -0
  38. package/web/src/components/dashboard/ModelTable.jsx +86 -0
  39. package/web/src/components/dashboard/ModelTable.module.css +46 -0
  40. package/web/src/components/dashboard/StatsBar.jsx +40 -0
  41. package/web/src/components/dashboard/StatsBar.module.css +28 -0
  42. package/web/src/components/layout/Footer.jsx +19 -0
  43. package/web/src/components/layout/Footer.module.css +10 -0
  44. package/web/src/components/layout/Header.jsx +38 -0
  45. package/web/src/components/layout/Header.module.css +73 -0
  46. package/web/src/components/layout/Sidebar.jsx +41 -0
  47. package/web/src/components/layout/Sidebar.module.css +76 -0
  48. package/web/src/components/settings/SettingsView.jsx +264 -0
  49. package/web/src/components/settings/SettingsView.module.css +377 -0
  50. package/web/src/global.css +199 -0
  51. package/web/src/hooks/useFilter.js +83 -0
  52. package/web/src/hooks/useSSE.js +49 -0
  53. package/web/src/hooks/useTheme.js +27 -0
  54. package/web/src/main.jsx +15 -0
  55. package/web/src/utils/download.js +15 -0
  56. package/web/src/utils/format.js +42 -0
  57. package/web/src/utils/ranks.js +37 -0
  58. package/web/styles.legacy.css +963 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @file web/src/components/layout/Sidebar.jsx
3
+ * @description Collapsible sidebar navigation with Dashboard / Settings / Analytics links + theme toggle.
4
+ */
5
+ import styles from './Sidebar.module.css'
6
+
7
+ const NAV_ITEMS = [
8
+ { id: 'dashboard', icon: '▤', label: 'Dashboard' },
9
+ { id: 'settings', icon: '⚙', label: 'Settings' },
10
+ { id: 'analytics', icon: '▌▌', label: 'Analytics' },
11
+ ]
12
+
13
+ export default function Sidebar({ currentView, onNavigate, onToggleTheme }) {
14
+ return (
15
+ <aside className={styles.sidebar}>
16
+ <div className={styles.logo}>
17
+ <span className={styles.logoIcon}>⚡</span>
18
+ <span className={styles.logoText}>FCM</span>
19
+ </div>
20
+ <nav className={styles.nav}>
21
+ {NAV_ITEMS.map(({ id, icon, label }) => (
22
+ <button
23
+ key={id}
24
+ className={`${styles.navItem} ${currentView === id ? styles.active : ''}`}
25
+ onClick={() => onNavigate(id)}
26
+ title={label}
27
+ >
28
+ <span className={styles.navIcon}>{icon}</span>
29
+ <span className={styles.navLabel}>{label}</span>
30
+ </button>
31
+ ))}
32
+ </nav>
33
+ <div className={styles.bottom}>
34
+ <button className={styles.navItem} onClick={onToggleTheme} title="Toggle Theme">
35
+ <span className={styles.navIcon}>☽</span>
36
+ <span className={styles.navLabel}>Theme</span>
37
+ </button>
38
+ </div>
39
+ </aside>
40
+ )
41
+ }
@@ -0,0 +1,76 @@
1
+ .sidebar {
2
+ position: fixed;
3
+ left: 0; top: 0; bottom: 0;
4
+ width: 64px;
5
+ z-index: 200;
6
+ background: var(--color-bg-elevated);
7
+ border-right: 1px solid var(--color-border);
8
+ display: flex;
9
+ flex-direction: column;
10
+ padding: 12px 0;
11
+ transition: width 250ms cubic-bezier(0.16, 1, 0.3, 1);
12
+ overflow: hidden;
13
+ }
14
+ .sidebar:hover { width: 200px; }
15
+
16
+ .logo {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 10px;
20
+ padding: 8px 18px;
21
+ margin-bottom: 16px;
22
+ white-space: nowrap;
23
+ overflow: hidden;
24
+ }
25
+ .logoIcon { font-size: 24px; flex-shrink: 0; }
26
+ .logoText {
27
+ font-size: 16px;
28
+ font-weight: 800;
29
+ background: linear-gradient(135deg, var(--color-accent), #06b6d4);
30
+ -webkit-background-clip: text;
31
+ -webkit-text-fill-color: transparent;
32
+ background-clip: text;
33
+ opacity: 0;
34
+ transition: opacity 250ms cubic-bezier(0.16, 1, 0.3, 1);
35
+ }
36
+ .sidebar:hover .logoText { opacity: 1; }
37
+
38
+ .nav { flex: 1; display: flex; flex-direction: column; gap: 4px; }
39
+ .bottom { display: flex; flex-direction: column; gap: 4px; }
40
+
41
+ .navItem {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 12px;
45
+ padding: 10px 20px;
46
+ border: none;
47
+ background: none;
48
+ color: var(--color-text-muted);
49
+ cursor: pointer;
50
+ white-space: nowrap;
51
+ overflow: hidden;
52
+ font-family: var(--font-sans);
53
+ font-size: 13px;
54
+ font-weight: 500;
55
+ transition: all 150ms cubic-bezier(0.16, 1, 0.3, 1);
56
+ }
57
+ .navItem:hover {
58
+ color: var(--color-text);
59
+ background: var(--color-bg-hover);
60
+ }
61
+ .navIcon {
62
+ width: 20px;
63
+ text-align: center;
64
+ flex-shrink: 0;
65
+ font-size: 16px;
66
+ }
67
+ .navLabel {
68
+ opacity: 0;
69
+ transition: opacity 250ms cubic-bezier(0.16, 1, 0.3, 1);
70
+ }
71
+ .sidebar:hover .navLabel { opacity: 1; }
72
+ .active {
73
+ color: var(--color-accent) !important;
74
+ background: var(--color-accent-dim) !important;
75
+ border-right: 3px solid var(--color-accent);
76
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * @file web/src/components/settings/SettingsView.jsx
3
+ * @description Full settings page for managing API keys and provider configurations.
4
+ * 📖 Fetches config from /api/config, renders expandable provider cards with
5
+ * key display (masked/revealed), save/delete/toggle actions, and search filter.
6
+ * @functions SettingsView → main settings page component
7
+ */
8
+ import { useState, useEffect, useCallback } from 'react'
9
+ import styles from './SettingsView.module.css'
10
+ import { maskKey } from '../../utils/format.js'
11
+
12
+ export default function SettingsView({ onToast }) {
13
+ const [config, setConfig] = useState(null)
14
+ const [searchQuery, setSearchQuery] = useState('')
15
+ const [expandedCards, setExpandedCards] = useState(new Set())
16
+ const [revealedKeys, setRevealedKeys] = useState(new Set())
17
+ const [keyInputs, setKeyInputs] = useState({})
18
+
19
+ const loadConfig = useCallback(async () => {
20
+ try {
21
+ const resp = await fetch('/api/config')
22
+ const data = await resp.json()
23
+ setConfig(data)
24
+ } catch {
25
+ onToast?.('Failed to load settings', 'error')
26
+ }
27
+ }, [onToast])
28
+
29
+ useEffect(() => { loadConfig() }, [loadConfig])
30
+
31
+ const toggleCard = (key) => {
32
+ setExpandedCards((prev) => {
33
+ const next = new Set(prev)
34
+ if (next.has(key)) next.delete(key)
35
+ else next.add(key)
36
+ return next
37
+ })
38
+ }
39
+
40
+ const expandAll = () => {
41
+ if (!config) return
42
+ setExpandedCards(new Set(Object.keys(config.providers)))
43
+ }
44
+
45
+ const collapseAll = () => setExpandedCards(new Set())
46
+
47
+ const toggleRevealKey = async (key) => {
48
+ if (revealedKeys.has(key)) {
49
+ setRevealedKeys((prev) => { const n = new Set(prev); n.delete(key); return n })
50
+ return
51
+ }
52
+ try {
53
+ const resp = await fetch(`/api/key/${key}`)
54
+ const data = await resp.json()
55
+ if (data.key) {
56
+ setRevealedKeys((prev) => new Set(prev).add(key))
57
+ }
58
+ } catch {
59
+ onToast?.('Failed to reveal key', 'error')
60
+ }
61
+ }
62
+
63
+ const copyKey = async (key) => {
64
+ try {
65
+ const resp = await fetch(`/api/key/${key}`)
66
+ const data = await resp.json()
67
+ if (data.key) {
68
+ await navigator.clipboard.writeText(data.key)
69
+ onToast?.('API key copied to clipboard', 'success')
70
+ } else {
71
+ onToast?.('No key to copy', 'warning')
72
+ }
73
+ } catch {
74
+ onToast?.('Failed to copy key', 'error')
75
+ }
76
+ }
77
+
78
+ const saveKey = async (key) => {
79
+ const value = keyInputs[key]?.trim()
80
+ if (!value) {
81
+ onToast?.('Please enter an API key', 'warning')
82
+ return
83
+ }
84
+ try {
85
+ const resp = await fetch('/api/settings', {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ apiKeys: { [key]: value } }),
89
+ })
90
+ const result = await resp.json()
91
+ if (result.success) {
92
+ onToast?.(`API key for ${key} saved successfully!`, 'success')
93
+ setKeyInputs((prev) => ({ ...prev, [key]: '' }))
94
+ setRevealedKeys((prev) => { const n = new Set(prev); n.delete(key); return n })
95
+ await loadConfig()
96
+ setExpandedCards((prev) => new Set(prev).add(key))
97
+ } else {
98
+ onToast?.(result.error || 'Failed to save', 'error')
99
+ }
100
+ } catch {
101
+ onToast?.('Network error while saving', 'error')
102
+ }
103
+ }
104
+
105
+ const deleteKey = async (key) => {
106
+ if (!confirm(`Are you sure you want to delete the API key for "${key}"?`)) return
107
+ try {
108
+ const resp = await fetch('/api/settings', {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ apiKeys: { [key]: '' } }),
112
+ })
113
+ const result = await resp.json()
114
+ if (result.success) {
115
+ onToast?.(`API key for ${key} deleted`, 'info')
116
+ setRevealedKeys((prev) => { const n = new Set(prev); n.delete(key); return n })
117
+ await loadConfig()
118
+ } else {
119
+ onToast?.(result.error || 'Failed to delete', 'error')
120
+ }
121
+ } catch {
122
+ onToast?.('Network error while deleting', 'error')
123
+ }
124
+ }
125
+
126
+ const toggleProvider = async (key, enabled) => {
127
+ try {
128
+ const resp = await fetch('/api/settings', {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({ providers: { [key]: { enabled } } }),
132
+ })
133
+ const result = await resp.json()
134
+ if (result.success) {
135
+ onToast?.(`${key} ${enabled ? 'enabled' : 'disabled'}`, 'success')
136
+ } else {
137
+ onToast?.(result.error || 'Failed to toggle', 'error')
138
+ }
139
+ } catch {
140
+ onToast?.('Network error', 'error')
141
+ }
142
+ }
143
+
144
+ if (!config) {
145
+ return (
146
+ <div className={styles.page}>
147
+ <div className={styles.loading}>Loading settings...</div>
148
+ </div>
149
+ )
150
+ }
151
+
152
+ const entries = Object.entries(config.providers)
153
+ .filter(([, p]) => {
154
+ if (!searchQuery) return true
155
+ const q = searchQuery.toLowerCase()
156
+ return p.name.toLowerCase().includes(q)
157
+ })
158
+ .sort((a, b) => a[1].name.localeCompare(b[1].name))
159
+
160
+ return (
161
+ <div className={styles.page}>
162
+ <div className={styles.pageHeader}>
163
+ <h1 className={styles.pageTitle}>⚙️ Provider Settings</h1>
164
+ <p className={styles.pageSubtitle}>
165
+ Manage your API keys and provider configurations. Keys are stored locally in{' '}
166
+ <code>~/.free-coding-models.json</code>
167
+ </p>
168
+ </div>
169
+
170
+ <div className={styles.toolbar}>
171
+ <div className={styles.toolbarSearch}>
172
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
173
+ <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
174
+ </svg>
175
+ <input
176
+ type="text"
177
+ placeholder="Search providers..."
178
+ value={searchQuery}
179
+ onChange={(e) => setSearchQuery(e.target.value)}
180
+ autoComplete="off"
181
+ />
182
+ </div>
183
+ <div className={styles.toolbarActions}>
184
+ <button className={styles.toolbarBtn} onClick={expandAll}>Expand All</button>
185
+ <button className={styles.toolbarBtn} onClick={collapseAll}>Collapse All</button>
186
+ </div>
187
+ </div>
188
+
189
+ <div className={styles.providers}>
190
+ {entries.map(([key, p]) => {
191
+ const isExpanded = expandedCards.has(key)
192
+ const isRevealed = revealedKeys.has(key)
193
+
194
+ return (
195
+ <div key={key} className={`${styles.card} ${isExpanded ? styles.cardExpanded : ''}`}>
196
+ <div className={styles.cardHeader} onClick={() => toggleCard(key)}>
197
+ <div className={styles.cardIcon}>🔌</div>
198
+ <div className={styles.cardInfo}>
199
+ <div className={styles.cardName}>{p.name}</div>
200
+ <div className={styles.cardMeta}>{p.modelCount} models · {key}</div>
201
+ </div>
202
+ <span className={`${styles.cardStatus} ${p.hasKey ? styles.statusConfigured : styles.statusMissing}`}>
203
+ {p.hasKey ? '✅ Active' : '🔑 No Key'}
204
+ </span>
205
+ <span className={`${styles.toggleIcon} ${isExpanded ? styles.toggleIconExpanded : ''}`}>▼</span>
206
+ </div>
207
+
208
+ <div className={styles.cardBody}>
209
+ <div className={styles.cardContent}>
210
+ {p.hasKey && (
211
+ <div className={styles.keyGroup}>
212
+ <label className={styles.keyLabel}>Current API Key</label>
213
+ <div className={styles.keyDisplay}>
214
+ <span className={styles.keyDisplayValue}>
215
+ {isRevealed ? (p.maskedKey || '••••••••') : maskKey(p.maskedKey || '')}
216
+ </span>
217
+ <div className={styles.keyDisplayActions}>
218
+ <button className={styles.actionBtn} onClick={() => toggleRevealKey(key)} title={isRevealed ? 'Hide' : 'Reveal'}>
219
+ {isRevealed ? '🙈' : '👁️'}
220
+ </button>
221
+ <button className={styles.actionBtn} onClick={() => copyKey(key)} title="Copy">📋</button>
222
+ <button className={`${styles.actionBtn} ${styles.actionBtnDanger}`} onClick={() => deleteKey(key)} title="Delete Key">🗑️</button>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ )}
227
+
228
+ <div className={styles.keyGroup}>
229
+ <label className={styles.keyLabel}>{p.hasKey ? 'Update API Key' : 'Add API Key'}</label>
230
+ <div className={styles.keyInputRow}>
231
+ <input
232
+ type="password"
233
+ className={styles.keyInput}
234
+ placeholder="Enter your API key..."
235
+ value={keyInputs[key] || ''}
236
+ onChange={(e) => setKeyInputs((prev) => ({ ...prev, [key]: e.target.value }))}
237
+ autoComplete="off"
238
+ />
239
+ <button className={styles.saveBtn} onClick={() => saveKey(key)}>
240
+ {p.hasKey ? 'Update' : 'Save'}
241
+ </button>
242
+ </div>
243
+ </div>
244
+
245
+ <div className={styles.enabledRow}>
246
+ <span className={styles.enabledLabel}>Provider Enabled</span>
247
+ <label className={styles.toggleSwitch}>
248
+ <input
249
+ type="checkbox"
250
+ defaultChecked={p.enabled !== false}
251
+ onChange={(e) => toggleProvider(key, e.target.checked)}
252
+ />
253
+ <span className={styles.toggleSlider} />
254
+ </label>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ )
260
+ })}
261
+ </div>
262
+ </div>
263
+ )
264
+ }