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.
- package/README.md +109 -888
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.87.md +48 -0
- package/changelog/v0.5.88.md +16 -0
- package/package.json +13 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/security.js +170 -24
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +173 -71
- package/src/tui/app.js +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/assets/index-DFg1h0Nd.js +44 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-Dm-Xfby9.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- 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
|
-
}
|