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