free-coding-models 0.5.86 → 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 (163) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/package.json +8 -5
  5. package/sources.js +4 -4
  6. package/src/core/cache.js +3 -1
  7. package/src/core/config.js +43 -5
  8. package/src/core/endpoint-installer.js +23 -12
  9. package/src/core/installed-models-manager.js +43 -20
  10. package/src/core/kilo-config.js +51 -5
  11. package/src/core/kilo.js +5 -3
  12. package/src/core/legacy-proxy-cleanup.js +22 -5
  13. package/src/core/model-merger.js +4 -0
  14. package/src/core/models-drift.js +9 -0
  15. package/src/core/opencode-config.js +57 -5
  16. package/src/core/opencode.js +67 -41
  17. package/src/core/playground.js +7 -0
  18. package/src/core/probe-cache.js +35 -4
  19. package/src/core/provider-key-tester.js +10 -2
  20. package/src/core/provider-metadata.js +10 -0
  21. package/src/core/provider-quota-fetchers.js +14 -7
  22. package/src/core/router-daemon.js +208 -26
  23. package/src/core/runtime-telemetry.js +27 -1
  24. package/src/core/shared-helpers.js +12 -1
  25. package/src/core/shell-env.js +3 -1
  26. package/src/core/telemetry.js +18 -5
  27. package/src/core/tool-launchers.js +9 -6
  28. package/src/core/updater.js +28 -2
  29. package/src/core/utils.js +76 -71
  30. package/src/tui/app.js +38 -14
  31. package/src/tui/command-palette.js +22 -2
  32. package/src/tui/key-handler.js +123 -179
  33. package/src/tui/overlays.js +21 -71
  34. package/src/tui/render-helpers.js +62 -4
  35. package/src/tui/render-table.js +34 -3
  36. package/src/tui/theme.js +36 -5
  37. package/src/tui/tui-filters.js +28 -8
  38. package/src/tui/tui-state.js +22 -6
  39. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  40. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  41. package/web/dist/index.html +5 -9
  42. package/web/index.html +3 -7
  43. package/web/server.js +97 -23
  44. package/web/vite.config.js +8 -0
  45. package/web/app.legacy.js +0 -900
  46. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  47. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  48. package/web/public/favicon.ico +0 -0
  49. package/web/public/favicons/apple-touch-icon.png +0 -0
  50. package/web/public/favicons/browserconfig.xml +0 -12
  51. package/web/public/favicons/favicon-16x16.png +0 -0
  52. package/web/public/favicons/favicon-192x192.png +0 -0
  53. package/web/public/favicons/favicon-32x32.png +0 -0
  54. package/web/public/favicons/favicon-48x48.png +0 -0
  55. package/web/public/favicons/favicon-512x512.png +0 -0
  56. package/web/public/favicons/favicon-96x96.png +0 -0
  57. package/web/public/favicons/favicon.ico +0 -0
  58. package/web/public/favicons/mstile-150x150.png +0 -0
  59. package/web/public/favicons/mstile-310x150.png +0 -0
  60. package/web/public/favicons/mstile-310x310.png +0 -0
  61. package/web/public/favicons/mstile-512x512.png +0 -0
  62. package/web/public/favicons/mstile-70x70.png +0 -0
  63. package/web/public/favicons/site.webmanifest +0 -25
  64. package/web/src/App.jsx +0 -521
  65. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  66. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  67. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  68. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  69. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  70. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  71. package/web/src/components/atoms/HealthCell.jsx +0 -143
  72. package/web/src/components/atoms/HealthCell.module.css +0 -60
  73. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  74. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  75. package/web/src/components/atoms/MoodCell.jsx +0 -25
  76. package/web/src/components/atoms/MoodCell.module.css +0 -6
  77. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  78. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  79. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  80. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  81. package/web/src/components/atoms/RankCell.jsx +0 -9
  82. package/web/src/components/atoms/RankCell.module.css +0 -9
  83. package/web/src/components/atoms/Sparkline.jsx +0 -63
  84. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  85. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  86. package/web/src/components/atoms/StatusDot.jsx +0 -13
  87. package/web/src/components/atoms/StatusDot.module.css +0 -29
  88. package/web/src/components/atoms/TPSCell.jsx +0 -36
  89. package/web/src/components/atoms/TPSCell.module.css +0 -44
  90. package/web/src/components/atoms/TierBadge.jsx +0 -10
  91. package/web/src/components/atoms/TierBadge.module.css +0 -29
  92. package/web/src/components/atoms/Toast.jsx +0 -25
  93. package/web/src/components/atoms/Toast.module.css +0 -41
  94. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  95. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  96. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  97. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  98. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  99. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  100. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  101. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  102. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  103. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  104. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  105. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  106. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  107. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  108. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  109. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  110. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  111. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  112. package/web/src/components/help/HelpView.jsx +0 -201
  113. package/web/src/components/help/HelpView.module.css +0 -156
  114. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  115. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  116. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  117. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  118. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  119. package/web/src/components/launch/LaunchButton.jsx +0 -30
  120. package/web/src/components/launch/LaunchButton.module.css +0 -35
  121. package/web/src/components/launch/LaunchModal.module.css +0 -125
  122. package/web/src/components/layout/Footer.jsx +0 -19
  123. package/web/src/components/layout/Footer.module.css +0 -10
  124. package/web/src/components/layout/Header.jsx +0 -265
  125. package/web/src/components/layout/Header.module.css +0 -357
  126. package/web/src/components/palette/CommandPalette.jsx +0 -301
  127. package/web/src/components/palette/CommandPalette.module.css +0 -121
  128. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  129. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  130. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  131. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  132. package/web/src/components/recommend/RecommendView.jsx +0 -121
  133. package/web/src/components/recommend/RecommendView.module.css +0 -57
  134. package/web/src/components/router/RouterView.jsx +0 -1212
  135. package/web/src/components/router/RouterView.module.css +0 -1289
  136. package/web/src/components/settings/SettingsView.jsx +0 -555
  137. package/web/src/components/settings/SettingsView.module.css +0 -550
  138. package/web/src/components/tools/ToolPicker.jsx +0 -86
  139. package/web/src/components/tools/ToolPicker.module.css +0 -97
  140. package/web/src/components/update/UpdateChip.jsx +0 -104
  141. package/web/src/components/update/UpdateChip.module.css +0 -146
  142. package/web/src/global.css +0 -298
  143. package/web/src/hooks/urlState.constants.js +0 -28
  144. package/web/src/hooks/useChangelog.js +0 -51
  145. package/web/src/hooks/useColumnSizing.js +0 -211
  146. package/web/src/hooks/useFavorites.js +0 -150
  147. package/web/src/hooks/useFilter.js +0 -310
  148. package/web/src/hooks/useInstalledModels.js +0 -42
  149. package/web/src/hooks/useRecommend.js +0 -67
  150. package/web/src/hooks/useRouterDashboard.js +0 -133
  151. package/web/src/hooks/useSSE.js +0 -73
  152. package/web/src/hooks/useSocket.js +0 -203
  153. package/web/src/hooks/useTheme.js +0 -83
  154. package/web/src/hooks/useTokenUsage.js +0 -103
  155. package/web/src/hooks/useToolMode.js +0 -77
  156. package/web/src/hooks/useUpdateChecker.js +0 -91
  157. package/web/src/hooks/useUrlState.js +0 -170
  158. package/web/src/main.jsx +0 -15
  159. package/web/src/utils/download.js +0 -15
  160. package/web/src/utils/format.js +0 -42
  161. package/web/src/utils/m3.js +0 -57
  162. package/web/src/utils/ranks.js +0 -37
  163. 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
- }