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.
- package/README.md +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.86.md +10 -0
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +47 -16
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +35 -75
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +46 -5
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-DWUely84.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- 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
|
-
}
|