free-coding-models 0.5.30 → 0.5.32

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.
@@ -48,7 +48,7 @@
48
48
  <link rel="preconnect" href="https://fonts.googleapis.com">
49
49
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
50
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
51
- <script type="module" crossorigin src="/assets/index-I6N91rH7.js"></script>
51
+ <script type="module" crossorigin src="/assets/index-C_tCF0A5.js"></script>
52
52
  <link rel="stylesheet" crossorigin href="/assets/index-CsFt3qt5.css">
53
53
  </head>
54
54
  <body>
package/web/server.js CHANGED
@@ -751,6 +751,45 @@ async function handleRequest(req, res) {
751
751
  return
752
752
  }
753
753
 
754
+ // 📖 /api/router/sets/:name — PUT to edit/rename/replace models in the set.
755
+ const setPutMatch = url.pathname.match(/^\/api\/router\/sets\/([^/]+)$/)
756
+ if (setPutMatch && req.method === 'PUT') {
757
+ const setName = decodeURIComponent(setPutMatch[1])
758
+ const body = await readJsonBody(req)
759
+ const proxy = await proxyToDaemon(`/sets/${encodeURIComponent(setName)}`, {
760
+ method: 'PUT',
761
+ headers: { 'Content-Type': 'application/json' },
762
+ body: JSON.stringify(body),
763
+ })
764
+ if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
765
+ // 📖 Fallback: if daemon is offline, edit config directly so the UI still works.
766
+ if (!proxy || !proxy.ok) {
767
+ if (!config.router) config.router = {}
768
+ if (!config.router.sets) config.router.sets = {}
769
+ if (config.router.sets[setName]) {
770
+ const nextName = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : setName
771
+ const nextSets = { ...config.router.sets }
772
+ delete nextSets[setName]
773
+ nextSets[nextName] = {
774
+ ...config.router.sets[setName],
775
+ ...body,
776
+ name: nextName,
777
+ models: Array.isArray(body.models) ? body.models : config.router.sets[setName].models,
778
+ }
779
+ config.router.sets = nextSets
780
+ if (config.router.activeSet === setName) {
781
+ config.router.activeSet = nextName
782
+ }
783
+ saveConfig(config)
784
+ broadcastUpdate({ immediate: true })
785
+ sendJson(res, 200, { set: config.router.sets[nextName], router: config.router })
786
+ return
787
+ }
788
+ }
789
+ sendJson(res, proxy?.status || 502, proxy?.data || { error: 'Daemon not reachable' })
790
+ return
791
+ }
792
+
754
793
  // 📖 /api/router/sets/:name/sync — re-run the probe-based sync pipeline
755
794
  // 📖 against the named set. Used by the Web Router Dashboard's "Sync
756
795
  // 📖 best models" button so the user can rebuild a set with models
package/web/src/App.jsx CHANGED
@@ -500,6 +500,7 @@ export default function App() {
500
500
  <RouterView
501
501
  onClose={() => setRouterOpen(false)}
502
502
  onToast={addToast}
503
+ favorites={favorites}
503
504
  />
504
505
  )}
505
506
 
@@ -55,7 +55,7 @@ const SAVE_STATUS_SAVING = { kind: 'saving' }
55
55
  const SAVE_STATUS_SAVED = { kind: 'saved' }
56
56
  const SAVE_STATUS_ERROR = (message) => ({ kind: 'error', message })
57
57
 
58
- export default function RouterView({ onClose, onToast }) {
58
+ export default function RouterView({ onClose, onToast, favorites }) {
59
59
  const [status, setStatus] = useState(null)
60
60
  const [stats, setStats] = useState(null)
61
61
  const [quickSetup, setQuickSetup] = useState(null)
@@ -252,6 +252,48 @@ export default function RouterView({ onClose, onToast }) {
252
252
  }
253
253
  }
254
254
 
255
+ // 📖 Replace active set models with favorites
256
+ const handleUseFavorites = async () => {
257
+ if (!activeSetName) return
258
+ const favList = favorites?.favorites || []
259
+ if (favList.length === 0) {
260
+ onToast?.('You do not have any favorite models yet. Star some models first!', 'info')
261
+ return
262
+ }
263
+
264
+ const nextModels = favList.map((key, idx) => {
265
+ const parts = key.split('/')
266
+ const provider = parts[0]
267
+ const model = parts.slice(1).join('/')
268
+ return { provider, model, priority: idx + 1 }
269
+ })
270
+
271
+ setSaveStatus(SAVE_STATUS_SAVING)
272
+ try {
273
+ const resp = await fetch(`/api/router/sets/${encodeURIComponent(activeSetName)}`, {
274
+ method: 'PUT',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify({ models: nextModels }),
277
+ })
278
+ if (!resp.ok) {
279
+ const err = await resp.json().catch(() => ({}))
280
+ throw new Error(err.error || `HTTP ${resp.status}`)
281
+ }
282
+ const data = await resp.json()
283
+ if (data?.sets?.[activeSetName]) {
284
+ setSetsData((prev) => ({ ...prev, sets: data.sets }))
285
+ } else {
286
+ await fetchSets()
287
+ }
288
+ setSaveStatus(SAVE_STATUS_SAVED)
289
+ onToast?.(`Replaced active set with ${favList.length} favorite model${favList.length === 1 ? '' : 's'}.`, 'success')
290
+ await fetchStatus()
291
+ } catch (err) {
292
+ setSaveStatus(SAVE_STATUS_ERROR(err.message || String(err)))
293
+ onToast?.(`Failed to replace set: ${err.message}`, 'error')
294
+ }
295
+ }
296
+
255
297
  const persistReorder = useCallback(async (nextModels) => {
256
298
  if (!activeSetName) return
257
299
  setSaveStatus(SAVE_STATUS_SAVING)
@@ -675,6 +717,17 @@ export default function RouterView({ onClose, onToast }) {
675
717
  <IconWand size={11} />
676
718
  Sync best
677
719
  </button>
720
+ {favorites && (
721
+ <button
722
+ className={styles.smallBtn}
723
+ onClick={handleUseFavorites}
724
+ disabled={saveStatus.kind === 'saving'}
725
+ title="Replace current router models with your favorite models"
726
+ >
727
+ <IconList size={11} />
728
+ Use favorites
729
+ </button>
730
+ )}
678
731
  {/* 📖 Probe all — run AI Latency benchmarks on every model in the
679
732
  set. Results stream into the rows below (AI Lat column). */}
680
733
  <button