free-coding-models 0.5.86 → 0.5.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +109 -888
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/changelog/v0.5.88.md +16 -0
  5. package/package.json +13 -5
  6. package/sources.js +4 -4
  7. package/src/core/cache.js +3 -1
  8. package/src/core/config.js +43 -5
  9. package/src/core/endpoint-installer.js +23 -12
  10. package/src/core/installed-models-manager.js +43 -20
  11. package/src/core/kilo-config.js +51 -5
  12. package/src/core/kilo.js +5 -3
  13. package/src/core/legacy-proxy-cleanup.js +22 -5
  14. package/src/core/model-merger.js +4 -0
  15. package/src/core/models-drift.js +9 -0
  16. package/src/core/opencode-config.js +57 -5
  17. package/src/core/opencode.js +67 -41
  18. package/src/core/playground.js +7 -0
  19. package/src/core/probe-cache.js +35 -4
  20. package/src/core/provider-key-tester.js +10 -2
  21. package/src/core/provider-metadata.js +10 -0
  22. package/src/core/provider-quota-fetchers.js +14 -7
  23. package/src/core/router-daemon.js +208 -26
  24. package/src/core/runtime-telemetry.js +27 -1
  25. package/src/core/security.js +170 -24
  26. package/src/core/shared-helpers.js +12 -1
  27. package/src/core/shell-env.js +3 -1
  28. package/src/core/telemetry.js +18 -5
  29. package/src/core/tool-launchers.js +9 -6
  30. package/src/core/updater.js +28 -2
  31. package/src/core/utils.js +173 -71
  32. package/src/tui/app.js +38 -14
  33. package/src/tui/command-palette.js +22 -2
  34. package/src/tui/key-handler.js +123 -179
  35. package/src/tui/overlays.js +21 -71
  36. package/src/tui/render-helpers.js +62 -4
  37. package/src/tui/render-table.js +34 -3
  38. package/src/tui/theme.js +36 -5
  39. package/src/tui/tui-filters.js +28 -8
  40. package/src/tui/tui-state.js +22 -6
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/assets/index-DFg1h0Nd.js +44 -0
  43. package/web/dist/index.html +5 -9
  44. package/web/index.html +3 -7
  45. package/web/server.js +97 -23
  46. package/web/vite.config.js +8 -0
  47. package/web/app.legacy.js +0 -900
  48. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  49. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  50. package/web/public/favicon.ico +0 -0
  51. package/web/public/favicons/apple-touch-icon.png +0 -0
  52. package/web/public/favicons/browserconfig.xml +0 -12
  53. package/web/public/favicons/favicon-16x16.png +0 -0
  54. package/web/public/favicons/favicon-192x192.png +0 -0
  55. package/web/public/favicons/favicon-32x32.png +0 -0
  56. package/web/public/favicons/favicon-48x48.png +0 -0
  57. package/web/public/favicons/favicon-512x512.png +0 -0
  58. package/web/public/favicons/favicon-96x96.png +0 -0
  59. package/web/public/favicons/favicon.ico +0 -0
  60. package/web/public/favicons/mstile-150x150.png +0 -0
  61. package/web/public/favicons/mstile-310x150.png +0 -0
  62. package/web/public/favicons/mstile-310x310.png +0 -0
  63. package/web/public/favicons/mstile-512x512.png +0 -0
  64. package/web/public/favicons/mstile-70x70.png +0 -0
  65. package/web/public/favicons/site.webmanifest +0 -25
  66. package/web/src/App.jsx +0 -521
  67. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  68. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  69. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  70. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  71. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  72. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  73. package/web/src/components/atoms/HealthCell.jsx +0 -143
  74. package/web/src/components/atoms/HealthCell.module.css +0 -60
  75. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  76. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  77. package/web/src/components/atoms/MoodCell.jsx +0 -25
  78. package/web/src/components/atoms/MoodCell.module.css +0 -6
  79. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  80. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  81. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  82. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  83. package/web/src/components/atoms/RankCell.jsx +0 -9
  84. package/web/src/components/atoms/RankCell.module.css +0 -9
  85. package/web/src/components/atoms/Sparkline.jsx +0 -63
  86. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  87. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  88. package/web/src/components/atoms/StatusDot.jsx +0 -13
  89. package/web/src/components/atoms/StatusDot.module.css +0 -29
  90. package/web/src/components/atoms/TPSCell.jsx +0 -36
  91. package/web/src/components/atoms/TPSCell.module.css +0 -44
  92. package/web/src/components/atoms/TierBadge.jsx +0 -10
  93. package/web/src/components/atoms/TierBadge.module.css +0 -29
  94. package/web/src/components/atoms/Toast.jsx +0 -25
  95. package/web/src/components/atoms/Toast.module.css +0 -41
  96. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  97. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  98. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  99. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  100. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  101. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  102. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  103. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  104. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  105. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  106. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  107. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  108. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  109. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  110. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  111. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  112. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  113. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  114. package/web/src/components/help/HelpView.jsx +0 -201
  115. package/web/src/components/help/HelpView.module.css +0 -156
  116. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  117. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  118. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  119. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  120. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  121. package/web/src/components/launch/LaunchButton.jsx +0 -30
  122. package/web/src/components/launch/LaunchButton.module.css +0 -35
  123. package/web/src/components/launch/LaunchModal.module.css +0 -125
  124. package/web/src/components/layout/Footer.jsx +0 -19
  125. package/web/src/components/layout/Footer.module.css +0 -10
  126. package/web/src/components/layout/Header.jsx +0 -265
  127. package/web/src/components/layout/Header.module.css +0 -357
  128. package/web/src/components/palette/CommandPalette.jsx +0 -301
  129. package/web/src/components/palette/CommandPalette.module.css +0 -121
  130. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  131. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  132. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  133. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  134. package/web/src/components/recommend/RecommendView.jsx +0 -121
  135. package/web/src/components/recommend/RecommendView.module.css +0 -57
  136. package/web/src/components/router/RouterView.jsx +0 -1212
  137. package/web/src/components/router/RouterView.module.css +0 -1289
  138. package/web/src/components/settings/SettingsView.jsx +0 -555
  139. package/web/src/components/settings/SettingsView.module.css +0 -550
  140. package/web/src/components/tools/ToolPicker.jsx +0 -86
  141. package/web/src/components/tools/ToolPicker.module.css +0 -97
  142. package/web/src/components/update/UpdateChip.jsx +0 -104
  143. package/web/src/components/update/UpdateChip.module.css +0 -146
  144. package/web/src/global.css +0 -298
  145. package/web/src/hooks/urlState.constants.js +0 -28
  146. package/web/src/hooks/useChangelog.js +0 -51
  147. package/web/src/hooks/useColumnSizing.js +0 -211
  148. package/web/src/hooks/useFavorites.js +0 -150
  149. package/web/src/hooks/useFilter.js +0 -310
  150. package/web/src/hooks/useInstalledModels.js +0 -42
  151. package/web/src/hooks/useRecommend.js +0 -67
  152. package/web/src/hooks/useRouterDashboard.js +0 -133
  153. package/web/src/hooks/useSSE.js +0 -73
  154. package/web/src/hooks/useSocket.js +0 -203
  155. package/web/src/hooks/useTheme.js +0 -83
  156. package/web/src/hooks/useTokenUsage.js +0 -103
  157. package/web/src/hooks/useToolMode.js +0 -77
  158. package/web/src/hooks/useUpdateChecker.js +0 -91
  159. package/web/src/hooks/useUrlState.js +0 -170
  160. package/web/src/main.jsx +0 -15
  161. package/web/src/utils/download.js +0 -15
  162. package/web/src/utils/format.js +0 -42
  163. package/web/src/utils/m3.js +0 -57
  164. package/web/src/utils/ranks.js +0 -37
  165. package/web/styles.legacy.css +0 -963
@@ -1,143 +0,0 @@
1
- /**
2
- * @file web/src/components/atoms/HealthCell.jsx
3
- * @description Detailed health/status column — matches CLI Health column.
4
- * 📖 Shows the current provider status next to a Tabler icon that matches the
5
- * 📖 state. The icon is color-coded so a quick glance is enough to know if
6
- * 📖 the row is healthy, slow, down, blocked, etc.
7
- * 📖
8
- * 📖 Icon → state mapping:
9
- * 📖 ✅ up → IconChecks (green)
10
- * 📖 ⏳ timeout → IconHourglassOff (yellow)
11
- * 📖 ⏳ pending → IconHourglass (yellow)
12
- * 📖 🔐 AUTH FAIL → IconLock (red, bold)
13
- * 📖 🔑 NO KEY → NoKeyIcon (yellow key + red slash)
14
- * 📖 🔥 429 → IconFlame (red)
15
- * 📖 🚫 404 → IconError404 (red)
16
- * 📖 💥 500 → IconAlertOctagon (red)
17
- * 📖 🔌 502 → IconPlug (red)
18
- * 📖 🔒 503 → IconCloudOff (red)
19
- * 📖 ⏰ 504 → IconClock (red)
20
- * 📖 ❌ unknown → IconBan (red)
21
- */
22
- import {
23
- IconChecks,
24
- IconHourglass,
25
- IconHourglassOff,
26
- IconCircleDotted,
27
- IconLock,
28
- IconFlame,
29
- IconError404,
30
- IconAlertOctagon,
31
- IconPlug,
32
- IconCloudOff,
33
- IconClock,
34
- IconBan,
35
- } from '@tabler/icons-react'
36
- import NoKeyIcon from './NoKeyIcon.jsx'
37
- import styles from './HealthCell.module.css'
38
-
39
- const ICON_SIZE = 12
40
- const ICON_STROKE = 1.8
41
-
42
- // 📖 Themed icon helper: same color palette as the rest of the Health cell text,
43
- // 📖 so the icon + label always match. Reading the CSS var at render time keeps
44
- // 📖 the dark/light palettes aligned with the rest of the dashboard.
45
- const HEALTH_ICON_COLOR = {
46
- success: 'var(--color-success, #00ff88)',
47
- warning: 'var(--color-warning, #ffaa00)',
48
- danger: 'var(--color-danger, #ff4444)',
49
- dim: 'var(--color-text-dim, #444)',
50
- }
51
-
52
- const ERROR_LABELS = {
53
- '404': '404 NOT FOUND',
54
- '410': '410 GONE',
55
- '429': '429 TRY LATER',
56
- '500': '500 ERROR',
57
- '502': '502 ERROR',
58
- '503': '503 ERROR',
59
- '504': '504 TIMEOUT',
60
- }
61
-
62
- // 📖 Per-HTTP-code icon + color picker. Anything not in this map falls back to
63
- // 📖 the generic down-state rendering below.
64
- const ERROR_ICON = {
65
- '429': { Icon: IconFlame, color: HEALTH_ICON_COLOR.danger },
66
- '404': { Icon: IconError404, color: HEALTH_ICON_COLOR.danger },
67
- '500': { Icon: IconAlertOctagon, color: HEALTH_ICON_COLOR.danger },
68
- '502': { Icon: IconPlug, color: HEALTH_ICON_COLOR.danger },
69
- '503': { Icon: IconCloudOff, color: HEALTH_ICON_COLOR.danger },
70
- '504': { Icon: IconClock, color: HEALTH_ICON_COLOR.danger },
71
- }
72
-
73
- function statusLabel(status, httpCode, inRouterSet) {
74
- if (status === 'noauth') return 'NO KEY'
75
- if (status === 'auth_error') return 'AUTH FAIL'
76
- if (status === 'pending') return inRouterSet ? 'wait' : 'NOT IN SET'
77
- if (status === 'timeout') return 'TIMEOUT'
78
- if (status === 'down') {
79
- return ERROR_LABELS[httpCode] || (httpCode || 'ERROR')
80
- }
81
- if (status === 'up') return 'UP'
82
- return '?'
83
- }
84
-
85
- function statusClass(status, inRouterSet) {
86
- if (status === 'noauth') return styles.noKey
87
- if (status === 'up') return styles.up
88
- if (status === 'down') return styles.error
89
- if (status === 'timeout') return styles.warning
90
- if (status === 'auth_error') return styles.errorBold
91
- if (status === 'pending') return inRouterSet ? styles.warning : styles.notInSet
92
- return styles.dim
93
- }
94
-
95
- export default function HealthCell({ status, httpCode, inRouterSet = true }) {
96
- const text = statusLabel(status, httpCode, inRouterSet)
97
- const cls = statusClass(status, inRouterSet)
98
- const isNoKey = status === 'noauth'
99
- const isNotInSet = status === 'pending' && !inRouterSet
100
-
101
- // 📖 Pick the right icon + color combo for this state.
102
- let Icon = null
103
- let iconColor = null
104
- if (status === 'up') {
105
- Icon = IconChecks
106
- iconColor = HEALTH_ICON_COLOR.success
107
- } else if (status === 'timeout') {
108
- Icon = IconHourglassOff
109
- iconColor = HEALTH_ICON_COLOR.warning
110
- } else if (isNotInSet) {
111
- Icon = IconCircleDotted
112
- iconColor = HEALTH_ICON_COLOR.dim
113
- } else if (status === 'pending') {
114
- Icon = IconHourglass
115
- iconColor = HEALTH_ICON_COLOR.warning
116
- } else if (status === 'auth_error') {
117
- Icon = IconLock
118
- iconColor = HEALTH_ICON_COLOR.danger
119
- } else if (status === 'down' && ERROR_ICON[httpCode]) {
120
- Icon = ERROR_ICON[httpCode].Icon
121
- iconColor = ERROR_ICON[httpCode].color
122
- } else if (status === 'down') {
123
- // 📖 Generic "down" fallback (no recognized HTTP code).
124
- Icon = IconBan
125
- iconColor = HEALTH_ICON_COLOR.danger
126
- }
127
-
128
- return (
129
- <span className={`${styles.cell} ${cls}`}>
130
- {isNoKey ? (
131
- <NoKeyIcon size={15} title="No API key configured for this provider" />
132
- ) : Icon ? (
133
- <Icon
134
- size={ICON_SIZE}
135
- stroke={ICON_STROKE}
136
- style={{ color: iconColor, flexShrink: 0 }}
137
- aria-hidden="true"
138
- />
139
- ) : null}
140
- <span className={isNoKey ? styles.noKeyText : undefined}>{text}</span>
141
- </span>
142
- )
143
- }
@@ -1,60 +0,0 @@
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
- */
7
- .cell {
8
- font-family: var(--font-mono);
9
- font-size: 11px;
10
- font-weight: 600;
11
- display: inline-flex;
12
- align-items: center;
13
- gap: 5px;
14
- letter-spacing: 0.2px;
15
- white-space: nowrap;
16
- overflow: hidden;
17
- text-overflow: ellipsis;
18
- }
19
- .up { color: #00ff88; }
20
- .warning { color: #ffaa00; }
21
- .error { color: #ff4444; }
22
- .errorBold { color: #ff4444; font-weight: 800; }
23
- .dim { color: var(--color-text-dim); }
24
- /* 📖 Models not in the active router set — muted text so they stand out from
25
- 📖 genuinely 'pending' models that are still waiting for their first probe. */
26
- .notInSet { color: #4a4a5a; opacity: 0.7; }
27
-
28
- /* 📖 The "NO KEY" treatment is intentionally louder than the other states:
29
- 📖 - yellow chip background instead of plain text
30
- 📖 - 13px font (vs 11px elsewhere in the column)
31
- 📖 - bold weight, slightly more letter-spacing
32
- 📖 - dark text on yellow for AA contrast in dark + light themes */
33
- .noKey {
34
- background: rgba(255, 170, 0, 0.18);
35
- color: #ffaa00;
36
- border: 1px solid rgba(255, 170, 0, 0.45);
37
- padding: 1px 7px;
38
- border-radius: 4px;
39
- font-size: 12px;
40
- font-weight: 700;
41
- letter-spacing: 0.4px;
42
- }
43
- .noKeyText {
44
- /* 📖 Inherits the surrounding font, just bumps the weight and tracking for the
45
- 📖 label so the chip reads as a single badge even at narrow column widths. */
46
- font-weight: 700;
47
- letter-spacing: 0.4px;
48
- }
49
-
50
- :global([data-theme="light"]) .noKey {
51
- background: rgba(255, 170, 0, 0.25);
52
- border-color: rgba(255, 170, 0, 0.55);
53
- color: #b86b00;
54
- }
55
- :global([data-theme="light"]) .up { color: #008f4d; }
56
- :global([data-theme="light"]) .warning { color: #b86b00; }
57
- :global([data-theme="light"]) .error,
58
- :global([data-theme="light"]) .errorBold { color: #c02323; }
59
- :global([data-theme="light"]) .dim { color: #888; }
60
- :global([data-theme="light"]) .notInSet { color: #a0a0a8; }
@@ -1,35 +0,0 @@
1
- /**
2
- * @file web/src/components/atoms/LastPingCell.jsx
3
- * @description Last ping latency number with color coding + animated spinner during pings.
4
- * 📖 Values are milliseconds, displayed without the `ms` suffix to keep table cells compact.
5
- * 📖 During ping rounds: animated CSS spinner shows which models are being tested.
6
- */
7
- import styles from './LastPingCell.module.css'
8
-
9
- function pingClass(ms) {
10
- if (ms == null || ms === Infinity) return styles.none
11
- if (ms < 500) return styles.fast
12
- if (ms < 1500) return styles.medium
13
- return styles.slow
14
- }
15
-
16
- export default function LastPingCell({ ms, isPinging }) {
17
- if (ms == null) {
18
- return (
19
- <span className={`${styles.cell} ${styles.none}`}>
20
- {isPinging ? (
21
- <span className={styles.spinner} title="Testing…" />
22
- ) : (
23
- '—'
24
- )}
25
- </span>
26
- )
27
- }
28
-
29
- return (
30
- <span className={`${styles.cell} ${pingClass(ms)}`}>
31
- <span className={styles.value}>{ms}</span>
32
- {isPinging && <span className={styles.spinner} />}
33
- </span>
34
- )
35
- }
@@ -1,45 +0,0 @@
1
- .cell {
2
- font-family: var(--font-mono);
3
- font-size: 12px;
4
- font-weight: 600;
5
- display: inline-flex;
6
- align-items: center;
7
- gap: 4px;
8
- min-width: 54px;
9
- }
10
- .value { }
11
- .fast { color: #00ff88; }
12
- .medium { color: #ffaa00; }
13
- .slow { color: #ff4444; }
14
- .none { color: var(--color-text-dim); }
15
-
16
- /* ── Animated spinner for active pings ── */
17
- @keyframes pingPulse {
18
- 0%, 100% { opacity: 1; }
19
- 50% { opacity: 0.3; }
20
- }
21
- @keyframes pingRing {
22
- 0% { transform: rotate(0deg) scale(1); opacity: 1; }
23
- 70% { transform: rotate(180deg) scale(0.6); opacity: 0.6; }
24
- 100% { transform: rotate(360deg) scale(1); opacity: 1; }
25
- }
26
- .spinner {
27
- display: inline-block;
28
- width: 10px;
29
- height: 10px;
30
- border: 1.5px solid rgba(0, 255, 136, 0.25);
31
- border-top-color: #00ff88;
32
- border-radius: 50%;
33
- animation: pingRing 0.8s linear infinite;
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;
45
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * @file web/src/components/atoms/MoodCell.jsx
3
- * @description Tiny verdict indicator emoji (1st column) matching CLI ❔ column.
4
- * 📖 Mirrors the full Verdict as a compact emoji: 🟩 Perfect, 🟢 Normal, 🟡 Spiky,
5
- * 🟠 Slow, 🔴 Very Slow, 🔥 Overloaded, 🟥 Unstable, ⚫ Not Active, ⏳ Pending.
6
- */
7
- import styles from './MoodCell.module.css'
8
-
9
- const VERDICT_EMOJI = {
10
- Perfect: '🟩',
11
- Normal: '🟢',
12
- Spiky: '🟡',
13
- Slow: '🟠',
14
- 'Very Slow': '🔴',
15
- Overloaded: '🔥',
16
- Unstable: '🟥',
17
- 'Not Active':'⚫',
18
- Pending: '⏳',
19
- Usable: '🟠', // fallback
20
- }
21
-
22
- export default function MoodCell({ verdict }) {
23
- const emoji = VERDICT_EMOJI[verdict] || '❔'
24
- return <span className={styles.mood}>{emoji}</span>
25
- }
@@ -1,6 +0,0 @@
1
- .mood {
2
- display: inline-block;
3
- font-size: 14px;
4
- text-align: center;
5
- width: 20px;
6
- }
@@ -1,65 +0,0 @@
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
- }
@@ -1,30 +0,0 @@
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
- }
@@ -1,187 +0,0 @@
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
- 'opencode-zen': { folder: 'opencode', hasColor: false, hasText: true },
79
- // 📖 Codestral intentionally reuses Mistral's branding — the colored Mistral
80
- // 📖 icon (so the brand association is visible) + the literal caption
81
- // 📖 "Codestral" as plain text. `iconFile` overrides the default filename
82
- // 📖 lookup (we want mistral-color, not codestral-color which doesn't exist),
83
- // 📖 and `caption` overrides the origin so we don't show "Codestral" twice.
84
- codestral: { folder: 'mistral', iconFile: 'mistral-color', hasColor: true, hasText: false, legacy: true, caption: 'Codestral' },
85
- }
86
-
87
- function getSvg(folder, filename) {
88
- if (!folder || !filename) return null
89
- return svgModules[`../../../assets/providers/${folder}/${filename}.svg`] || null
90
- }
91
-
92
- export default function ProviderLogo({ providerKey, origin }) {
93
- const cfg = PROVIDER_CONFIG[providerKey]
94
-
95
- // 📖 Unknown / unmapped provider: fall back to plain text. Same as the old
96
- // 📖 `.providerPill` so we don't visually regress on rows we couldn't classify.
97
- if (!cfg) {
98
- return <span className={styles.fallback}>{origin}</span>
99
- }
100
-
101
- // 📖 Pick the best icon: prefer the colorful variant, fall back to mono.
102
- // 📖 Mono uses currentColor → inherits the cell's text color → theme-adaptive.
103
- // 📖 `cfg.iconFile` overrides the default filename so providers like Codestral
104
- // 📖 can borrow another provider's icon (e.g. mistral-color for brand parity).
105
- const iconSvg =
106
- (cfg.iconFile && getSvg(cfg.folder, cfg.iconFile)) ||
107
- (cfg.hasColor && getSvg(cfg.folder, `${cfg.folder}-color`)) ||
108
- getSvg(cfg.folder, cfg.folder)
109
-
110
- // 📖 Pick the text wordmark (currentColor-based, theme-adaptive).
111
- const textSvg = cfg.hasText ? getSvg(cfg.folder, `${cfg.folder}-text`) : null
112
-
113
- // ─── Layouts ─────────────────────────────────────────────────────────────
114
- // 📖 Single horizontal row: [icon | gap | wordmark], all in --pl-max-h tall.
115
- // 📖 `overflow: hidden` on the row is a safety net for the rare case where
116
- // 📖 a particularly wide wordmark (e.g. cloudflare ~145px) still overflows
117
- // 📖 the column even at 12px height — better clipped than warping the table.
118
- if (iconSvg && textSvg) {
119
- return (
120
- <span className={styles.row} title={origin}>
121
- <span
122
- className={styles.icon}
123
- aria-hidden="true"
124
- dangerouslySetInnerHTML={{ __html: iconSvg }}
125
- />
126
- <span
127
- className={styles.textLogo}
128
- aria-hidden="true"
129
- dangerouslySetInnerHTML={{ __html: textSvg }}
130
- />
131
- </span>
132
- )
133
- }
134
-
135
- // 📖 Legacy providers (Scaleway, OVH by default): graphic only, no wordmark SVG.
136
- // 📖 Show the graphic + a caption as plain text so the cell stays scannable
137
- // 📖 in the table. `cfg.caption` overrides the origin name (Codestral wants
138
- // 📖 its own label, not whatever sources.js returns). `cfg.noCaption` skips
139
- // 📖 the caption entirely (OVH's graphic is itself a full wordmark, and
140
- // 📖 Scaleway's wordmark already contains the brand name).
141
- // 📖 `cfg.iconClass` (when set) is appended to the icon span — used to attach
142
- // 📖 a global class for per-theme color overrides (e.g. Scaleway's lighter
143
- // 📖 purple in dark mode).
144
- if (iconSvg) {
145
- const captionText = cfg.caption ?? origin
146
- if (cfg.noCaption) {
147
- const wordmarkClass = cfg.iconClass
148
- ? `${styles.iconLegacyWordmark} ${cfg.iconClass}`
149
- : styles.iconLegacyWordmark
150
- return (
151
- <span className={styles.row} title={origin}>
152
- <span
153
- className={wordmarkClass}
154
- aria-hidden="true"
155
- dangerouslySetInnerHTML={{ __html: iconSvg }}
156
- />
157
- </span>
158
- )
159
- }
160
- return (
161
- <span className={styles.row} title={origin}>
162
- <span
163
- className={styles.icon}
164
- aria-hidden="true"
165
- dangerouslySetInnerHTML={{ __html: iconSvg }}
166
- />
167
- <span className={styles.fallback}>{captionText}</span>
168
- </span>
169
- )
170
- }
171
-
172
- // 📖 Icon-only fallback (shouldn't happen with current assets, but safe).
173
- if (textSvg) {
174
- return (
175
- <span className={styles.row} title={origin}>
176
- <span
177
- className={styles.textLogo}
178
- aria-hidden="true"
179
- dangerouslySetInnerHTML={{ __html: textSvg }}
180
- />
181
- </span>
182
- )
183
- }
184
-
185
- // 📖 No assets at all (e.g. codestral without icon). Plain text fallback.
186
- return <span className={styles.fallback}>{origin}</span>
187
- }