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,69 +0,0 @@
1
- /**
2
- * @file web/src/components/launch/IncompatibleFallbackModal.jsx
3
- * @description Web equivalent of the TUI incompatible-fallback overlay. Lets the
4
- * user switch to a compatible endpoint target or install a similar model endpoint.
5
- *
6
- * @functions IncompatibleFallbackModal → switch-tool and similar-model picker
7
- * @exports IncompatibleFallbackModal
8
- */
9
- import { IconAlertTriangle, IconPlugConnected, IconTool, IconX } from '@tabler/icons-react'
10
- import {
11
- findSimilarCompatibleModels,
12
- getCompatibleTools,
13
- getToolMeta,
14
- } from '../../../../src/core/tool-metadata.js'
15
- import styles from './LaunchModal.module.css'
16
-
17
- export default function IncompatibleFallbackModal({ request, models = [], onClose, onSwitchToolLaunch, onLaunchSimilar }) {
18
- if (!request?.model) return null
19
- const { model, toolMode } = request
20
- const activeMeta = getToolMeta(toolMode)
21
- const compatibleTools = getCompatibleTools(model.providerKey)
22
- const similar = findSimilarCompatibleModels(model.sweScore, toolMode, models, 3)
23
- .filter((candidate) => candidate.modelId !== model.modelId || candidate.providerKey !== model.providerKey)
24
-
25
- return (
26
- <div className={styles.backdrop} onClick={onClose}>
27
- <div className={styles.modalWide} onClick={(event) => event.stopPropagation()}>
28
- <div className={styles.header}>
29
- <div>
30
- <p className={styles.eyebrow}>Incompatible model</p>
31
- <h2><IconAlertTriangle size={18} /> {model.label} cannot be installed in {activeMeta.emoji} {activeMeta.label}</h2>
32
- </div>
33
- <button className={styles.close} onClick={onClose} aria-label="Close"><IconX size={18} /></button>
34
- </div>
35
- <div className={styles.grid}>
36
- <section className={styles.card}>
37
- <h3><IconTool size={15} /> Switch tool</h3>
38
- <p className={styles.muted}>This provider can be configured in these tools. Pick one and FCM installs the endpoint.</p>
39
- <div className={styles.stack}>
40
- {compatibleTools.map((mode) => {
41
- const meta = getToolMeta(mode)
42
- return (
43
- <button key={mode} className={styles.option} onClick={() => onSwitchToolLaunch?.(mode, model)}>
44
- <span>{meta.emoji}</span>
45
- <strong>{meta.label}</strong>
46
- </button>
47
- )
48
- })}
49
- </div>
50
- </section>
51
- <section className={styles.card}>
52
- <h3><IconPlugConnected size={15} /> Similar models</h3>
53
- <p className={styles.muted}>Or keep {activeMeta.label} and install a compatible model close to the same SWE score.</p>
54
- <div className={styles.stack}>
55
- {similar.length === 0 && <p className={styles.muted}>No close compatible alternative found yet.</p>}
56
- {similar.map((candidate) => (
57
- <button key={`${candidate.providerKey}/${candidate.modelId}`} className={styles.option} onClick={() => onLaunchSimilar?.(candidate)}>
58
- <span>{candidate.tier}</span>
59
- <strong>{candidate.label}</strong>
60
- <small>{candidate.providerKey} · {candidate.sweScore}</small>
61
- </button>
62
- ))}
63
- </div>
64
- </section>
65
- </div>
66
- </div>
67
- </div>
68
- )
69
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * @file web/src/components/launch/LaunchButton.jsx
3
- * @description Reusable M3 endpoint install action for table rows, DetailPanel, and Recommend results.
4
- * @functions LaunchButton → renders a plug button that installs the selected model endpoint
5
- * @exports LaunchButton
6
- */
7
- import { IconPlugConnected } from '@tabler/icons-react'
8
- import { getToolMeta } from '../../../../src/core/tool-metadata.js'
9
- import styles from './LaunchButton.module.css'
10
-
11
- export default function LaunchButton({ model, toolMode = 'opencode', onLaunch, variant = 'default', disabled = false }) {
12
- const meta = getToolMeta(toolMode)
13
- const label = variant === 'icon' ? 'Install endpoint' : `Install endpoint in ${meta.emoji} ${meta.label}`
14
- return (
15
- <button
16
- type="button"
17
- className={`${styles.button} ${styles[variant] || ''}`}
18
- disabled={disabled || !model}
19
- onClick={(event) => {
20
- event.stopPropagation()
21
- if (model) onLaunch?.(model)
22
- }}
23
- title={model ? `Install ${model.label} endpoint in ${meta.label}` : 'Select a model first'}
24
- aria-label={model ? `Install ${model.label} endpoint in ${meta.label}` : 'Install selected model endpoint'}
25
- >
26
- <IconPlugConnected size={variant === 'icon' ? 13 : 14} stroke={1.8} />
27
- {variant !== 'icon' && <span>{label}</span>}
28
- </button>
29
- )
30
- }
@@ -1,35 +0,0 @@
1
- /** @file web/src/components/launch/LaunchButton.module.css */
2
- .button {
3
- display: inline-flex;
4
- align-items: center;
5
- justify-content: center;
6
- gap: 7px;
7
- padding: 8px 12px;
8
- border: 1px solid rgba(61, 220, 132, 0.45);
9
- border-radius: 6px;
10
- background: rgba(61, 220, 132, 0.12);
11
- color: #3ddc84;
12
- cursor: pointer;
13
- font-size: 12px;
14
- font-weight: 800;
15
- font-family: var(--font-mono);
16
- transition: transform 120ms, background 120ms, border-color 120ms;
17
- }
18
- .button:hover:not(:disabled) {
19
- transform: translateY(-1px);
20
- background: rgba(61, 220, 132, 0.20);
21
- border-color: rgba(61, 220, 132, 0.75);
22
- }
23
- .button:disabled { opacity: 0.45; cursor: not-allowed; }
24
- .icon {
25
- width: 26px;
26
- height: 24px;
27
- padding: 0;
28
- opacity: 0.72;
29
- }
30
- .icon:hover:not(:disabled) { opacity: 1; }
31
- :global([data-theme="light"]) .button {
32
- color: #087d42;
33
- border-color: rgba(8, 125, 66, 0.35);
34
- background: rgba(8, 125, 66, 0.08);
35
- }
@@ -1,125 +0,0 @@
1
- /** @file web/src/components/launch/LaunchModal.module.css */
2
- .backdrop {
3
- position: fixed;
4
- inset: 0;
5
- z-index: 500;
6
- display: flex;
7
- align-items: center;
8
- justify-content: center;
9
- padding: 24px;
10
- background: rgba(0, 0, 0, 0.62);
11
- backdrop-filter: blur(4px);
12
- }
13
- .modal,
14
- .modalWide {
15
- width: min(560px, 96vw);
16
- max-height: min(760px, 92vh);
17
- overflow: auto;
18
- border: 1px solid var(--color-border);
19
- border-radius: 14px;
20
- background: var(--color-bg-elevated);
21
- color: var(--color-text);
22
- box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
23
- }
24
- .modalWide { width: min(920px, 96vw); }
25
- .header {
26
- display: flex;
27
- align-items: flex-start;
28
- justify-content: space-between;
29
- gap: 18px;
30
- padding: 20px 22px;
31
- border-bottom: 1px solid var(--color-border);
32
- }
33
- .header h2 {
34
- display: flex;
35
- align-items: center;
36
- gap: 8px;
37
- margin: 4px 0 0;
38
- font-size: 18px;
39
- }
40
- .eyebrow {
41
- margin: 0;
42
- color: var(--color-accent);
43
- font-size: 11px;
44
- font-weight: 900;
45
- text-transform: uppercase;
46
- letter-spacing: .9px;
47
- }
48
- .close {
49
- display: inline-flex;
50
- align-items: center;
51
- justify-content: center;
52
- width: 30px;
53
- height: 30px;
54
- border: 1px solid var(--color-border);
55
- border-radius: 7px;
56
- background: var(--color-surface);
57
- color: var(--color-text-muted);
58
- cursor: pointer;
59
- }
60
- .close:hover { color: var(--color-text); background: var(--color-bg-hover); }
61
- .body { padding: 20px 22px; display: flex; flex-direction: column; gap: 12px; }
62
- .command {
63
- display: block;
64
- padding: 12px;
65
- border: 1px solid var(--color-border);
66
- border-radius: 8px;
67
- background: var(--color-surface);
68
- color: var(--color-accent);
69
- white-space: pre-wrap;
70
- word-break: break-word;
71
- }
72
- .note,
73
- .muted { color: var(--color-text-muted); font-size: 13px; line-height: 1.45; }
74
- .docs { display: inline-flex; align-items: center; gap: 5px; color: var(--color-accent); font-weight: 700; text-decoration: none; }
75
- .error { color: #ff5a7a; font-weight: 700; }
76
- .footer {
77
- display: flex;
78
- justify-content: flex-end;
79
- gap: 10px;
80
- padding: 16px 22px 20px;
81
- border-top: 1px solid var(--color-border);
82
- }
83
- .primary,
84
- .secondary,
85
- .option {
86
- border: 1px solid var(--color-border);
87
- border-radius: 8px;
88
- cursor: pointer;
89
- font-weight: 800;
90
- }
91
- .primary {
92
- padding: 9px 14px;
93
- background: var(--color-accent);
94
- color: var(--color-bg);
95
- border-color: var(--color-accent);
96
- }
97
- .primary:disabled { opacity: .55; cursor: not-allowed; }
98
- .secondary {
99
- padding: 9px 14px;
100
- background: var(--color-surface);
101
- color: var(--color-text);
102
- }
103
- .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; padding: 18px; }
104
- .card {
105
- padding: 16px;
106
- border: 1px solid var(--color-border);
107
- border-radius: 12px;
108
- background: var(--color-surface);
109
- }
110
- .card h3 { display: flex; align-items: center; gap: 7px; margin: 0 0 6px; font-size: 14px; }
111
- .stack { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
112
- .option {
113
- width: 100%;
114
- display: grid;
115
- grid-template-columns: auto 1fr auto;
116
- align-items: center;
117
- gap: 10px;
118
- padding: 10px;
119
- background: var(--color-bg-card);
120
- color: var(--color-text);
121
- text-align: left;
122
- }
123
- .option:hover { border-color: var(--color-accent); background: var(--color-bg-hover); }
124
- .option small { color: var(--color-text-dim); font-family: var(--font-mono); font-size: 10px; }
125
- @media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
@@ -1,19 +0,0 @@
1
- /**
2
- * @file web/src/components/layout/Footer.jsx
3
- * @description Simple footer with author credit and links.
4
- */
5
- import styles from './Footer.module.css'
6
-
7
- export default function Footer() {
8
- return (
9
- <footer className={styles.footer}>
10
- <div className={styles.left}>
11
- by <a href="https://vavanessa.dev" target="_blank" rel="noopener">Vava-Nessa</a>
12
- </div>
13
- <div className={styles.right}>
14
- <a href="https://github.com/vava-nessa/free-coding-models" target="_blank" rel="noopener">GitHub</a>
15
- <a href="https://discord.gg/ZTNFHvvCkU" target="_blank" rel="noopener">Discord</a>
16
- </div>
17
- </footer>
18
- )
19
- }
@@ -1,10 +0,0 @@
1
- .footer {
2
- position: relative; z-index: 1;
3
- display: flex; align-items: center; justify-content: space-between;
4
- padding: 16px 24px;
5
- border-top: 1px solid var(--color-border);
6
- font-size: 12px; color: var(--color-text-dim);
7
- }
8
- .left a, .right a { color: var(--color-text-dim); text-decoration: none; }
9
- .left a:hover, .right a:hover { color: var(--color-accent); }
10
- .right { display: flex; gap: 16px; }
@@ -1,265 +0,0 @@
1
- /**
2
- * @file web/src/components/layout/Header.jsx
3
- * @description Top header bar — global navigation + search + actions.
4
- * 📖 Layout refactor (M1): no more left sidebar. All navigation lives here.
5
- * 📖 - Always-visible nav buttons: Dashboard, Settings, Analytics, Recommend, Router
6
- * 📖 - Overflow menu (kebab): Help, Changelog, Install Endpoints, Installed Models
7
- * 📖 - Right side: ⌘K (command palette), AI Latency, theme, export
8
- * 📖 Each unimplemented feature shows a friendly "Coming in M2/M3/M4" toast so
9
- * 📖 the menu structure is honest and complete from day one.
10
- */
11
- import { useEffect, useRef, useState } from 'react'
12
- import {
13
- IconBolt, IconSearch, IconDownload, IconSettings, IconMoon, IconSun,
14
- IconPlayerPlay, IconCommand, IconLayoutDashboard, IconActivity,
15
- IconSparkles, IconRoute, IconDots, IconQuestionMark, IconHistory,
16
- IconPlug, IconFolders, IconMenu2, IconMessageChatbot,
17
- } from '@tabler/icons-react'
18
- import ToolPicker from '../tools/ToolPicker.jsx'
19
- import styles from './Header.module.css'
20
-
21
- // 📖 Top-level nav items — always visible as buttons. Inlined here so the
22
- // 📖 order, icon, and "coming soon" milestone are colocated with the
23
- // 📖 rendering code. When a view ships, remove the `comingIn` field.
24
- const NAV_ITEMS = [
25
- { id: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
26
- { id: 'router', label: 'Router', icon: IconRoute },
27
- { id: 'playground', label: 'Playground', icon: IconMessageChatbot },
28
- { id: 'help', label: 'Help', icon: IconQuestionMark },
29
- { id: 'install-endpoints', label: 'Install Endpoints', icon: IconPlug },
30
- ]
31
-
32
- // 📖 Overflow menu items
33
- const MENU_ITEMS = [
34
- { id: 'analytics', label: 'Analytics', icon: IconActivity },
35
- { id: 'recommend', label: 'Recommend', icon: IconSparkles },
36
- { id: 'changelog', label: 'Changelog', icon: IconHistory },
37
- { id: 'installed-models', label: 'Installed Models', icon: IconFolders },
38
- ]
39
-
40
- export default function Header({
41
- searchQuery, onSearchChange,
42
- currentView, onNavigate,
43
- onToggleTheme, onOpenExport, onOpenCommandPalette,
44
- onBenchmark, benchmarkRunning, benchmarkTotal, benchmarkCompleted,
45
- modelsCount, theme, onToast,
46
- toolMode = 'opencode', onSetToolMode, onCycleToolMode,
47
- updateSlot = null,
48
- }) {
49
- const [menuOpen, setMenuOpen] = useState(false)
50
- const [mobileNavOpen, setMobileNavOpen] = useState(false)
51
- const menuRef = useRef(null)
52
-
53
- // 📖 Close the overflow menu on outside click or Esc.
54
- useEffect(() => {
55
- if (!menuOpen && !mobileNavOpen) return
56
- const onClick = (e) => {
57
- if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false)
58
- }
59
- const onKey = (e) => {
60
- if (e.key === 'Escape') { setMenuOpen(false); setMobileNavOpen(false) }
61
- }
62
- document.addEventListener('mousedown', onClick)
63
- document.addEventListener('keydown', onKey)
64
- return () => {
65
- document.removeEventListener('mousedown', onClick)
66
- document.removeEventListener('keydown', onKey)
67
- }
68
- }, [menuOpen, mobileNavOpen])
69
-
70
- const handleNavClick = (item) => {
71
- if (item.comingIn) {
72
- onToast?.(`${item.label} arrives in milestone ${item.comingIn}.`, 'info')
73
- return
74
- }
75
- onNavigate(item.id)
76
- setMobileNavOpen(false)
77
- }
78
-
79
- const handleMenuClick = (item) => {
80
- setMenuOpen(false)
81
- if (item.comingIn) {
82
- onToast?.(`${item.label} arrives in milestone ${item.comingIn}.`, 'info')
83
- return
84
- }
85
- onNavigate(item.id)
86
- }
87
-
88
- return (
89
- <header className={styles.header}>
90
- <div className={styles.left}>
91
- <div className={styles.logo} onClick={() => onNavigate('dashboard')} style={{ cursor: 'pointer' }}>
92
- <svg className={styles.logoIconSvg} width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
93
- <path d="M4 5L11 12L4 19" stroke="var(--color-brand)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
94
- <path d="M14 5V19M14 5H21M14 11H18" stroke="var(--color-brand)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
95
- </svg>
96
- <span className={styles.logoText}>
97
- <span className={styles.logoTextHighlight}>free</span>
98
- <span>-coding-models</span>
99
- <span className={styles.logoTextHighlight}>_</span>
100
- </span>
101
- </div>
102
- <span className={styles.version}>v{__APP_VERSION__}</span>
103
-
104
- {/* 📖 M5: Hamburger for narrow viewports (never a sidebar). Shows a
105
- dropdown with all nav + overflow items on mobile. */}
106
- <button
107
- className={styles.hamburgerBtn}
108
- onClick={() => setMobileNavOpen((o) => !o)}
109
- aria-label="Navigation menu"
110
- aria-expanded={mobileNavOpen}
111
- aria-haspopup="true"
112
- >
113
- <IconMenu2 size={18} stroke={1.5} />
114
- </button>
115
- {mobileNavOpen && (
116
- <div className={styles.mobileNav} role="menu">
117
- {NAV_ITEMS.map((item) => {
118
- const Icon = item.icon
119
- return (
120
- <button
121
- key={item.id}
122
- className={`${styles.mobileNavItem} ${currentView === item.id ? styles.mobileNavActive : ''}`}
123
- onClick={() => handleNavClick(item)}
124
- role="menuitem"
125
- >
126
- <Icon size={16} stroke={1.5} />
127
- <span>{item.label}</span>
128
- </button>
129
- )
130
- })}
131
- <div className={styles.mobileNavDivider} />
132
- {MENU_ITEMS.map((item) => {
133
- const Icon = item.icon
134
- return (
135
- <button
136
- key={item.id}
137
- className={styles.mobileNavItem}
138
- onClick={() => handleMenuClick(item)}
139
- role="menuitem"
140
- >
141
- <Icon size={16} stroke={1.5} />
142
- <span>{item.label}</span>
143
- </button>
144
- )
145
- })}
146
- </div>
147
- )}
148
-
149
- {/* Always-visible primary nav (replaces the old left sidebar) */}
150
- <nav className={styles.nav} aria-label="Primary">
151
- {NAV_ITEMS.map((item) => {
152
- const Icon = item.icon
153
- const isActive = currentView === item.id
154
- return (
155
- <button
156
- key={item.id}
157
- className={`${styles.navBtn} ${isActive ? styles.navBtnActive : ''}`}
158
- onClick={() => handleNavClick(item)}
159
- title={item.comingIn ? `${item.label} — coming in ${item.comingIn}` : item.label}
160
- aria-current={isActive ? 'page' : undefined}
161
- >
162
- <Icon size={14} stroke={1.5} />
163
- <span>{item.label}</span>
164
- {item.comingIn && <span className={styles.comingBadge}>{item.comingIn}</span>}
165
- </button>
166
- )
167
- })}
168
-
169
- {/* Overflow menu (kebab) — hidden features & occasional flows */}
170
- <div className={styles.menuWrap} ref={menuRef}>
171
- <button
172
- className={`${styles.navBtn} ${styles.menuTrigger} ${MENU_ITEMS.some((m) => m.id === currentView) ? styles.navBtnActive : ''}`}
173
- onClick={() => setMenuOpen((o) => !o)}
174
- title="More features"
175
- aria-label="More features"
176
- aria-haspopup="true"
177
- aria-expanded={menuOpen}
178
- >
179
- <IconDots size={16} stroke={1.5} />
180
- </button>
181
- {menuOpen && (
182
- <div className={styles.menuPopover} role="menu">
183
- {MENU_ITEMS.map((item) => {
184
- const Icon = item.icon
185
- const isActive = currentView === item.id
186
- return (
187
- <button
188
- key={item.id}
189
- className={`${styles.menuItem} ${isActive ? styles.menuItemActive : ''}`}
190
- onClick={() => handleMenuClick(item)}
191
- role="menuitem"
192
- aria-current={isActive ? 'page' : undefined}
193
- >
194
- <Icon size={14} stroke={1.5} />
195
- <span>{item.label}</span>
196
- {item.comingIn && <span className={styles.comingBadge}>{item.comingIn}</span>}
197
- </button>
198
- )
199
- })}
200
- </div>
201
- )}
202
- </div>
203
- </nav>
204
- </div>
205
-
206
- <div className={styles.right}>
207
- <button
208
- className={`${styles.navBtn} ${currentView === 'settings' ? styles.navBtnActive : ''}`}
209
- onClick={() => onNavigate('settings')}
210
- title="Settings"
211
- aria-current={currentView === 'settings' ? 'page' : undefined}
212
- style={{ marginRight: '2px' }}
213
- >
214
- <IconSettings size={14} stroke={1.5} />
215
- <span>Settings</span>
216
- </button>
217
-
218
- <ToolPicker
219
- toolMode={toolMode}
220
- onSetToolMode={onSetToolMode}
221
- onCycleToolMode={onCycleToolMode}
222
- />
223
-
224
- {/* ⌘K — the only global keyboard shortcut, opens the command palette */}
225
- <button
226
- className={styles.cmdkBtn}
227
- onClick={onOpenCommandPalette}
228
- title="Command palette (⌘K / Ctrl+P)"
229
- aria-label="Open command palette (⌘K)"
230
- >
231
- <IconCommand size={14} stroke={1.5} />
232
- <span className={styles.cmdkLabel}>⌘K</span>
233
- </button>
234
-
235
- <button
236
- className={`${styles.benchmarkBtn} ${benchmarkRunning ? styles.benchmarkActive : ''}`}
237
- onClick={onBenchmark}
238
- disabled={benchmarkRunning}
239
- title={benchmarkRunning ? `AI Speed Test running — ${benchmarkCompleted}/${benchmarkTotal}` : `Run AI Latency benchmark on ${modelsCount} visible models`}
240
- aria-label={benchmarkRunning ? `AI Speed Test running — ${benchmarkCompleted} of ${benchmarkTotal} complete` : 'AI Latency benchmark'}
241
- >
242
- <IconPlayerPlay size={14} stroke={1.5} />
243
- {benchmarkRunning ? (
244
- <span className={styles.benchmarkRunning}>
245
- <span className={styles.spinner} />
246
- RUN {benchmarkCompleted}/{benchmarkTotal}
247
- </span>
248
- ) : (
249
- <span>AI Latency</span>
250
- )}
251
- </button>
252
-
253
- {/* 📖 M2: update chip slot. Hidden when no update is available. */}
254
- {updateSlot}
255
-
256
- <button className={styles.iconBtn} onClick={onToggleTheme} title={`Theme: ${theme} (click to cycle auto / dark / light)`} aria-label={`Theme: ${theme}. Click to cycle.`}>
257
- {theme === 'light' ? <IconMoon size={16} stroke={1.5} /> : <IconSun size={16} stroke={1.5} />}
258
- </button>
259
- <button className={styles.iconBtn} onClick={onOpenExport} title="Export Data" aria-label="Export model data">
260
- <IconDownload size={16} stroke={1.5} />
261
- </button>
262
- </div>
263
- </header>
264
- )
265
- }