free-coding-models 0.5.1 → 0.5.4
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 +31 -5
- package/changelog/v0.5.4.md +24 -0
- package/package.json +5 -2
- package/sources.js +2 -0
- package/src/tui/key-handler.js +1 -1
- package/web/assets/providers/aistudio/aistudio-text.svg +1 -0
- package/web/assets/providers/aistudio/aistudio.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-brand-color.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-brand.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-color.svg +1 -0
- package/web/assets/providers/cerebras/cerebras-text.svg +1 -0
- package/web/assets/providers/cerebras/cerebras.svg +1 -0
- package/web/assets/providers/cloudflare/cloudflare-color.svg +1 -0
- package/web/assets/providers/cloudflare/cloudflare-text.svg +1 -0
- package/web/assets/providers/cloudflare/cloudflare.svg +1 -0
- package/web/assets/providers/gemini/gemini-color.svg +1 -0
- package/web/assets/providers/gemini/gemini-text.svg +1 -0
- package/web/assets/providers/gemini/gemini.svg +1 -0
- package/web/assets/providers/github/github-text.svg +1 -0
- package/web/assets/providers/github/github.svg +1 -0
- package/web/assets/providers/groq/groq-text.svg +1 -0
- package/web/assets/providers/groq/groq.svg +1 -0
- package/web/assets/providers/mistral/mistral-color.svg +1 -0
- package/web/assets/providers/mistral/mistral-text.svg +1 -0
- package/web/assets/providers/mistral/mistral.svg +1 -0
- package/web/assets/providers/nvidia/nvidia-color.svg +1 -0
- package/web/assets/providers/nvidia/nvidia-text.svg +1 -0
- package/web/assets/providers/nvidia/nvidia.svg +1 -0
- package/web/assets/providers/opencode/opencode-text.svg +1 -0
- package/web/assets/providers/opencode/opencode.svg +1 -0
- package/web/assets/providers/openrouter/openrouter (1).svg +1 -0
- package/web/assets/providers/openrouter/openrouter-text.svg +1 -0
- package/web/assets/providers/openrouter/openrouter.svg +1 -0
- package/web/assets/providers/ovhcloud/ovhcloud.svg +12 -0
- package/web/assets/providers/qwen/qwen-color.svg +1 -0
- package/web/assets/providers/qwen/qwen-text.svg +1 -0
- package/web/assets/providers/qwen/qwen.svg +1 -0
- package/web/assets/providers/sambanova/sambanova-color.svg +1 -0
- package/web/assets/providers/sambanova/sambanova-text.svg +1 -0
- package/web/assets/providers/sambanova/sambanova.svg +1 -0
- package/web/assets/providers/scalewaylogo/ScalewayLogo.svg +13 -0
- package/web/assets/providers/zai/zai-text.svg +1 -0
- package/web/assets/providers/zai/zai.svg +1 -0
- package/web/dist/assets/index-BoWmUveV.js +39 -0
- package/web/dist/assets/index-BrpHevg4.css +1 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicons/apple-touch-icon.png +0 -0
- package/web/dist/favicons/browserconfig.xml +12 -0
- package/web/dist/favicons/favicon-16x16.png +0 -0
- package/web/dist/favicons/favicon-192x192.png +0 -0
- package/web/dist/favicons/favicon-32x32.png +0 -0
- package/web/dist/favicons/favicon-48x48.png +0 -0
- package/web/dist/favicons/favicon-512x512.png +0 -0
- package/web/dist/favicons/favicon-96x96.png +0 -0
- package/web/dist/favicons/favicon.ico +0 -0
- package/web/dist/favicons/mstile-150x150.png +0 -0
- package/web/dist/favicons/mstile-310x150.png +0 -0
- package/web/dist/favicons/mstile-310x310.png +0 -0
- package/web/dist/favicons/mstile-512x512.png +0 -0
- package/web/dist/favicons/mstile-70x70.png +0 -0
- package/web/dist/favicons/site.webmanifest +25 -0
- package/web/dist/index.html +42 -3
- package/web/index.html +40 -1
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +12 -0
- 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 +25 -0
- package/web/server.js +71 -1
- package/web/src/App.jsx +209 -89
- package/web/src/components/analytics/AnalyticsView.jsx +30 -2
- package/web/src/components/analytics/AnalyticsView.module.css +4 -0
- package/web/src/components/atoms/AILatencyCell.module.css +9 -1
- package/web/src/components/atoms/HealthCell.jsx +113 -28
- package/web/src/components/atoms/HealthCell.module.css +43 -2
- package/web/src/components/atoms/LastPingCell.module.css +10 -0
- package/web/src/components/atoms/NoKeyIcon.jsx +65 -0
- package/web/src/components/atoms/NoKeyIcon.module.css +30 -0
- package/web/src/components/atoms/ProviderLogo.jsx +188 -0
- package/web/src/components/atoms/ProviderLogo.module.css +103 -0
- package/web/src/components/atoms/Sparkline.jsx +21 -2
- package/web/src/components/atoms/StabilityCell.module.css +5 -0
- package/web/src/components/atoms/StatusDot.module.css +7 -0
- package/web/src/components/atoms/TPSCell.module.css +7 -1
- package/web/src/components/atoms/TierBadge.module.css +11 -0
- package/web/src/components/atoms/Toast.module.css +6 -0
- package/web/src/components/atoms/VerdictBadge.module.css +12 -0
- package/web/src/components/dashboard/DetailPanel.jsx +63 -2
- package/web/src/components/dashboard/DetailPanel.module.css +130 -0
- package/web/src/components/dashboard/FilterBar.jsx +146 -53
- package/web/src/components/dashboard/FilterBar.module.css +138 -58
- package/web/src/components/dashboard/ModelTable.jsx +235 -30
- package/web/src/components/dashboard/ModelTable.module.css +205 -16
- package/web/src/components/layout/Header.jsx +140 -12
- package/web/src/components/layout/Header.module.css +191 -27
- package/web/src/components/palette/CommandPalette.jsx +146 -0
- package/web/src/components/palette/CommandPalette.module.css +121 -0
- package/web/src/global.css +52 -15
- package/web/src/hooks/useColumnSizing.js +211 -0
- package/web/src/hooks/useFavorites.js +150 -0
- package/web/src/hooks/useFilter.js +157 -24
- package/web/src/hooks/useTheme.js +72 -16
- package/web/src/hooks/useUrlState.js +93 -0
- package/web/dist/assets/index-ByGf4Kq-.js +0 -14
- package/web/dist/assets/index-Ds7wmHBv.css +0 -1
- package/web/src/components/dashboard/StatsBar.jsx +0 -42
- package/web/src/components/dashboard/StatsBar.module.css +0 -28
- package/web/src/components/layout/Sidebar.jsx +0 -50
- package/web/src/components/layout/Sidebar.module.css +0 -86
- package/web/src/components/map/MapView.jsx +0 -17
- package/web/src/components/map/MapView.module.css +0 -25
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/HealthCell.module.css
|
|
3
|
+
* @description Styles for the HealthCell atom — matches CLI Health column typography.
|
|
4
|
+
* 📖 "NO KEY" is rendered as a louder, bold, warning-yellow chip with the NoKeyIcon
|
|
5
|
+
* 📖 sitting inline next to the label, so a missing credential jumps out at the user.
|
|
6
|
+
*/
|
|
1
7
|
.cell {
|
|
2
8
|
font-family: var(--font-mono);
|
|
3
9
|
font-size: 11px;
|
|
4
10
|
font-weight: 600;
|
|
5
|
-
display: inline-
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: 5px;
|
|
6
14
|
letter-spacing: 0.2px;
|
|
7
15
|
white-space: nowrap;
|
|
8
16
|
overflow: hidden;
|
|
@@ -12,4 +20,37 @@
|
|
|
12
20
|
.warning { color: #ffaa00; }
|
|
13
21
|
.error { color: #ff4444; }
|
|
14
22
|
.errorBold { color: #ff4444; font-weight: 800; }
|
|
15
|
-
.dim { color: var(--color-text-dim); }
|
|
23
|
+
.dim { color: var(--color-text-dim); }
|
|
24
|
+
|
|
25
|
+
/* 📖 The "NO KEY" treatment is intentionally louder than the other states:
|
|
26
|
+
📖 - yellow chip background instead of plain text
|
|
27
|
+
📖 - 13px font (vs 11px elsewhere in the column)
|
|
28
|
+
📖 - bold weight, slightly more letter-spacing
|
|
29
|
+
📖 - dark text on yellow for AA contrast in dark + light themes */
|
|
30
|
+
.noKey {
|
|
31
|
+
background: rgba(255, 170, 0, 0.18);
|
|
32
|
+
color: #ffaa00;
|
|
33
|
+
border: 1px solid rgba(255, 170, 0, 0.45);
|
|
34
|
+
padding: 1px 7px;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
font-weight: 700;
|
|
38
|
+
letter-spacing: 0.4px;
|
|
39
|
+
}
|
|
40
|
+
.noKeyText {
|
|
41
|
+
/* 📖 Inherits the surrounding font, just bumps the weight and tracking for the
|
|
42
|
+
📖 label so the chip reads as a single badge even at narrow column widths. */
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
letter-spacing: 0.4px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:global([data-theme="light"]) .noKey {
|
|
48
|
+
background: rgba(255, 170, 0, 0.25);
|
|
49
|
+
border-color: rgba(255, 170, 0, 0.55);
|
|
50
|
+
color: #b86b00;
|
|
51
|
+
}
|
|
52
|
+
:global([data-theme="light"]) .up { color: #008f4d; }
|
|
53
|
+
:global([data-theme="light"]) .warning { color: #b86b00; }
|
|
54
|
+
:global([data-theme="light"]) .error,
|
|
55
|
+
:global([data-theme="light"]) .errorBold { color: #c02323; }
|
|
56
|
+
:global([data-theme="light"]) .dim { color: #888; }
|
|
@@ -32,4 +32,14 @@
|
|
|
32
32
|
border-radius: 50%;
|
|
33
33
|
animation: pingRing 0.8s linear infinite;
|
|
34
34
|
flex-shrink: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* 📖 Light theme: the neon green ring is unreadable on white, swap to a deep
|
|
38
|
+
📖 green ring on a soft neutral track so the spinner stays visible. */
|
|
39
|
+
:global([data-theme="light"]) .fast { color: #008f4d; }
|
|
40
|
+
:global([data-theme="light"]) .medium { color: #b86b00; }
|
|
41
|
+
:global([data-theme="light"]) .slow { color: #c8143a; }
|
|
42
|
+
:global([data-theme="light"]) .spinner {
|
|
43
|
+
border-color: rgba(0, 0, 0, 0.12);
|
|
44
|
+
border-top-color: #008f4d;
|
|
35
45
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/NoKeyIcon.jsx
|
|
3
|
+
* @description Compact "no API key" indicator — yellow key with a red diagonal strikethrough.
|
|
4
|
+
*
|
|
5
|
+
* 📖 Used in two places to surface the missing-key state without duplicating the
|
|
6
|
+
* 📖 verbose "NO KEY" pill that used to live next to the model label:
|
|
7
|
+
* 📖 1. Inline next to the model name in the Model cell (replaces the old orange pill)
|
|
8
|
+
* 📖 2. Inside the Health cell, sitting next to the "NO KEY" text label
|
|
9
|
+
*
|
|
10
|
+
* 📖 The strikethrough is rendered as an inline SVG `<line>` on top of the Tabler
|
|
11
|
+
* 📖 `IconKey`. Doing it in SVG (instead of CSS) keeps the line aligned with the
|
|
12
|
+
* 📖 icon at any size and avoids the CSS overflow quirks that come with rotating
|
|
13
|
+
* 📖 a pseudo-element inside a flex parent.
|
|
14
|
+
*
|
|
15
|
+
* 📖 `size` defaults to 14px which is the same as the Tabler icons used in Settings;
|
|
16
|
+
* 📖 pass a smaller value (e.g. 11) for inline use next to model names where space is tight.
|
|
17
|
+
*
|
|
18
|
+
* @functions
|
|
19
|
+
* → NoKeyIcon — renders a yellow key with a red strikethrough
|
|
20
|
+
* @exports NoKeyIcon
|
|
21
|
+
*/
|
|
22
|
+
import { IconKey } from '@tabler/icons-react'
|
|
23
|
+
import styles from './NoKeyIcon.module.css'
|
|
24
|
+
|
|
25
|
+
export default function NoKeyIcon({ size = 14, title = 'No API key configured' }) {
|
|
26
|
+
// 📖 Stroke width follows the rest of the dashboard's icon style (1.5).
|
|
27
|
+
// 📖 The key is yellow (warning palette); the slash is a bright red so it reads
|
|
28
|
+
// 📖 as a "blocked" state at a glance even in peripheral vision.
|
|
29
|
+
const strokeWidth = 1.5
|
|
30
|
+
const slashPadding = Math.max(1, Math.round(size * 0.07))
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
className={styles.wrapper}
|
|
35
|
+
style={{ width: `${size}px`, height: `${size}px` }}
|
|
36
|
+
role="img"
|
|
37
|
+
aria-label={title}
|
|
38
|
+
title={title}
|
|
39
|
+
>
|
|
40
|
+
<IconKey
|
|
41
|
+
size={size}
|
|
42
|
+
stroke={strokeWidth}
|
|
43
|
+
className={styles.key}
|
|
44
|
+
/>
|
|
45
|
+
<svg
|
|
46
|
+
className={styles.slash}
|
|
47
|
+
width={size + slashPadding * 2}
|
|
48
|
+
height={size + slashPadding * 2}
|
|
49
|
+
viewBox={`0 0 ${size + slashPadding * 2} ${size + slashPadding * 2}`}
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
focusable="false"
|
|
52
|
+
>
|
|
53
|
+
<line
|
|
54
|
+
x1={slashPadding + 1}
|
|
55
|
+
y1={slashPadding + 1}
|
|
56
|
+
x2={slashPadding + size - 1}
|
|
57
|
+
y2={slashPadding + size - 1}
|
|
58
|
+
stroke="var(--color-danger, #ff4444)"
|
|
59
|
+
strokeWidth="1.1"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
</span>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/NoKeyIcon.module.css
|
|
3
|
+
* @description Styles for the NoKeyIcon atom — yellow key with a red diagonal slash.
|
|
4
|
+
* 📖 The wrapper is a fixed-size box so the absolutely-positioned slash SVG can
|
|
5
|
+
* 📖 center itself over the icon regardless of the parent's flex alignment.
|
|
6
|
+
*/
|
|
7
|
+
.wrapper {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
position: relative;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
line-height: 0;
|
|
14
|
+
vertical-align: middle;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.key {
|
|
18
|
+
color: #ffaa00; /* 📖 warning yellow — same family as the rest of the dashboard */
|
|
19
|
+
position: relative;
|
|
20
|
+
z-index: 1;
|
|
21
|
+
filter: drop-shadow(0 0 1px rgba(255, 170, 0, 0.35));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.slash {
|
|
25
|
+
position: absolute;
|
|
26
|
+
inset: 0;
|
|
27
|
+
margin: auto;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
z-index: 2;
|
|
30
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/ProviderLogo.jsx
|
|
3
|
+
* @description Renders a provider cell as [icon + text-wordmark] on a single line.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* - Looks up the provider's assets under `web/assets/providers/<folder>/`.
|
|
7
|
+
* - Prefers the **color** icon when available (recognizable + lively in both themes);
|
|
8
|
+
* falls back to the **mono** (currentColor) icon when no color variant exists.
|
|
9
|
+
* - Renders the **text** wordmark (`<id>-text.svg`) next to the icon when available.
|
|
10
|
+
* Text wordmarks use `fill="currentColor"`, so they automatically adapt to the
|
|
11
|
+
* active theme (white in dark mode, black in light mode) without any JS.
|
|
12
|
+
* - For providers without a text wordmark (legacy OVH / Scaleway SVGs, or models
|
|
13
|
+
* like Codestral that share a parent's brand), shows the graphic icon + the
|
|
14
|
+
* human-readable caption as plain text.
|
|
15
|
+
* - Inlines the SVGs as raw strings via `import.meta.glob(..., { as: 'raw' })` so
|
|
16
|
+
* `currentColor` resolves against the cell's text color. Using `<img src>` would
|
|
17
|
+
* sandbox the SVG and break the theme inheritance.
|
|
18
|
+
* - Constrained to a single row height (~22px) via `--pl-max-h` so the cell never
|
|
19
|
+
* grows taller than the rest of the table, regardless of which variant is shown.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} props
|
|
22
|
+
* @param {string} props.providerKey - The kebab-case key from sources.js (e.g. 'nvidia', 'github-models', 'opencode-zen', 'googleai').
|
|
23
|
+
* @param {string} props.origin - Human-readable name from sources.js, used as fallback text / tooltip.
|
|
24
|
+
* @functions
|
|
25
|
+
* → ProviderLogo (default export)
|
|
26
|
+
* → getSvg (internal) – raw-string lookup keyed by asset path
|
|
27
|
+
* @see web/src/components/dashboard/ModelTable.jsx (consumer)
|
|
28
|
+
* @see web/assets/providers/ (asset source)
|
|
29
|
+
* @see https://lobehub.com/icons (variant inventory source)
|
|
30
|
+
*/
|
|
31
|
+
import styles from './ProviderLogo.module.css'
|
|
32
|
+
|
|
33
|
+
// 📖 Eager raw import of every provider SVG. The key is a relative path from this
|
|
34
|
+
// 📖 file; the value is the SVG markup as a string. Vite bundles it at build time.
|
|
35
|
+
// 📖 Path resolves to `web/assets/providers/**/*.svg` (3 levels up from atoms/).
|
|
36
|
+
const svgModules = import.meta.glob('../../../assets/providers/**/*.svg', {
|
|
37
|
+
query: '?raw',
|
|
38
|
+
import: 'default',
|
|
39
|
+
eager: true,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 📖 Provider key → asset folder + variant availability.
|
|
43
|
+
// 📖 `folder` MUST match the directory name under web/assets/providers/.
|
|
44
|
+
// 📖 `hasColor` / `hasText` are derived from the lobe-icons static-svg inventory
|
|
45
|
+
// 📖 (see https://lobehub.com/icons). `legacy` marks providers that ship with a
|
|
46
|
+
// 📖 single graphic SVG and no wordmark — for those we render icon + caption.
|
|
47
|
+
// 📖 Keys MUST match the `providerKey` field sent by the backend (kebab-case for
|
|
48
|
+
// 📖 compound names like `github-models` / `opencode-zen` — see sources.js).
|
|
49
|
+
const PROVIDER_CONFIG = {
|
|
50
|
+
nvidia: { folder: 'nvidia', hasColor: true, hasText: true },
|
|
51
|
+
groq: { folder: 'groq', hasColor: false, hasText: true },
|
|
52
|
+
cerebras: { folder: 'cerebras', hasColor: true, hasText: true },
|
|
53
|
+
googleai: { folder: 'aistudio', hasColor: false, hasText: true },
|
|
54
|
+
'github-models': { folder: 'github', hasColor: false, hasText: true },
|
|
55
|
+
mistral: { folder: 'mistral', hasColor: true, hasText: true },
|
|
56
|
+
cloudflare: { folder: 'cloudflare', hasColor: true, hasText: true },
|
|
57
|
+
openrouter: { folder: 'openrouter', hasColor: false, hasText: true },
|
|
58
|
+
sambanova: { folder: 'sambanova', hasColor: true, hasText: true },
|
|
59
|
+
// 📖 OVH's logo is itself a full wordmark ("OVHcloud") — adding the origin
|
|
60
|
+
// 📖 name "OVHcloud AI" as a caption would be redundant. `noCaption` skips
|
|
61
|
+
// 📖 the text label and lets the wordmark stand on its own. The user pointed
|
|
62
|
+
// 📖 us to https://www.ovhcloud.com/.../OVHcloud_logo.svg#ocmscloud_logo for
|
|
63
|
+
// 📖 the international wordmark; we extract the inner <symbol> and save it as
|
|
64
|
+
// 📖 a renderable <svg> with the same viewBox.
|
|
65
|
+
ovhcloud: { folder: 'ovhcloud', hasColor: false, hasText: false, legacy: true, noCaption: true },
|
|
66
|
+
// 📖 Scaleway's pre-existing asset is `ScalewayLogo.svg` (capitalized) inside
|
|
67
|
+
// 📖 `scalewaylogo/`. The default filename lookup would look for
|
|
68
|
+
// 📖 `scalewaylogo/scalewaylogo.svg` which doesn't exist, so `iconFile`
|
|
69
|
+
// 📖 overrides it. The wordmark IS the brand name ("Scaleway") so adding
|
|
70
|
+
// 📖 the origin name as a separate caption is redundant — `noCaption` skips it.
|
|
71
|
+
// 📖 `iconClass: 'scalewayLogo'` attaches a global class so CSS can swap to
|
|
72
|
+
// 📖 a lighter purple in dark mode (the brand #521094 is barely visible on
|
|
73
|
+
// 📖 the near-black background). SVG paths were rewritten to use
|
|
74
|
+
// 📖 `fill="currentColor"` for this CSS control.
|
|
75
|
+
scaleway: { folder: 'scalewaylogo', iconFile: 'ScalewayLogo', iconClass: 'scalewayLogo', hasColor: false, hasText: false, legacy: true, noCaption: true },
|
|
76
|
+
zai: { folder: 'zai', hasColor: false, hasText: true },
|
|
77
|
+
qwen: { folder: 'qwen', hasColor: true, hasText: true },
|
|
78
|
+
gemini: { folder: 'gemini', hasColor: true, hasText: true },
|
|
79
|
+
'opencode-zen': { folder: 'opencode', hasColor: false, hasText: true },
|
|
80
|
+
// 📖 Codestral intentionally reuses Mistral's branding — the colored Mistral
|
|
81
|
+
// 📖 icon (so the brand association is visible) + the literal caption
|
|
82
|
+
// 📖 "Codestral" as plain text. `iconFile` overrides the default filename
|
|
83
|
+
// 📖 lookup (we want mistral-color, not codestral-color which doesn't exist),
|
|
84
|
+
// 📖 and `caption` overrides the origin so we don't show "Codestral" twice.
|
|
85
|
+
codestral: { folder: 'mistral', iconFile: 'mistral-color', hasColor: true, hasText: false, legacy: true, caption: 'Codestral' },
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getSvg(folder, filename) {
|
|
89
|
+
if (!folder || !filename) return null
|
|
90
|
+
return svgModules[`../../../assets/providers/${folder}/${filename}.svg`] || null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default function ProviderLogo({ providerKey, origin }) {
|
|
94
|
+
const cfg = PROVIDER_CONFIG[providerKey]
|
|
95
|
+
|
|
96
|
+
// 📖 Unknown / unmapped provider: fall back to plain text. Same as the old
|
|
97
|
+
// 📖 `.providerPill` so we don't visually regress on rows we couldn't classify.
|
|
98
|
+
if (!cfg) {
|
|
99
|
+
return <span className={styles.fallback}>{origin}</span>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 📖 Pick the best icon: prefer the colorful variant, fall back to mono.
|
|
103
|
+
// 📖 Mono uses currentColor → inherits the cell's text color → theme-adaptive.
|
|
104
|
+
// 📖 `cfg.iconFile` overrides the default filename so providers like Codestral
|
|
105
|
+
// 📖 can borrow another provider's icon (e.g. mistral-color for brand parity).
|
|
106
|
+
const iconSvg =
|
|
107
|
+
(cfg.iconFile && getSvg(cfg.folder, cfg.iconFile)) ||
|
|
108
|
+
(cfg.hasColor && getSvg(cfg.folder, `${cfg.folder}-color`)) ||
|
|
109
|
+
getSvg(cfg.folder, cfg.folder)
|
|
110
|
+
|
|
111
|
+
// 📖 Pick the text wordmark (currentColor-based, theme-adaptive).
|
|
112
|
+
const textSvg = cfg.hasText ? getSvg(cfg.folder, `${cfg.folder}-text`) : null
|
|
113
|
+
|
|
114
|
+
// ─── Layouts ─────────────────────────────────────────────────────────────
|
|
115
|
+
// 📖 Single horizontal row: [icon | gap | wordmark], all in --pl-max-h tall.
|
|
116
|
+
// 📖 `overflow: hidden` on the row is a safety net for the rare case where
|
|
117
|
+
// 📖 a particularly wide wordmark (e.g. cloudflare ~145px) still overflows
|
|
118
|
+
// 📖 the column even at 12px height — better clipped than warping the table.
|
|
119
|
+
if (iconSvg && textSvg) {
|
|
120
|
+
return (
|
|
121
|
+
<span className={styles.row} title={origin}>
|
|
122
|
+
<span
|
|
123
|
+
className={styles.icon}
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
dangerouslySetInnerHTML={{ __html: iconSvg }}
|
|
126
|
+
/>
|
|
127
|
+
<span
|
|
128
|
+
className={styles.textLogo}
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
dangerouslySetInnerHTML={{ __html: textSvg }}
|
|
131
|
+
/>
|
|
132
|
+
</span>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 📖 Legacy providers (Scaleway, OVH by default): graphic only, no wordmark SVG.
|
|
137
|
+
// 📖 Show the graphic + a caption as plain text so the cell stays scannable
|
|
138
|
+
// 📖 in the table. `cfg.caption` overrides the origin name (Codestral wants
|
|
139
|
+
// 📖 its own label, not whatever sources.js returns). `cfg.noCaption` skips
|
|
140
|
+
// 📖 the caption entirely (OVH's graphic is itself a full wordmark, and
|
|
141
|
+
// 📖 Scaleway's wordmark already contains the brand name).
|
|
142
|
+
// 📖 `cfg.iconClass` (when set) is appended to the icon span — used to attach
|
|
143
|
+
// 📖 a global class for per-theme color overrides (e.g. Scaleway's lighter
|
|
144
|
+
// 📖 purple in dark mode).
|
|
145
|
+
if (iconSvg) {
|
|
146
|
+
const captionText = cfg.caption ?? origin
|
|
147
|
+
if (cfg.noCaption) {
|
|
148
|
+
const wordmarkClass = cfg.iconClass
|
|
149
|
+
? `${styles.iconLegacyWordmark} ${cfg.iconClass}`
|
|
150
|
+
: styles.iconLegacyWordmark
|
|
151
|
+
return (
|
|
152
|
+
<span className={styles.row} title={origin}>
|
|
153
|
+
<span
|
|
154
|
+
className={wordmarkClass}
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
dangerouslySetInnerHTML={{ __html: iconSvg }}
|
|
157
|
+
/>
|
|
158
|
+
</span>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return (
|
|
162
|
+
<span className={styles.row} title={origin}>
|
|
163
|
+
<span
|
|
164
|
+
className={styles.icon}
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
dangerouslySetInnerHTML={{ __html: iconSvg }}
|
|
167
|
+
/>
|
|
168
|
+
<span className={styles.fallback}>{captionText}</span>
|
|
169
|
+
</span>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 📖 Icon-only fallback (shouldn't happen with current assets, but safe).
|
|
174
|
+
if (textSvg) {
|
|
175
|
+
return (
|
|
176
|
+
<span className={styles.row} title={origin}>
|
|
177
|
+
<span
|
|
178
|
+
className={styles.textLogo}
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
dangerouslySetInnerHTML={{ __html: textSvg }}
|
|
181
|
+
/>
|
|
182
|
+
</span>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 📖 No assets at all (e.g. codestral without icon). Plain text fallback.
|
|
187
|
+
return <span className={styles.fallback}>{origin}</span>
|
|
188
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/ProviderLogo.module.css
|
|
3
|
+
* @description Styles for the inline [icon + wordmark] provider cell.
|
|
4
|
+
*
|
|
5
|
+
* - Single horizontal row, max-height pinned so the cell never exceeds the
|
|
6
|
+
* table's row height (matches the 7px vertical padding on .td).
|
|
7
|
+
* - `color: var(--color-text)` propagates the active theme's text color into
|
|
8
|
+
* inlined SVGs that use `fill="currentColor"` (mono icons + all wordmarks).
|
|
9
|
+
* - Sized to fit within a ~150px column at default row height. Cloudflare
|
|
10
|
+
* is the widest wordmark (~145px at 12px high) — it fits but is tight.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.row {
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 6px;
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
max-height: var(--pl-max-h, 22px);
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
/* 📖 Inherited by the inlined SVGs that use `fill="currentColor"`. Set on
|
|
21
|
+
📖 the row so both the mono icon and the wordmark pick up the theme. */
|
|
22
|
+
color: var(--color-text);
|
|
23
|
+
line-height: 1;
|
|
24
|
+
vertical-align: middle;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.icon {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
height: var(--pl-icon-h, 16px);
|
|
32
|
+
width: auto;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
}
|
|
35
|
+
/* 📖 The inlined <svg> from lobe-icons ships with `width="1em" height="1em"`.
|
|
36
|
+
📖 We override both so the wrapper's height controls the rendered size, and
|
|
37
|
+
📖 let the viewBox dictate the width (preserves each icon's aspect ratio). */
|
|
38
|
+
.icon svg {
|
|
39
|
+
height: 100%;
|
|
40
|
+
width: auto;
|
|
41
|
+
display: block;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.textLogo {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
height: var(--pl-text-h, 12px);
|
|
49
|
+
width: auto;
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
}
|
|
52
|
+
.textLogo svg {
|
|
53
|
+
height: 100%;
|
|
54
|
+
width: auto;
|
|
55
|
+
display: block;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* 📖 Wordmark-only variant used by OVH (and any future provider whose "icon"
|
|
59
|
+
📖 asset is itself a full horizontal wordmark). Sized to a slightly tighter
|
|
60
|
+
📖 max height than the square icons so the wordmark doesn't visually crowd
|
|
61
|
+
📖 the row. The viewBox aspect ratio of the OVH wordmark is ~6.3:1, so at
|
|
62
|
+
📖 14px high it renders ~88px wide — fits comfortably in 150px. */
|
|
63
|
+
.iconLegacyWordmark {
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
height: var(--pl-icon-h, 14px);
|
|
68
|
+
width: auto;
|
|
69
|
+
flex-shrink: 0;
|
|
70
|
+
/* 📖 Default text color is inherited from the row. Wordmarks that use
|
|
71
|
+
📖 `fill="currentColor"` (OVH) follow the theme automatically. Wordmarks
|
|
72
|
+
📖 with hardcoded fills (Scaleway) need a per-theme override — see
|
|
73
|
+
📖 `.scalewayLogo` below. */
|
|
74
|
+
color: var(--color-text);
|
|
75
|
+
}
|
|
76
|
+
.iconLegacyWordmark svg {
|
|
77
|
+
height: 100%;
|
|
78
|
+
width: auto;
|
|
79
|
+
display: block;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* 📖 Scaleway: brand purple #521094 is the official color. It looks great on
|
|
83
|
+
📖 white but is barely visible on the dark background — the contrast is
|
|
84
|
+
📖 too low (dark purple on near-black). Bump to a lighter, more saturated
|
|
85
|
+
📖 shade in dark mode so the wordmark reads cleanly without losing brand
|
|
86
|
+
📖 identity. Paths were rewritten to use `fill="currentColor"` so this
|
|
87
|
+
📖 single CSS color property controls the whole wordmark. */
|
|
88
|
+
:global(.scalewayLogo) {
|
|
89
|
+
color: #521094;
|
|
90
|
+
transition: color 200ms var(--ease-out, ease);
|
|
91
|
+
}
|
|
92
|
+
:global([data-theme="dark"]) :global(.scalewayLogo) {
|
|
93
|
+
color: #a78bfa;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.fallback {
|
|
97
|
+
font-size: 11px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
color: var(--color-text);
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
/* 📖 Slight tracking so the origin name reads cleanly next to the icon. */
|
|
102
|
+
letter-spacing: 0.2px;
|
|
103
|
+
}
|
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
* @file web/src/components/atoms/Sparkline.jsx
|
|
3
3
|
* @description SVG sparkline chart for ping history trend visualization.
|
|
4
4
|
* 📖 Renders a polyline with gradient area fill and endpoint dot.
|
|
5
|
+
* 📖 Colors are theme-aware: reads CSS custom properties from :root so the
|
|
6
|
+
* 📖 stroke/dot swap to the AA-contrast palette in light mode automatically.
|
|
5
7
|
*/
|
|
6
8
|
import { useMemo } from 'react'
|
|
7
9
|
|
|
10
|
+
// 📖 Read CSS custom properties at module scope; falls back to dark-mode neon
|
|
11
|
+
// 📖 values when running outside the browser (e.g. during server rendering).
|
|
12
|
+
function readCssVar(name, fallback) {
|
|
13
|
+
if (typeof window === 'undefined' || !window.document?.documentElement) return fallback
|
|
14
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
|
15
|
+
return v || fallback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 📖 Threshold-based color picker that returns the current theme's variant of
|
|
19
|
+
// 📖 each color. Reading the variables on every render is fine — they only
|
|
20
|
+
// 📖 change when the user toggles the theme, and the memo cache key catches it.
|
|
21
|
+
function pickSparkColor(lastVal) {
|
|
22
|
+
if (lastVal < 500) return readCssVar('--color-success', '#00ff88')
|
|
23
|
+
if (lastVal < 1500) return readCssVar('--color-warning', '#ffaa00')
|
|
24
|
+
return readCssVar('--color-danger', '#ff4444')
|
|
25
|
+
}
|
|
26
|
+
|
|
8
27
|
export default function Sparkline({ history }) {
|
|
9
28
|
const svg = useMemo(() => {
|
|
10
29
|
if (!history || history.length < 2) return null
|
|
@@ -28,13 +47,13 @@ export default function Sparkline({ history }) {
|
|
|
28
47
|
.join(' ')
|
|
29
48
|
|
|
30
49
|
const lastVal = values[values.length - 1]
|
|
31
|
-
const color = lastVal
|
|
50
|
+
const color = pickSparkColor(lastVal)
|
|
32
51
|
const lastX = ((values.length - 1) * step).toFixed(1)
|
|
33
52
|
const lastY = (h - ((lastVal - min) / range) * (h - 4) - 2).toFixed(1)
|
|
34
53
|
|
|
35
54
|
return (
|
|
36
55
|
<svg className="sparkline-svg" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
|
37
|
-
<polyline fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" points={points} opacity="0.
|
|
56
|
+
<polyline fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" points={points} opacity="0.85" />
|
|
38
57
|
<circle cx={lastX} cy={lastY} r="2.5" fill={color} />
|
|
39
58
|
</svg>
|
|
40
59
|
)
|
|
@@ -6,3 +6,8 @@
|
|
|
6
6
|
.low { background: linear-gradient(90deg, #ff4444, #cc0000); }
|
|
7
7
|
.value { font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 600; }
|
|
8
8
|
.none { color: var(--color-text-dim); }
|
|
9
|
+
|
|
10
|
+
/* 📖 Light theme: drop the neon gradients to deep, saturated versions. */
|
|
11
|
+
:global([data-theme="light"]) .high { background: linear-gradient(90deg, #008f4d, #4a7a00); }
|
|
12
|
+
:global([data-theme="light"]) .mid { background: linear-gradient(90deg, #b86b00, #c45a00); }
|
|
13
|
+
:global([data-theme="light"]) .low { background: linear-gradient(90deg, #c8143a, #990000); }
|
|
@@ -15,3 +15,10 @@
|
|
|
15
15
|
0%, 100% { box-shadow: 0 0 0 0 rgba(118,185,0,0.25); }
|
|
16
16
|
50% { box-shadow: 0 0 0 6px transparent; }
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/* 📖 Light theme: deep green/red for the dot so it still reads as "up"/"down"
|
|
20
|
+
📖 on a white card; the glow halo is dimmed so it doesn't blow out. */
|
|
21
|
+
:global([data-theme="light"]) .up { background: #008f4d; box-shadow: 0 0 4px rgba(0,143,77,0.35); }
|
|
22
|
+
:global([data-theme="light"]) .down,
|
|
23
|
+
:global([data-theme="light"]) .timeout { background: #c8143a; }
|
|
24
|
+
:global([data-theme="light"]) .pending { background: #888899; }
|
|
@@ -35,4 +35,10 @@
|
|
|
35
35
|
border-radius: 50%;
|
|
36
36
|
animation: miniSpin 0.6s linear infinite;
|
|
37
37
|
flex-shrink: 0;
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* 📖 Light theme overrides — match AILatencyCell. */
|
|
41
|
+
:global([data-theme="light"]) .fast { color: #008f4d; }
|
|
42
|
+
:global([data-theme="light"]) .slow { color: #c8143a; }
|
|
43
|
+
:global([data-theme="light"]) .badge { color: #0e7490; }
|
|
44
|
+
:global([data-theme="light"]) .runningCell { background: rgba(125, 0, 180, 0.08); }
|
|
@@ -16,3 +16,14 @@
|
|
|
16
16
|
.tier_bplus { background: rgba(168,168,200,0.1); color: #a8a8c8; border: 1px solid rgba(168,168,200,0.15); }
|
|
17
17
|
.tier_b { background: rgba(128,128,152,0.1); color: #808098; border: 1px solid rgba(128,128,152,0.15); }
|
|
18
18
|
.tier_c { background: rgba(96,96,120,0.1); color: #606078; border: 1px solid rgba(96,96,120,0.15); }
|
|
19
|
+
|
|
20
|
+
/* 📖 Light theme: darker, AA-contrast tier text colors. Borders stay the same
|
|
21
|
+
📖 hue so the badge still feels "tier-colored", just more legible. */
|
|
22
|
+
:global([data-theme="light"]) .tier_splus { color: #7a5e00; border-color: rgba(176,136,0,0.4); }
|
|
23
|
+
:global([data-theme="light"]) .tier_s { color: #a14a00; border-color: rgba(196,90,0,0.4); }
|
|
24
|
+
:global([data-theme="light"]) .tier_aplus { color: #005d8a; border-color: rgba(0,115,168,0.4); }
|
|
25
|
+
:global([data-theme="light"]) .tier_a { color: #1f7a3e; border-color: rgba(31,122,62,0.4); }
|
|
26
|
+
:global([data-theme="light"]) .tier_aminus { color: #2f7a2f; border-color: rgba(47,122,47,0.4); }
|
|
27
|
+
:global([data-theme="light"]) .tier_bplus { color: #4a4a72; border-color: rgba(90,90,138,0.4); }
|
|
28
|
+
:global([data-theme="light"]) .tier_b { color: #555566; border-color: rgba(85,85,102,0.4); }
|
|
29
|
+
:global([data-theme="light"]) .tier_c { color: #5a5a6a; border-color: rgba(106,106,122,0.4); }
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
.error { border-left: 4px solid #ff4444; }
|
|
19
19
|
.warning { border-left: 4px solid #ffaa00; }
|
|
20
20
|
.info { border-left: 4px solid #06b6d4; }
|
|
21
|
+
|
|
22
|
+
/* 📖 Light theme: deeper accent bars so they pop on the white toast card. */
|
|
23
|
+
:global([data-theme="light"]) .success { border-left-color: #008f4d; }
|
|
24
|
+
:global([data-theme="light"]) .error { border-left-color: #c8143a; }
|
|
25
|
+
:global([data-theme="light"]) .warning { border-left-color: #b86b00; }
|
|
26
|
+
:global([data-theme="light"]) .info { border-left-color: #0e7490; }
|
|
21
27
|
.icon { font-size: 18px; flex-shrink: 0; }
|
|
22
28
|
.message { flex: 1; }
|
|
23
29
|
.close {
|
|
@@ -26,3 +26,15 @@
|
|
|
26
26
|
.unstable { background: rgba(204,0,0,0.12); color: #cc0000; }
|
|
27
27
|
.notactive { background: rgba(85,85,112,0.12); color: #555570; }
|
|
28
28
|
.pending { background: rgba(68,68,96,0.1); color: #444460; }
|
|
29
|
+
|
|
30
|
+
/* 📖 Light theme: deeper, more saturated text colors. Backgrounds stay
|
|
31
|
+
📖 the same since they are translucent and adapt to the white card. */
|
|
32
|
+
:global([data-theme="light"]) .perfect { color: #008f4d; background: rgba(0,143,77,0.10); }
|
|
33
|
+
:global([data-theme="light"]) .normal { color: #4a7a00; background: rgba(74,122,0,0.10); }
|
|
34
|
+
:global([data-theme="light"]) .slow { color: #b86b00; background: rgba(184,107,0,0.10); }
|
|
35
|
+
:global([data-theme="light"]) .spiky { color: #c45000; background: rgba(196,80,0,0.10); }
|
|
36
|
+
:global([data-theme="light"]) .veryslow { color: #c8143a; background: rgba(200,20,58,0.10); }
|
|
37
|
+
:global([data-theme="light"]) .overloaded { color: #b8101f; background: rgba(184,16,31,0.10); }
|
|
38
|
+
:global([data-theme="light"]) .unstable { color: #990000; background: rgba(153,0,0,0.10); }
|
|
39
|
+
:global([data-theme="light"]) .notactive { color: #5a5a72; }
|
|
40
|
+
:global([data-theme="light"]) .pending { color: #5a5a8a; }
|