free-coding-models 0.5.85 → 0.5.87

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 (164) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.86.md +10 -0
  4. package/changelog/v0.5.87.md +48 -0
  5. package/package.json +8 -5
  6. package/sources.js +4 -4
  7. package/src/core/cache.js +3 -1
  8. package/src/core/config.js +43 -5
  9. package/src/core/endpoint-installer.js +23 -12
  10. package/src/core/installed-models-manager.js +43 -20
  11. package/src/core/kilo-config.js +51 -5
  12. package/src/core/kilo.js +5 -3
  13. package/src/core/legacy-proxy-cleanup.js +22 -5
  14. package/src/core/model-merger.js +4 -0
  15. package/src/core/models-drift.js +9 -0
  16. package/src/core/opencode-config.js +57 -5
  17. package/src/core/opencode.js +67 -41
  18. package/src/core/playground.js +7 -0
  19. package/src/core/probe-cache.js +35 -4
  20. package/src/core/provider-key-tester.js +10 -2
  21. package/src/core/provider-metadata.js +10 -0
  22. package/src/core/provider-quota-fetchers.js +14 -7
  23. package/src/core/router-daemon.js +208 -26
  24. package/src/core/runtime-telemetry.js +27 -1
  25. package/src/core/shared-helpers.js +12 -1
  26. package/src/core/shell-env.js +3 -1
  27. package/src/core/telemetry.js +18 -5
  28. package/src/core/tool-launchers.js +9 -6
  29. package/src/core/updater.js +28 -2
  30. package/src/core/utils.js +76 -71
  31. package/src/tui/app.js +47 -16
  32. package/src/tui/command-palette.js +22 -2
  33. package/src/tui/key-handler.js +123 -179
  34. package/src/tui/overlays.js +35 -75
  35. package/src/tui/render-helpers.js +62 -4
  36. package/src/tui/render-table.js +46 -5
  37. package/src/tui/theme.js +36 -5
  38. package/src/tui/tui-filters.js +28 -8
  39. package/src/tui/tui-state.js +22 -6
  40. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/index.html +5 -9
  43. package/web/index.html +3 -7
  44. package/web/server.js +97 -23
  45. package/web/vite.config.js +8 -0
  46. package/web/app.legacy.js +0 -900
  47. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  48. package/web/dist/assets/index-DWUely84.js +0 -40
  49. package/web/public/favicon.ico +0 -0
  50. package/web/public/favicons/apple-touch-icon.png +0 -0
  51. package/web/public/favicons/browserconfig.xml +0 -12
  52. package/web/public/favicons/favicon-16x16.png +0 -0
  53. package/web/public/favicons/favicon-192x192.png +0 -0
  54. package/web/public/favicons/favicon-32x32.png +0 -0
  55. package/web/public/favicons/favicon-48x48.png +0 -0
  56. package/web/public/favicons/favicon-512x512.png +0 -0
  57. package/web/public/favicons/favicon-96x96.png +0 -0
  58. package/web/public/favicons/favicon.ico +0 -0
  59. package/web/public/favicons/mstile-150x150.png +0 -0
  60. package/web/public/favicons/mstile-310x150.png +0 -0
  61. package/web/public/favicons/mstile-310x310.png +0 -0
  62. package/web/public/favicons/mstile-512x512.png +0 -0
  63. package/web/public/favicons/mstile-70x70.png +0 -0
  64. package/web/public/favicons/site.webmanifest +0 -25
  65. package/web/src/App.jsx +0 -521
  66. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  67. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  68. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  69. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  70. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  71. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  72. package/web/src/components/atoms/HealthCell.jsx +0 -143
  73. package/web/src/components/atoms/HealthCell.module.css +0 -60
  74. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  75. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  76. package/web/src/components/atoms/MoodCell.jsx +0 -25
  77. package/web/src/components/atoms/MoodCell.module.css +0 -6
  78. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  79. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  80. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  81. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  82. package/web/src/components/atoms/RankCell.jsx +0 -9
  83. package/web/src/components/atoms/RankCell.module.css +0 -9
  84. package/web/src/components/atoms/Sparkline.jsx +0 -63
  85. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  86. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  87. package/web/src/components/atoms/StatusDot.jsx +0 -13
  88. package/web/src/components/atoms/StatusDot.module.css +0 -29
  89. package/web/src/components/atoms/TPSCell.jsx +0 -36
  90. package/web/src/components/atoms/TPSCell.module.css +0 -44
  91. package/web/src/components/atoms/TierBadge.jsx +0 -10
  92. package/web/src/components/atoms/TierBadge.module.css +0 -29
  93. package/web/src/components/atoms/Toast.jsx +0 -25
  94. package/web/src/components/atoms/Toast.module.css +0 -41
  95. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  96. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  97. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  98. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  99. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  100. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  101. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  102. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  103. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  104. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  105. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  106. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  107. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  108. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  109. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  110. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  111. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  112. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  113. package/web/src/components/help/HelpView.jsx +0 -201
  114. package/web/src/components/help/HelpView.module.css +0 -156
  115. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  116. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  117. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  118. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  119. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  120. package/web/src/components/launch/LaunchButton.jsx +0 -30
  121. package/web/src/components/launch/LaunchButton.module.css +0 -35
  122. package/web/src/components/launch/LaunchModal.module.css +0 -125
  123. package/web/src/components/layout/Footer.jsx +0 -19
  124. package/web/src/components/layout/Footer.module.css +0 -10
  125. package/web/src/components/layout/Header.jsx +0 -265
  126. package/web/src/components/layout/Header.module.css +0 -357
  127. package/web/src/components/palette/CommandPalette.jsx +0 -301
  128. package/web/src/components/palette/CommandPalette.module.css +0 -121
  129. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  130. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  131. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  132. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  133. package/web/src/components/recommend/RecommendView.jsx +0 -121
  134. package/web/src/components/recommend/RecommendView.module.css +0 -57
  135. package/web/src/components/router/RouterView.jsx +0 -1212
  136. package/web/src/components/router/RouterView.module.css +0 -1289
  137. package/web/src/components/settings/SettingsView.jsx +0 -555
  138. package/web/src/components/settings/SettingsView.module.css +0 -550
  139. package/web/src/components/tools/ToolPicker.jsx +0 -86
  140. package/web/src/components/tools/ToolPicker.module.css +0 -97
  141. package/web/src/components/update/UpdateChip.jsx +0 -104
  142. package/web/src/components/update/UpdateChip.module.css +0 -146
  143. package/web/src/global.css +0 -298
  144. package/web/src/hooks/urlState.constants.js +0 -28
  145. package/web/src/hooks/useChangelog.js +0 -51
  146. package/web/src/hooks/useColumnSizing.js +0 -211
  147. package/web/src/hooks/useFavorites.js +0 -150
  148. package/web/src/hooks/useFilter.js +0 -310
  149. package/web/src/hooks/useInstalledModels.js +0 -42
  150. package/web/src/hooks/useRecommend.js +0 -67
  151. package/web/src/hooks/useRouterDashboard.js +0 -133
  152. package/web/src/hooks/useSSE.js +0 -73
  153. package/web/src/hooks/useSocket.js +0 -203
  154. package/web/src/hooks/useTheme.js +0 -83
  155. package/web/src/hooks/useTokenUsage.js +0 -103
  156. package/web/src/hooks/useToolMode.js +0 -77
  157. package/web/src/hooks/useUpdateChecker.js +0 -91
  158. package/web/src/hooks/useUrlState.js +0 -170
  159. package/web/src/main.jsx +0 -15
  160. package/web/src/utils/download.js +0 -15
  161. package/web/src/utils/format.js +0 -42
  162. package/web/src/utils/m3.js +0 -57
  163. package/web/src/utils/ranks.js +0 -37
  164. package/web/styles.legacy.css +0 -963
@@ -1,371 +0,0 @@
1
- /**
2
- * @file web/src/components/playground/PlaygroundView.jsx
3
- * @description Playground chat modal — multi-turn chat through the FCM router
4
- * OR directly to a specific model. This is the HOST (chrome) layer; the
5
- * actual chat (transcript, input, SSE streaming, metadata) is delegated to
6
- * the shared <PlaygroundChat> core so the full playground, the RouterView
7
- * "Test Router", and the ExpandedDetailRow "Mini Playground" all look and
8
- * behave identically.
9
- *
10
- * 📖 What this host still owns:
11
- * 📖 - Header (title, message/token counter, clear + close)
12
- * 📖 - Model bar (selector, streaming toggle, pre-prompt toggle, router status)
13
- * 📖 - Pre-prompt hint (fetched from /api/router/preprompt)
14
- * 📖 - Router daemon lifecycle (auto-check, 5s auto-start, start button)
15
- * 📖 - Smart model selection on mount (best "up" model with a key)
16
- *
17
- * 📖 What it delegates to <PlaygroundChat>:
18
- * 📖 - The transcript + input + send/stop
19
- * 📖 - SSE streaming through /api/playground/chat
20
- * 📖 - The harmonized metadata row under every reply
21
- * 📖 (provider/model · ms · tok · t/s)
22
- *
23
- * @functions PlaygroundView — full-screen chat modal host wrapping PlaygroundChat
24
- */
25
- import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
26
- import {
27
- IconMessageChatbot,
28
- IconX,
29
- IconTrash,
30
- IconBolt,
31
- IconAlertTriangle,
32
- IconPlayerPlay,
33
- IconLoader,
34
- } from '@tabler/icons-react'
35
- import PlaygroundChat from './PlaygroundChat.jsx'
36
- import styles from './PlaygroundView.module.css'
37
-
38
- const SUGGESTIONS = [
39
- 'Write a Python fizzbuzz with type hints',
40
- 'Explain Big O notation with three examples',
41
- 'Refactor a deeply nested for-loop into map/filter',
42
- 'Write a tiny Express endpoint that returns JSON',
43
- ]
44
-
45
- /**
46
- * 📖 Router status pill shown in the model bar. `daemonRunning` is a tri-state:
47
- * 📖 null = checking, true = online, false = offline. Kept here because it is
48
- * 📖 pure chrome (not part of the chat core).
49
- */
50
- function StatusPill({ routerStatus, daemonRunning }) {
51
- if (daemonRunning === true) {
52
- return (
53
- <span className={styles.metaChip}>
54
- <IconBolt size={11} />
55
- Router online
56
- </span>
57
- )
58
- }
59
- if (daemonRunning === false) {
60
- return (
61
- <span className={`${styles.metaChip} ${styles.error}`}>
62
- <IconAlertTriangle size={11} />
63
- Router offline
64
- </span>
65
- )
66
- }
67
- return (
68
- <span className={styles.metaChip}>
69
- <IconLoader size={11} className={styles.spin} />
70
- Checking router…
71
- </span>
72
- )
73
- }
74
-
75
- export default function PlaygroundView({ onClose, onToast, models, routerStatus }) {
76
- // ── Chrome state (host-owned) ──────────────────────────────────────────
77
- const [model, setModel] = useState(null) // null = computing best model
78
- const [streamOn, setStreamOn] = useState(true)
79
- const [prePromptEnabled, setPrePromptEnabled] = useState(true)
80
- const [prePromptText, setPrePromptText] = useState('')
81
-
82
- // 📖 Router daemon lifecycle (null = checking, true = running, false = down)
83
- const [daemonRunning, setDaemonRunning] = useState(
84
- routerStatus?.running === true ? true : null
85
- )
86
- const [daemonStarting, setDaemonStarting] = useState(false)
87
- const [autoStartSec, setAutoStartSec] = useState(5)
88
- const cooldownRef = useRef(null)
89
- const daemonCheckRef = useRef(null)
90
- const autoStartTriggered = useRef(false)
91
-
92
- // 📖 Header counter + empty-state gate. Updated by PlaygroundChat via the
93
- // 📖 onTurnComplete callback so the host chrome stays in sync with the
94
- // 📖 chat core it no longer owns directly.
95
- const [chatStats, setChatStats] = useState({ count: 0, totalTokens: 0 })
96
- const chatRef = useRef(null)
97
-
98
- // 📖 Fetch the pre-prompt once on mount so the toggle shows the real value
99
- // 📖 and the hint matches what the router injects server-side.
100
- useEffect(() => {
101
- void fetch('/api/router/preprompt')
102
- .then((r) => r.json())
103
- .then((data) => {
104
- if (data && typeof data === 'object') {
105
- setPrePromptEnabled(data.enabled === true)
106
- if (typeof data.text === 'string') setPrePromptText(data.text)
107
- }
108
- })
109
- .catch(() => {})
110
- }, [])
111
-
112
- // 📖 Smart model selection on mount: pick the best "up" model with an API
113
- // 📖 key, preferring lower latency and higher tier. Falls back to "fcm" so
114
- // 📖 the playground opens immediately even without a working model.
115
- useEffect(() => {
116
- if (model !== null) return
117
- const upModels = (Array.isArray(models) ? models : [])
118
- .filter((m) => m.status === 'up' && m.hasApiKey && !m.isPinging)
119
-
120
- if (upModels.length > 0) {
121
- const tierOrder = { 'S+': 0, 'S': 1, 'A+': 2, 'A': 3, 'A-': 4, 'B+': 5, 'B': 6, 'C': 7 }
122
- upModels.sort((a, b) => {
123
- const ta = tierOrder[a.tier] ?? 99
124
- const tb = tierOrder[b.tier] ?? 99
125
- if (ta !== tb) return ta - tb
126
- const avgA = typeof a.avg === 'number' ? a.avg : 99999
127
- const avgB = typeof b.avg === 'number' ? b.avg : 99999
128
- return avgA - avgB
129
- })
130
- setModel(`${upModels[0].providerKey}/${upModels[0].modelId}`)
131
- return
132
- }
133
- setModel('fcm')
134
- }, [models, model])
135
-
136
- // 📖 Check daemon status on mount.
137
- useEffect(() => {
138
- let mounted = true
139
- async function checkDaemon() {
140
- try {
141
- const resp = await fetch('/api/router/status')
142
- const data = await resp.json()
143
- if (!mounted) return
144
- setDaemonRunning(data?.running === true)
145
- } catch {
146
- if (mounted) setDaemonRunning(false)
147
- }
148
- }
149
- void checkDaemon()
150
- return () => {
151
- mounted = false
152
- if (cooldownRef.current) clearInterval(cooldownRef.current)
153
- if (daemonCheckRef.current) clearInterval(daemonCheckRef.current)
154
- }
155
- }, [])
156
-
157
- // 📖 Start the router daemon, then poll until it is ready.
158
- const startDaemon = useCallback(async () => {
159
- if (daemonStarting) return
160
- if (cooldownRef.current) { clearInterval(cooldownRef.current); cooldownRef.current = null }
161
- autoStartTriggered.current = true
162
- setDaemonStarting(true)
163
- try {
164
- await fetch('/api/router/start', { method: 'POST' })
165
- let attempts = 0
166
- await new Promise((resolve) => {
167
- daemonCheckRef.current = setInterval(async () => {
168
- attempts += 1
169
- try {
170
- const resp = await fetch('/api/router/status')
171
- const data = await resp.json()
172
- if (data?.running) {
173
- clearInterval(daemonCheckRef.current)
174
- setDaemonRunning(true)
175
- setDaemonStarting(false)
176
- resolve()
177
- }
178
- } catch {}
179
- if (attempts >= 30) {
180
- clearInterval(daemonCheckRef.current)
181
- setDaemonStarting(false)
182
- resolve()
183
- }
184
- }, 1000)
185
- })
186
- } catch {
187
- setDaemonStarting(false)
188
- }
189
- }, [daemonStarting])
190
-
191
- // 📖 Auto-start the daemon after 5 seconds if it is down, the model is the
192
- // 📖 "fcm" auto-router, and auto-start hasn't fired yet. The button is always
193
- // 📖 clickable so the user can trigger it manually at any time.
194
- useEffect(() => {
195
- if (daemonRunning !== false || model !== 'fcm' || autoStartTriggered.current) return
196
- let remaining = 5
197
- setAutoStartSec(5)
198
- cooldownRef.current = setInterval(() => {
199
- remaining -= 1
200
- setAutoStartSec(remaining)
201
- if (remaining <= 0) {
202
- clearInterval(cooldownRef.current)
203
- autoStartTriggered.current = true
204
- void startDaemon()
205
- }
206
- }, 1000)
207
- return () => { if (cooldownRef.current) clearInterval(cooldownRef.current) }
208
- }, [daemonRunning, model, startDaemon])
209
-
210
- // 📖 Stable list of model identifiers for the dropdown — defaults to `fcm`
211
- // 📖 (the auto-router) and lets the user pin a specific catalog entry.
212
- const modelOptions = useMemo(() => {
213
- const opts = [{ value: 'fcm', label: 'fcm — auto router (recommended)' }]
214
- if (Array.isArray(models)) {
215
- for (const m of models.slice(0, 200)) {
216
- const id = m.modelId || m.id
217
- if (!id) continue
218
- const label = m.label || id
219
- opts.push({ value: `${m.providerKey}/${id}`, label: `${m.providerKey}/${id} (${label})` })
220
- }
221
- }
222
- return opts
223
- }, [models])
224
-
225
- // 📖 PlaygroundChat turn callback → keep the header counter in sync and
226
- // 📖 drive the daemon-panel gate (only show it when there are no messages).
227
- const handleTurnComplete = useCallback((summary) => {
228
- setChatStats({ count: summary.count, totalTokens: summary.totalTokens })
229
- }, [])
230
-
231
- const handleClear = useCallback(() => {
232
- chatRef.current?.clear()
233
- setChatStats({ count: 0, totalTokens: 0 })
234
- }, [])
235
-
236
- // 📖 Can the user send right now? fcm needs the daemon; a pinned model
237
- // 📖 routes directly to its provider and works without the daemon.
238
- const canSend = !(model === 'fcm' && daemonRunning !== true)
239
-
240
- // 📖 Daemon-panel gates. The panels replace the chat area entirely when the
241
- // 📖 user is on the auto-router, the daemon is down/starting, and there is
242
- // 📖 no conversation yet to keep on screen.
243
- const hasMessages = chatStats.count > 0
244
- const showDaemonStartPanel = model === 'fcm' && !hasMessages && daemonRunning === false && !daemonStarting
245
- const showDaemonStartingPanel = model === 'fcm' && !hasMessages && daemonStarting
246
- const showChat = !showDaemonStartPanel && !showDaemonStartingPanel
247
-
248
- return (
249
- <div className={styles.overlay} onClick={(e) => { if (e.target === e.currentTarget) onClose() }}>
250
- <div className={styles.modal} role="dialog" aria-label="Free Coding Models Playground">
251
- <div className={styles.header}>
252
- <div className={styles.headerLeft}>
253
- <div className={styles.headerTitle}>
254
- <IconMessageChatbot size={18} />
255
- Playground
256
- </div>
257
- <div className={styles.headerSubtitle}>
258
- Chat with the FCM router · {chatStats.count} message{chatStats.count === 1 ? '' : 's'} · {chatStats.totalTokens} tokens
259
- </div>
260
- </div>
261
- <div className={styles.headerActions}>
262
- <button
263
- className={styles.iconBtn}
264
- onClick={handleClear}
265
- title="Clear conversation"
266
- disabled={!hasMessages}
267
- >
268
- <IconTrash size={16} />
269
- </button>
270
- <button
271
- className={styles.iconBtn}
272
- onClick={onClose}
273
- title="Close (Esc)"
274
- >
275
- <IconX size={16} />
276
- </button>
277
- </div>
278
- </div>
279
-
280
- <div className={styles.modelBar}>
281
- <span className={styles.modelLabel}>Model:</span>
282
- <select
283
- className={styles.modelSelect}
284
- value={model ?? 'fcm'}
285
- onChange={(e) => setModel(e.target.value)}
286
- disabled={!showChat}
287
- >
288
- {modelOptions.map((opt) => (
289
- <option key={opt.value} value={opt.value}>{opt.label}</option>
290
- ))}
291
- </select>
292
- <button
293
- className={styles.presetChip}
294
- onClick={() => setStreamOn((v) => !v)}
295
- title="Toggle streaming"
296
- >
297
- {streamOn ? '⚡ Streaming' : '🐢 One-shot'}
298
- </button>
299
- <label className={styles.prePromptToggle} title="Router persona injected as the first system message">
300
- <input
301
- type="checkbox"
302
- checked={prePromptEnabled}
303
- onChange={(e) => setPrePromptEnabled(e.target.checked)}
304
- />
305
- Pre-prompt
306
- </label>
307
- <StatusPill routerStatus={routerStatus} daemonRunning={daemonRunning} />
308
- </div>
309
-
310
- {prePromptEnabled && prePromptText && (
311
- <div className={styles.modelBar} style={{ borderTop: 'none', background: 'transparent', paddingTop: 4, paddingBottom: 8, fontSize: 11 }}>
312
- <span className={styles.modelLabel} style={{ flexShrink: 0 }}>Persona:</span>
313
- <span style={{ opacity: 0.7, fontStyle: 'italic' }}>
314
- {prePromptText.length > 160 ? `${prePromptText.slice(0, 160)}…` : prePromptText}
315
- </span>
316
- </div>
317
- )}
318
-
319
- <div className={styles.chatBody}>
320
- {showDaemonStartPanel ? (
321
- <div className={styles.daemonStartPanel}>
322
- <IconAlertTriangle size={32} style={{ color: '#fbbf24', opacity: 0.8 }} />
323
- <div className={styles.daemonStartTitle}>Router daemon is not running</div>
324
- <div className={styles.daemonStartHint}>
325
- The playground routes your chats through the FCM router daemon. Start it to begin chatting with free coding models, or pick a specific model above.
326
- </div>
327
- <button
328
- className={styles.daemonStartBtn}
329
- onClick={startDaemon}
330
- title="Start the router daemon"
331
- >
332
- {daemonStarting ? (
333
- <><IconLoader size={14} className={styles.spin} /> Starting…</>
334
- ) : (
335
- <><IconPlayerPlay size={14} /> Start Router{autoStartSec > 0 && !autoStartTriggered.current ? ` (auto in ${autoStartSec}s)` : ''}</>
336
- )}
337
- </button>
338
- <div className={styles.daemonStartAlt}>
339
- Or run <code>free-coding-models --daemon-bg</code> in your terminal
340
- </div>
341
- </div>
342
- ) : showDaemonStartingPanel ? (
343
- <div className={styles.daemonStartPanel}>
344
- <IconLoader size={32} className={styles.spin} style={{ color: 'var(--accent, #22c55e)' }} />
345
- <div className={styles.daemonStartTitle}>Starting router daemon…</div>
346
- <div className={styles.daemonStartHint}>
347
- The daemon is being launched. This usually takes a few seconds. You'll be able to chat as soon as it's ready.
348
- </div>
349
- </div>
350
- ) : null}
351
-
352
- {showChat && (
353
- <PlaygroundChat
354
- ref={chatRef}
355
- model={model || 'fcm'}
356
- variant="full"
357
- stream={streamOn}
358
- disabled={!canSend}
359
- onTurnComplete={handleTurnComplete}
360
- suggestions={SUGGESTIONS}
361
- emptyTitle="Try the FCM router in 10 seconds"
362
- emptyHint="Each request is auto-routed to the healthiest free coding model in your active set, or sent directly to the model you picked. Every reply shows the served model, latency, and TPS below it."
363
- emptyIcon={<IconMessageChatbot size={42} style={{ opacity: 0.5 }} />}
364
- placeholder="Ask anything. Enter to send, Shift+Enter for a newline."
365
- />
366
- )}
367
- </div>
368
- </div>
369
- </div>
370
- )
371
- }