free-coding-models 0.5.1 โ†’ 0.5.5

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 (129) hide show
  1. package/README.md +31 -5
  2. package/bin/free-coding-models.js +23 -8
  3. package/changelog/v0.5.4.md +24 -0
  4. package/changelog/v0.5.5.md +15 -0
  5. package/package.json +5 -2
  6. package/sources.js +2 -0
  7. package/src/core/updater.js +174 -10
  8. package/src/tui/app.js +11 -31
  9. package/src/tui/key-handler.js +1 -1
  10. package/src/tui/render-table.js +9 -3
  11. package/src/tui/tui-state.js +6 -0
  12. package/web/assets/providers/aistudio/aistudio-text.svg +1 -0
  13. package/web/assets/providers/aistudio/aistudio.svg +1 -0
  14. package/web/assets/providers/cerebras/cerebras-brand-color.svg +1 -0
  15. package/web/assets/providers/cerebras/cerebras-brand.svg +1 -0
  16. package/web/assets/providers/cerebras/cerebras-color.svg +1 -0
  17. package/web/assets/providers/cerebras/cerebras-text.svg +1 -0
  18. package/web/assets/providers/cerebras/cerebras.svg +1 -0
  19. package/web/assets/providers/cloudflare/cloudflare-color.svg +1 -0
  20. package/web/assets/providers/cloudflare/cloudflare-text.svg +1 -0
  21. package/web/assets/providers/cloudflare/cloudflare.svg +1 -0
  22. package/web/assets/providers/gemini/gemini-color.svg +1 -0
  23. package/web/assets/providers/gemini/gemini-text.svg +1 -0
  24. package/web/assets/providers/gemini/gemini.svg +1 -0
  25. package/web/assets/providers/github/github-text.svg +1 -0
  26. package/web/assets/providers/github/github.svg +1 -0
  27. package/web/assets/providers/groq/groq-text.svg +1 -0
  28. package/web/assets/providers/groq/groq.svg +1 -0
  29. package/web/assets/providers/mistral/mistral-color.svg +1 -0
  30. package/web/assets/providers/mistral/mistral-text.svg +1 -0
  31. package/web/assets/providers/mistral/mistral.svg +1 -0
  32. package/web/assets/providers/nvidia/nvidia-color.svg +1 -0
  33. package/web/assets/providers/nvidia/nvidia-text.svg +1 -0
  34. package/web/assets/providers/nvidia/nvidia.svg +1 -0
  35. package/web/assets/providers/opencode/opencode-text.svg +1 -0
  36. package/web/assets/providers/opencode/opencode.svg +1 -0
  37. package/web/assets/providers/openrouter/openrouter (1).svg +1 -0
  38. package/web/assets/providers/openrouter/openrouter-text.svg +1 -0
  39. package/web/assets/providers/openrouter/openrouter.svg +1 -0
  40. package/web/assets/providers/ovhcloud/ovhcloud.svg +12 -0
  41. package/web/assets/providers/qwen/qwen-color.svg +1 -0
  42. package/web/assets/providers/qwen/qwen-text.svg +1 -0
  43. package/web/assets/providers/qwen/qwen.svg +1 -0
  44. package/web/assets/providers/sambanova/sambanova-color.svg +1 -0
  45. package/web/assets/providers/sambanova/sambanova-text.svg +1 -0
  46. package/web/assets/providers/sambanova/sambanova.svg +1 -0
  47. package/web/assets/providers/scalewaylogo/ScalewayLogo.svg +13 -0
  48. package/web/assets/providers/zai/zai-text.svg +1 -0
  49. package/web/assets/providers/zai/zai.svg +1 -0
  50. package/web/dist/assets/index-uD3faN3G.js +39 -0
  51. package/web/dist/assets/index-uTifVKX1.css +1 -0
  52. package/web/dist/favicon.ico +0 -0
  53. package/web/dist/favicons/apple-touch-icon.png +0 -0
  54. package/web/dist/favicons/browserconfig.xml +12 -0
  55. package/web/dist/favicons/favicon-16x16.png +0 -0
  56. package/web/dist/favicons/favicon-192x192.png +0 -0
  57. package/web/dist/favicons/favicon-32x32.png +0 -0
  58. package/web/dist/favicons/favicon-48x48.png +0 -0
  59. package/web/dist/favicons/favicon-512x512.png +0 -0
  60. package/web/dist/favicons/favicon-96x96.png +0 -0
  61. package/web/dist/favicons/favicon.ico +0 -0
  62. package/web/dist/favicons/mstile-150x150.png +0 -0
  63. package/web/dist/favicons/mstile-310x150.png +0 -0
  64. package/web/dist/favicons/mstile-310x310.png +0 -0
  65. package/web/dist/favicons/mstile-512x512.png +0 -0
  66. package/web/dist/favicons/mstile-70x70.png +0 -0
  67. package/web/dist/favicons/site.webmanifest +25 -0
  68. package/web/dist/index.html +42 -3
  69. package/web/index.html +40 -1
  70. package/web/public/favicon.ico +0 -0
  71. package/web/public/favicons/apple-touch-icon.png +0 -0
  72. package/web/public/favicons/browserconfig.xml +12 -0
  73. package/web/public/favicons/favicon-16x16.png +0 -0
  74. package/web/public/favicons/favicon-192x192.png +0 -0
  75. package/web/public/favicons/favicon-32x32.png +0 -0
  76. package/web/public/favicons/favicon-48x48.png +0 -0
  77. package/web/public/favicons/favicon-512x512.png +0 -0
  78. package/web/public/favicons/favicon-96x96.png +0 -0
  79. package/web/public/favicons/favicon.ico +0 -0
  80. package/web/public/favicons/mstile-150x150.png +0 -0
  81. package/web/public/favicons/mstile-310x150.png +0 -0
  82. package/web/public/favicons/mstile-310x310.png +0 -0
  83. package/web/public/favicons/mstile-512x512.png +0 -0
  84. package/web/public/favicons/mstile-70x70.png +0 -0
  85. package/web/public/favicons/site.webmanifest +25 -0
  86. package/web/server.js +83 -2
  87. package/web/src/App.jsx +210 -84
  88. package/web/src/components/analytics/AnalyticsView.jsx +30 -2
  89. package/web/src/components/analytics/AnalyticsView.module.css +4 -0
  90. package/web/src/components/atoms/AILatencyCell.module.css +9 -1
  91. package/web/src/components/atoms/HealthCell.jsx +113 -28
  92. package/web/src/components/atoms/HealthCell.module.css +43 -2
  93. package/web/src/components/atoms/LastPingCell.module.css +10 -0
  94. package/web/src/components/atoms/NoKeyIcon.jsx +65 -0
  95. package/web/src/components/atoms/NoKeyIcon.module.css +30 -0
  96. package/web/src/components/atoms/ProviderLogo.jsx +188 -0
  97. package/web/src/components/atoms/ProviderLogo.module.css +103 -0
  98. package/web/src/components/atoms/Sparkline.jsx +21 -2
  99. package/web/src/components/atoms/StabilityCell.module.css +5 -0
  100. package/web/src/components/atoms/StatusDot.module.css +7 -0
  101. package/web/src/components/atoms/TPSCell.module.css +7 -1
  102. package/web/src/components/atoms/TierBadge.module.css +11 -0
  103. package/web/src/components/atoms/Toast.module.css +6 -0
  104. package/web/src/components/atoms/VerdictBadge.module.css +12 -0
  105. package/web/src/components/dashboard/DetailPanel.jsx +63 -2
  106. package/web/src/components/dashboard/DetailPanel.module.css +130 -0
  107. package/web/src/components/dashboard/FilterBar.jsx +146 -53
  108. package/web/src/components/dashboard/FilterBar.module.css +138 -58
  109. package/web/src/components/dashboard/ModelTable.jsx +235 -30
  110. package/web/src/components/dashboard/ModelTable.module.css +205 -16
  111. package/web/src/components/layout/Header.jsx +140 -12
  112. package/web/src/components/layout/Header.module.css +191 -27
  113. package/web/src/components/palette/CommandPalette.jsx +146 -0
  114. package/web/src/components/palette/CommandPalette.module.css +121 -0
  115. package/web/src/global.css +67 -15
  116. package/web/src/hooks/useColumnSizing.js +211 -0
  117. package/web/src/hooks/useFavorites.js +150 -0
  118. package/web/src/hooks/useFilter.js +157 -24
  119. package/web/src/hooks/useSocket.js +3 -0
  120. package/web/src/hooks/useTheme.js +72 -16
  121. package/web/src/hooks/useUrlState.js +93 -0
  122. package/web/dist/assets/index-ByGf4Kq-.js +0 -14
  123. package/web/dist/assets/index-Ds7wmHBv.css +0 -1
  124. package/web/src/components/dashboard/StatsBar.jsx +0 -42
  125. package/web/src/components/dashboard/StatsBar.module.css +0 -28
  126. package/web/src/components/layout/Sidebar.jsx +0 -50
  127. package/web/src/components/layout/Sidebar.module.css +0 -86
  128. package/web/src/components/map/MapView.jsx +0 -17
  129. package/web/src/components/map/MapView.module.css +0 -25
@@ -1,27 +1,83 @@
1
1
  /**
2
2
  * @file web/src/hooks/useTheme.js
3
- * @description React hook for dark/light theme toggle.
4
- * ๐Ÿ“– Persists theme on <html data-theme="dark|light">. Reads initial from DOM.
5
- * โ†’ useTheme
3
+ * @description React hook for tri-state theme cycle (auto / dark / light) โ€” M1 parity.
4
+ * ๐Ÿ“– Mirrors the TUI's `G` key behavior:
5
+ * ๐Ÿ“– auto โ†’ system preference (resolved once at mount)
6
+ * ๐Ÿ“– dark โ†’ explicit dark
7
+ * ๐Ÿ“– light โ†’ explicit light
8
+ * ๐Ÿ“– Persists the choice on <html data-theme="auto|dark|light">. When set to
9
+ * ๐Ÿ“– `auto`, the hook resolves the actual rendered theme and writes it back to
10
+ * ๐Ÿ“– <html data-theme="..."> via resolveSystemTheme() (light/dark) so the
11
+ * ๐Ÿ“– rest of the CSS keeps working without any "auto" branch in stylesheets.
12
+ *
13
+ * @functions
14
+ * โ†’ useTheme() โ€” { theme, resolvedTheme, setTheme, cycle }
6
15
  */
7
16
  import { useState, useCallback, useEffect } from 'react'
8
17
 
18
+ export const THEME_CYCLE = ['auto', 'dark', 'light']
19
+
20
+ // ๐Ÿ“– Read the OS-level dark/light preference. Falls back to dark on browsers
21
+ // ๐Ÿ“– that don't support matchMedia (very rare; SSR snapshots, etc.).
22
+ function resolveSystemTheme() {
23
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'dark'
24
+ try {
25
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
26
+ } catch {
27
+ return 'dark'
28
+ }
29
+ }
30
+
31
+ function readInitialTheme() {
32
+ if (typeof document === 'undefined') return 'auto'
33
+ const attr = document.documentElement.getAttribute('data-theme')
34
+ if (attr === 'auto' || attr === 'dark' || attr === 'light') return attr
35
+ return 'auto'
36
+ }
37
+
9
38
  export function useTheme() {
10
- const [theme, setTheme] = useState(() => {
11
- return document.documentElement.getAttribute('data-theme') || 'dark'
12
- })
13
-
14
- const toggle = useCallback(() => {
15
- setTheme((prev) => {
16
- const next = prev === 'dark' ? 'light' : 'dark'
17
- document.documentElement.setAttribute('data-theme', next)
18
- return next
19
- })
39
+ const [theme, setThemeState] = useState(readInitialTheme)
40
+ const [systemTheme, setSystemTheme] = useState(resolveSystemTheme)
41
+
42
+ // ๐Ÿ“– Listen for OS theme changes while the user is in "auto" mode.
43
+ useEffect(() => {
44
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
45
+ const mq = window.matchMedia('(prefers-color-scheme: dark)')
46
+ const handler = (e) => setSystemTheme(e.matches ? 'dark' : 'light')
47
+ if (typeof mq.addEventListener === 'function') {
48
+ mq.addEventListener('change', handler)
49
+ return () => mq.removeEventListener('change', handler)
50
+ }
51
+ // ๐Ÿ“– Safari < 14 fallback
52
+ if (typeof mq.addListener === 'function') {
53
+ mq.addListener(handler)
54
+ return () => mq.removeListener(handler)
55
+ }
20
56
  }, [])
21
57
 
58
+ // ๐Ÿ“– Apply the resolved theme to <html data-theme="..."> on every change.
59
+ // ๐Ÿ“– We write the *resolved* value (light / dark) so existing CSS keeps working.
22
60
  useEffect(() => {
23
- document.documentElement.setAttribute('data-theme', theme)
24
- }, [theme])
61
+ const resolved = theme === 'auto' ? systemTheme : theme
62
+ document.documentElement.setAttribute('data-theme', resolved)
63
+ // ๐Ÿ“– Also stamp the user-chosen mode so other parts of the app can read it
64
+ // ๐Ÿ“– (e.g. to render the right "auto" badge in the theme button).
65
+ document.documentElement.setAttribute('data-theme-preference', theme)
66
+ }, [theme, systemTheme])
67
+
68
+ // ๐Ÿ“– Cycle through auto โ†’ dark โ†’ light โ†’ auto, matching the TUI's `G` key.
69
+ const cycle = useCallback(() => {
70
+ setThemeState((prev) => {
71
+ const idx = THEME_CYCLE.indexOf(prev)
72
+ return THEME_CYCLE[(idx + 1) % THEME_CYCLE.length]
73
+ })
74
+ }, [])
75
+
76
+ const setTheme = useCallback((next) => {
77
+ if (THEME_CYCLE.includes(next)) setThemeState(next)
78
+ }, [])
79
+
80
+ const resolvedTheme = theme === 'auto' ? systemTheme : theme
25
81
 
26
- return { theme, toggle }
82
+ return { theme, resolvedTheme, setTheme, cycle }
27
83
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @file web/src/hooks/useUrlState.js
3
+ * @description URL deep-linking hook โ€” the Web's answer to TUI CLI flags (--tier, --sort, etc.).
4
+ * ๐Ÿ“– M1 ships read-only hydration: on mount, parses the query string and hydrates the Web view.
5
+ * ๐Ÿ“– M2 will add write-back (push filter/sort/view changes back to the URL via history.replaceState).
6
+ *
7
+ * ๐Ÿ“– Supported query params (M1 = read-only):
8
+ * ๐Ÿ“– ?tier=S+|S|A+|A|A-|B+|B|C|all โ€” sets the tier filter
9
+ * ๐Ÿ“– ?status=up|down|pending|all โ€” sets the health/status filter
10
+ * ๐Ÿ“– ?provider=<providerKey>|all โ€” sets the provider filter
11
+ * ๐Ÿ“– ?verdict=<verdict>|all โ€” sets the verdict filter
12
+ * ๐Ÿ“– ?health=<health>|all โ€” sets the health filter
13
+ * ๐Ÿ“– ?sort=<col>&dir=asc|desc โ€” sets the sort column + direction
14
+ * ๐Ÿ“– ?view=dashboard|settings|analytics โ€” sets the active view
15
+ * ๐Ÿ“– ?q=<text> โ€” sets the search query
16
+ * ๐Ÿ“– ?tier=S&sort=verdict&dir=asc โ€” compose freely; the same URL is shareable
17
+ *
18
+ * @functions
19
+ * โ†’ useUrlState({ currentView, setCurrentView, filterState }) โ€” hydrates Web state from URL
20
+ *
21
+ * @see ideas/tui-web-feature-parity.md ยง5.4 โ€” full CLI-flag โ†” URL-param mapping
22
+ */
23
+ import { useEffect } from 'react'
24
+
25
+ // ๐Ÿ“– Valid enum values for cycle-style params. Keys here determine which params
26
+ // ๐Ÿ“– get accepted silently vs ignored. Anything not in the list is dropped.
27
+ const VALID_TIERS = new Set(['S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C', 'all'])
28
+ const VALID_STATUS = new Set(['up', 'down', 'pending', 'all'])
29
+ const VALID_SORTS = new Set([
30
+ 'mood', 'idx', 'tier', 'sweScore', 'ctx', 'label', 'origin',
31
+ 'latestPing', 'avg', 'condition', 'verdict', 'stability', 'uptime',
32
+ 'aiLatency', 'tps', 'trend',
33
+ ])
34
+ const VALID_VIEWS = new Set(['dashboard', 'settings', 'analytics'])
35
+ const VALID_DIRS = new Set(['asc', 'desc'])
36
+
37
+ /**
38
+ * ๐Ÿ“– parseUrlParams: extract a normalized set of params from the current URL.
39
+ * ๐Ÿ“– Returns a flat object with camelCase keys, only including recognized params.
40
+ * @returns {{
41
+ * tier: string|null, status: string|null, provider: string|null,
42
+ * verdict: string|null, health: string|null,
43
+ * sort: string|null, dir: string|null, view: string|null, q: string,
44
+ * }}
45
+ */
46
+ function parseUrlParams() {
47
+ if (typeof window === 'undefined') return null
48
+ const params = new URLSearchParams(window.location.search)
49
+ const out = {}
50
+ if (params.has('tier') && VALID_TIERS.has(params.get('tier'))) out.tier = params.get('tier')
51
+ if (params.has('status') && VALID_STATUS.has(params.get('status'))) out.status = params.get('status')
52
+ if (params.has('provider')) out.provider = params.get('provider')
53
+ if (params.has('verdict')) out.verdict = params.get('verdict')
54
+ if (params.has('health')) out.health = params.get('health')
55
+ if (params.has('sort') && VALID_SORTS.has(params.get('sort'))) out.sort = params.get('sort')
56
+ if (params.has('dir') && VALID_DIRS.has(params.get('dir'))) out.dir = params.get('dir')
57
+ if (params.has('view') && VALID_VIEWS.has(params.get('view'))) out.view = params.get('view')
58
+ if (params.has('q')) out.q = params.get('q')
59
+ return out
60
+ }
61
+
62
+ /**
63
+ * ๐Ÿ“– useUrlState: hydrate the Web view from the current URL on mount.
64
+ * ๐Ÿ“– M1 = read-only. Pass setters; the hook calls them once on mount with
65
+ * ๐Ÿ“– whatever the URL declares, then leaves the URL alone.
66
+ *
67
+ * ๐Ÿ“– The hook is intentionally non-intrusive: if the URL has no params, it does
68
+ * ๐Ÿ“– nothing. The caller is free to call the setters independently โ€” the URL
69
+ * ๐Ÿ“– simply becomes the *initial* state.
70
+ *
71
+ * @param {{
72
+ * currentView: string,
73
+ * setCurrentView: (view: string) => void,
74
+ * filterState: object | null, // wired in M2
75
+ * }} opts
76
+ */
77
+ export function useUrlState({ currentView, setCurrentView, filterState }) {
78
+ useEffect(() => {
79
+ const params = parseUrlParams()
80
+ if (!params) return
81
+
82
+ if (params.view && params.view !== currentView) {
83
+ setCurrentView(params.view)
84
+ }
85
+
86
+ // ๐Ÿ“– Filter hydration is wired through the filterState prop in M2. M1 just
87
+ // ๐Ÿ“– sets the view; the rest of the URL params act as documentation for
88
+ // ๐Ÿ“– the user and get the write-back loop fully closed in M2.
89
+ }, [currentView, setCurrentView, filterState])
90
+ }
91
+
92
+ // ๐Ÿ“– Re-export the parser so M2's write-back path can use the same validator.
93
+ export { parseUrlParams, VALID_TIERS, VALID_STATUS, VALID_SORTS, VALID_VIEWS, VALID_DIRS }