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,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
- }