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,135 +0,0 @@
1
- /**
2
- * @file web/src/components/changelog/ChangelogView.jsx
3
- * @description Changelog modal — M2 parity with the TUI's Changelog overlay (N key / Settings link).
4
- * 📖 Two-phase UI: index of versions on the left, details on the right. Same
5
- * 📖 content as the TUI's renderChangelog (which uses src/core/changelog-loader.js
6
- * 📖 — we hit the same data through `/api/changelog`).
7
- *
8
- * @functions
9
- * → ChangelogView → main modal component
10
- */
11
- import { useState } from 'react'
12
- import { IconArrowLeft, IconX, IconCalendar } from '@tabler/icons-react'
13
- import { useChangelog } from '../../hooks/useChangelog.js'
14
- import styles from './ChangelogView.module.css'
15
-
16
- // 📖 Section order matches the changelog files (`### Added` / `### Fixed` /
17
- // 📖 `### Changed` / `### Updated`). The TUI uses the same order in
18
- // 📖 formatChangelogForDisplay.
19
- const SECTION_LABELS = [
20
- { key: 'added', label: '✨ Added', icon: '✨' },
21
- { key: 'fixed', label: '🐛 Fixed', icon: '🐛' },
22
- { key: 'changed', label: '🔄 Changed', icon: '🔄' },
23
- { key: 'updated', label: '📝 Updated', icon: '📝' },
24
- ]
25
-
26
- export default function ChangelogView({ onClose, defaultVersion = null }) {
27
- const { sortedVersions, getVersion, loading, error } = useChangelog()
28
- // 📖 Two-phase navigation: 'index' (version list) or 'details' (one version).
29
- // 📖 `selectedVersion` is null on the index and a version string on details.
30
- const [phase, setPhase] = useState(defaultVersion ? 'details' : 'index')
31
- const [selectedVersion, setSelectedVersion] = useState(defaultVersion)
32
-
33
- const openDetails = (version) => {
34
- setSelectedVersion(version)
35
- setPhase('details')
36
- }
37
- const backToIndex = () => {
38
- setSelectedVersion(null)
39
- setPhase('index')
40
- }
41
-
42
- const details = selectedVersion ? getVersion(selectedVersion) : null
43
-
44
- return (
45
- <div className={styles.backdrop} onClick={onClose}>
46
- <div className={styles.modal} onClick={(e) => e.stopPropagation()}>
47
- <div className={styles.header}>
48
- <div className={styles.titleRow}>
49
- <h2 className={styles.title}>
50
- {phase === 'index' ? '📋 Changelog' : `📋 v${selectedVersion}`}
51
- </h2>
52
- {phase === 'details' && (
53
- <button className={styles.backBtn} onClick={backToIndex} title="Back to index (TUI: B)">
54
- <IconArrowLeft size={14} stroke={1.5} /> Index
55
- </button>
56
- )}
57
- <button className={styles.closeBtn} onClick={onClose} aria-label="Close changelog">
58
- <IconX size={18} stroke={1.5} />
59
- </button>
60
- </div>
61
- </div>
62
-
63
- <div className={styles.body}>
64
- {loading && <div className={styles.empty}>Loading changelog…</div>}
65
- {error && !loading && <div className={styles.empty}>Failed to load changelog: {error}</div>}
66
-
67
- {!loading && !error && phase === 'index' && (
68
- <div className={styles.indexWrap}>
69
- <p className={styles.indexHint}>
70
- {sortedVersions.length} versions. Click any to read the release notes.
71
- </p>
72
- <ul className={styles.versionList}>
73
- {sortedVersions.map((version) => {
74
- const changes = getVersion(version)
75
- const summary = []
76
- if (changes?.added?.length) summary.push(`${changes.added.length} added`)
77
- if (changes?.fixed?.length) summary.push(`${changes.fixed.length} fixed`)
78
- if (changes?.changed?.length) summary.push(`${changes.changed.length} changed`)
79
- return (
80
- <li key={version}>
81
- <button
82
- className={styles.versionBtn}
83
- onClick={() => openDetails(version)}
84
- >
85
- <span className={styles.versionLabel}>v{version}</span>
86
- <span className={styles.versionSummary}>
87
- {summary.length > 0 ? summary.join(' · ') : '—'}
88
- </span>
89
- </button>
90
- </li>
91
- )
92
- })}
93
- </ul>
94
- </div>
95
- )}
96
-
97
- {!loading && !error && phase === 'details' && details && (
98
- <div className={styles.details}>
99
- {SECTION_LABELS.map(({ key, label }) => {
100
- const items = details[key]
101
- if (!items || items.length === 0) return null
102
- return (
103
- <section key={key} className={styles.section}>
104
- <h3 className={styles.sectionTitle}>{label}</h3>
105
- <ul className={styles.itemList}>
106
- {items.map((item, idx) => (
107
- <li key={idx} className={styles.item}>{formatItem(item)}</li>
108
- ))}
109
- </ul>
110
- </section>
111
- )
112
- })}
113
- {SECTION_LABELS.every(({ key }) => !details[key]?.length) && (
114
- <div className={styles.empty}>No release notes for v{selectedVersion}.</div>
115
- )}
116
- </div>
117
- )}
118
-
119
- {!loading && !error && phase === 'details' && !details && (
120
- <div className={styles.empty}>No notes for v{selectedVersion}.</div>
121
- )}
122
- </div>
123
- </div>
124
- </div>
125
- )
126
- }
127
-
128
- // 📖 Strip **bold** and `code` markdown markers for the Web view. The TUI does
129
- // 📖 the same in formatChangelogForDisplay, so the two surfaces stay in sync.
130
- function formatItem(text) {
131
- return text
132
- .replace(/\*\*([^*]+)\*\*/g, '$1')
133
- .replace(/`([^`]+)`/g, '$1')
134
- .trim()
135
- }
@@ -1,159 +0,0 @@
1
- /**
2
- * @file web/src/components/changelog/ChangelogView.module.css
3
- * @description Two-phase changelog modal styles (TUI parity).
4
- */
5
-
6
- .backdrop {
7
- display: flex;
8
- flex-direction: column;
9
- flex: 1;
10
- min-height: 0;
11
- width: 100%;
12
- padding: 24px;
13
- }
14
-
15
- .modal {
16
- width: 100%;
17
- max-width: 720px;
18
- margin: 0 auto;
19
- background: var(--color-bg-card, #080908);
20
- border: 1px solid var(--color-border, #161a16);
21
- border-radius: 14px;
22
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
23
- display: flex;
24
- flex-direction: column;
25
- overflow: hidden;
26
- flex: 1;
27
- min-height: 0;
28
- }
29
-
30
- .header {
31
- padding: 16px 20px;
32
- border-bottom: 1px solid var(--color-border);
33
- }
34
-
35
- .titleRow {
36
- display: flex;
37
- align-items: center;
38
- gap: 12px;
39
- }
40
-
41
- .title {
42
- font-size: 16px;
43
- font-weight: 700;
44
- margin: 0;
45
- color: var(--color-text);
46
- flex: 1;
47
- min-width: 0;
48
- }
49
-
50
- .backBtn {
51
- display: inline-flex;
52
- align-items: center;
53
- gap: 4px;
54
- padding: 4px 10px;
55
- background: var(--color-surface);
56
- color: var(--color-text);
57
- border: 1px solid var(--color-border);
58
- border-radius: 6px;
59
- font-size: 11px;
60
- font-weight: 600;
61
- cursor: pointer;
62
- font-family: var(--font-sans);
63
- white-space: nowrap;
64
- }
65
- .backBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
66
-
67
- .closeBtn {
68
- background: transparent;
69
- border: none;
70
- color: var(--color-text-muted);
71
- cursor: pointer;
72
- padding: 4px;
73
- border-radius: 4px;
74
- display: inline-flex;
75
- align-items: center;
76
- justify-content: center;
77
- flex-shrink: 0;
78
- }
79
- .closeBtn:hover { color: var(--color-text); background: var(--color-bg-hover); }
80
-
81
- .body {
82
- flex: 1;
83
- overflow-y: auto;
84
- padding: 16px 20px 20px;
85
- }
86
-
87
- .indexWrap {}
88
- .indexHint {
89
- margin: 0 0 12px;
90
- font-size: 11px;
91
- color: var(--color-text-muted);
92
- }
93
-
94
- .versionList {
95
- list-style: none;
96
- margin: 0;
97
- padding: 0;
98
- display: flex;
99
- flex-direction: column;
100
- gap: 4px;
101
- }
102
- .versionBtn {
103
- display: flex;
104
- align-items: center;
105
- justify-content: space-between;
106
- gap: 12px;
107
- width: 100%;
108
- padding: 8px 12px;
109
- background: var(--color-surface);
110
- border: 1px solid var(--color-border);
111
- border-radius: 6px;
112
- cursor: pointer;
113
- text-align: left;
114
- transition: all 100ms;
115
- font-family: var(--font-sans);
116
- }
117
- .versionBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
118
- .versionLabel {
119
- font-family: var(--font-mono);
120
- font-size: 13px;
121
- font-weight: 700;
122
- color: var(--color-text);
123
- }
124
- .versionSummary {
125
- font-size: 11px;
126
- color: var(--color-text-muted);
127
- }
128
-
129
- .details { display: flex; flex-direction: column; gap: 14px; }
130
- .section {}
131
- .sectionTitle {
132
- font-size: 12px;
133
- font-weight: 700;
134
- text-transform: uppercase;
135
- letter-spacing: 0.6px;
136
- color: var(--color-text-muted);
137
- margin: 0 0 6px;
138
- }
139
- .itemList {
140
- list-style: disc;
141
- padding-left: 20px;
142
- margin: 0;
143
- display: flex;
144
- flex-direction: column;
145
- gap: 4px;
146
- }
147
- .item {
148
- font-size: 12px;
149
- color: var(--color-text);
150
- line-height: 1.55;
151
- word-break: break-word;
152
- }
153
-
154
- .empty {
155
- text-align: center;
156
- padding: 40px 20px;
157
- color: var(--color-text-dim);
158
- font-size: 13px;
159
- }
@@ -1,217 +0,0 @@
1
- /**
2
- * @file web/src/components/dashboard/DetailPanel.jsx
3
- * @description Slide-in detail panel showing full model stats, metrics, and a latency chart.
4
- * 📖 Rendered as a fixed overlay on the right side. Controlled by `model` prop (null = hidden).
5
- * Displays model ID, provider, tier, SWE score, context, status, pings, stability, verdict, uptime,
6
- * ping count, API key status, a larger SVG latency trend chart, and an M1 favorites + benchmark
7
- * 📖 action section (star + reorder + per-row AI Speed Test button).
8
- * @functions DetailPanel → main panel component, buildDetailChart → SVG chart builder
9
- */
10
- import { IconStar, IconStarFilled, IconChevronUp, IconChevronDown, IconPlayerPlayFilled, IconAlertTriangle } from '@tabler/icons-react'
11
- import TierBadge from '../atoms/TierBadge.jsx'
12
- import VerdictBadge from '../atoms/VerdictBadge.jsx'
13
- import StatusDot from '../atoms/StatusDot.jsx'
14
- import StabilityCell from '../atoms/StabilityCell.jsx'
15
- import { formatPing, formatAvg, pingClass } from '../../utils/format.js'
16
- import { sweClass } from '../../utils/ranks.js'
17
- import LaunchButton from '../launch/LaunchButton.jsx'
18
- import ToolPicker from '../tools/ToolPicker.jsx'
19
- import { getToolMeta, isModelCompatibleWithTool } from '../../../../src/core/tool-metadata.js'
20
- import styles from './DetailPanel.module.css'
21
-
22
- function buildDetailChart(history) {
23
- if (!history || history.length < 2) {
24
- return <div className={styles.chartEmpty}>Waiting for ping data...</div>
25
- }
26
-
27
- const valid = history.filter(p => p.code === '200' || p.code === '401')
28
- if (valid.length < 2) {
29
- return <div className={styles.chartEmpty}>Not enough data yet...</div>
30
- }
31
-
32
- const values = valid.map(p => p.ms)
33
- const max = Math.max(...values, 1)
34
- const min = Math.min(...values, 0)
35
- const range = max - min || 1
36
- const w = 340, h = 100
37
- const padding = 4
38
-
39
- const points = values.map((v, i) => {
40
- const x = padding + i * ((w - 2 * padding) / (values.length - 1))
41
- const y = padding + (h - 2 * padding) - ((v - min) / range) * (h - 2 * padding)
42
- return [x.toFixed(1), y.toFixed(1)]
43
- })
44
-
45
- const linePoints = points.map(p => p.join(',')).join(' ')
46
- const areaPoints = `${points[0][0]},${h - padding} ${linePoints} ${points[points.length - 1][0]},${h - padding}`
47
- const lastPt = points[points.length - 1]
48
-
49
- return (
50
- <svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: 'block' }}>
51
- <defs>
52
- <linearGradient id="chart-grad" x1="0" y1="0" x2="0" y2="1">
53
- <stop offset="0%" stopColor="var(--color-accent)" stopOpacity="0.3" />
54
- <stop offset="100%" stopColor="var(--color-accent)" stopOpacity="0.02" />
55
- </linearGradient>
56
- </defs>
57
- <polygon fill="url(#chart-grad)" points={areaPoints} />
58
- <polyline fill="none" stroke="var(--color-accent)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" points={linePoints} />
59
- <circle cx={lastPt[0]} cy={lastPt[1]} r="3.5" fill="var(--color-accent)" stroke="var(--color-bg)" strokeWidth="1.5" />
60
- <text x={padding} y={h - 2} fontSize="9" fill="var(--color-text-dim)" fontFamily="var(--font-mono)">{min}ms</text>
61
- <text x={w - padding} y={padding + 8} fontSize="9" fill="var(--color-text-dim)" fontFamily="var(--font-mono)" textAnchor="end">{max}ms</text>
62
- </svg>
63
- )
64
- }
65
-
66
- function StatRow({ label, children }) {
67
- return (
68
- <div className={styles.stat}>
69
- <span className={styles.statLabel}>{label}</span>
70
- <span className={styles.statValue}>{children}</span>
71
- </div>
72
- )
73
- }
74
-
75
- export default function DetailPanel({
76
- model, onClose, favorites, onBenchmark, onLaunch, onToast,
77
- toolMode = 'opencode', onSetToolMode, onCycleToolMode, onOpenFallback,
78
- }) {
79
- if (!model) return null
80
-
81
- const chartSvg = buildDetailChart(model.pingHistory)
82
- const isFav = favorites?.isFavorite(model) ?? false
83
- const favRank = favorites?.favoriteRank(model) ?? Number.MAX_SAFE_INTEGER
84
- const isFavAtTop = isFav && favRank === 0
85
- const isFavAtBottom = isFav && favRank === (favorites?.favorites.length ?? 0) - 1
86
- const toolMeta = getToolMeta(toolMode)
87
- const compatible = isModelCompatibleWithTool(model.providerKey, toolMode)
88
-
89
- return (
90
- <div className={`${styles.panel} ${styles.panelOpen}`}>
91
- <div className={styles.header}>
92
- <h3 className={styles.title}>{model.label}</h3>
93
- <button className={styles.closeBtn} onClick={onClose}>&times;</button>
94
- </div>
95
- <div className={styles.body}>
96
- <StatRow label="Model ID">
97
- <span style={{ fontSize: 11, wordBreak: 'break-all' }}>{model.modelId}</span>
98
- </StatRow>
99
- <StatRow label="Provider">{model.origin}</StatRow>
100
- <StatRow label="Tier"><TierBadge tier={model.tier} /></StatRow>
101
- <StatRow label="SWE-bench Score">
102
- <span className={`${styles.swe} ${styles[sweClass(model.sweScore)]}`}>{model.sweScore || '—'}</span>
103
- </StatRow>
104
- <StatRow label="Context Window">{model.ctx || '—'}</StatRow>
105
- <StatRow label="Status">
106
- <StatusDot status={model.status} /> {model.status}
107
- </StatRow>
108
- <StatRow label="Latest Ping">
109
- <span className={`${styles.ping} ${styles[pingClass(model.latestPing)]}`}>
110
- {formatPing(model.latestPing, model.latestCode).text}
111
- </span>
112
- </StatRow>
113
- <StatRow label="Average Latency">
114
- <span className={`${styles.ping} ${styles[pingClass(model.avg)]}`}>
115
- {formatAvg(model.avg).text}
116
- </span>
117
- </StatRow>
118
- <StatRow label="P95 Latency">
119
- {model.p95 != null && model.p95 !== Infinity ? `${model.p95}ms` : '—'}
120
- </StatRow>
121
- <StatRow label="Jitter (σ)">
122
- {model.jitter != null && model.jitter !== Infinity ? `${model.jitter}ms` : '—'}
123
- </StatRow>
124
- <StatRow label="Stability Score">
125
- <StabilityCell score={model.stability} />
126
- </StatRow>
127
- <StatRow label="Verdict">
128
- <VerdictBadge verdict={model.verdict} httpCode={model.httpCode} />
129
- </StatRow>
130
- <StatRow label="Uptime">
131
- {model.uptime > 0 ? `${model.uptime}%` : '—'}
132
- </StatRow>
133
- <StatRow label="Ping Count">{model.pingCount}</StatRow>
134
- <StatRow label="API Key">
135
- {model.hasApiKey ? '✅ Configured' : '❌ Missing'}
136
- </StatRow>
137
- <StatRow label="Tool">
138
- <ToolPicker
139
- compact
140
- toolMode={toolMode}
141
- onSetToolMode={onSetToolMode}
142
- onCycleToolMode={onCycleToolMode}
143
- />
144
- </StatRow>
145
- {!compatible && (
146
- <div className={styles.compatWarning}>
147
- <IconAlertTriangle size={14} />
148
- <span>{model.label} is not compatible with {toolMeta.emoji} {toolMeta.label}.</span>
149
- <button onClick={() => onOpenFallback?.(model)}>Install in compatible tool</button>
150
- </div>
151
- )}
152
-
153
- {/* ── M1/M3: favorites + benchmark + endpoint install action section ── */}
154
- {(favorites || onBenchmark) && (
155
- <div className={styles.actions}>
156
- {favorites && (
157
- <div className={styles.favBlock}>
158
- <button
159
- className={`${styles.favBtn} ${isFav ? styles.favBtnActive : ''}`}
160
- onClick={() => favorites.toggle(model)}
161
- title={isFav ? `Unfavorite ${model.label} (TUI: F)` : `Favorite ${model.label} (TUI: F)`}
162
- >
163
- {isFav ? <IconStarFilled size={14} stroke={1.5} /> : <IconStar size={14} stroke={1.5} />}
164
- <span>{isFav ? 'Favorited' : 'Add to favorites'}</span>
165
- </button>
166
- {isFav && (
167
- <div className={styles.reorderRow}>
168
- <button
169
- className={styles.reorderBtn}
170
- disabled={isFavAtTop}
171
- onClick={() => favorites.reorder(model, 'up')}
172
- title="Move up in router priority (TUI: Shift+↑)"
173
- aria-label="Move favorite up"
174
- >
175
- <IconChevronUp size={12} stroke={1.5} /> Up
176
- </button>
177
- <button
178
- className={styles.reorderBtn}
179
- disabled={isFavAtBottom}
180
- onClick={() => favorites.reorder(model, 'down')}
181
- title="Move down in router priority (TUI: Shift+↓)"
182
- aria-label="Move favorite down"
183
- >
184
- <IconChevronDown size={12} stroke={1.5} /> Down
185
- </button>
186
- <span className={styles.rankBadge}>#{favRank + 1}</span>
187
- </div>
188
- )}
189
- </div>
190
- )}
191
- {onLaunch && (
192
- <LaunchButton model={model} toolMode={toolMode} onLaunch={onLaunch} />
193
- )}
194
- {onBenchmark && (
195
- <button
196
- className={styles.benchBtn}
197
- onClick={() => {
198
- onBenchmark(model)
199
- onToast?.(`Benchmark started for ${model.label}…`, 'info')
200
- }}
201
- title="Run AI Speed Test on this model (TUI: Ctrl+A)"
202
- >
203
- <IconPlayerPlayFilled size={12} stroke={1.5} />
204
- <span>AI Speed Test</span>
205
- </button>
206
- )}
207
- </div>
208
- )}
209
-
210
- <div className={styles.chart}>
211
- <div className={styles.chartTitle}>Latency Trend (last 20 pings)</div>
212
- {chartSvg}
213
- </div>
214
- </div>
215
- </div>
216
- )
217
- }