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