free-coding-models 0.5.86 → 0.5.88

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