fix(dashboard): cut duplicate API/icon spam, lazy-load provider assets
Share one /api/models fetch via useModelCaps cache, mount ModelSelectModal only when open, stop double fetchModelAliases on CLI tool cards, and resolve provider icons through a session 404 cache with missing PNGs + loading=lazy. Also include Claude Exa MCP toggle (claude-settings + ClaudeToolCard) that was already in the working tree. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,44 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getCapabilitiesForModel } from "open-sse/providers/capabilities.js";
|
||||
|
||||
// Fetch model capabilities once and expose a lookup by fullModel ("provider/model") or bare model id.
|
||||
// Module cache: one /api/models fetch shared by every useModelCaps instance.
|
||||
let cache = null; // { byFull, byId } | null
|
||||
let inflight = null;
|
||||
|
||||
function buildMaps(models) {
|
||||
const byFull = {};
|
||||
const byId = {};
|
||||
for (const m of models || []) {
|
||||
if (!m.caps) continue;
|
||||
if (m.fullModel) byFull[m.fullModel] = m.caps;
|
||||
if (m.model) byId[m.model] = m.caps;
|
||||
}
|
||||
return { byFull, byId };
|
||||
}
|
||||
|
||||
function loadModelCaps() {
|
||||
if (cache) return Promise.resolve(cache);
|
||||
if (inflight) return inflight;
|
||||
inflight = fetch("/api/models")
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error(`models ${res.status}`);
|
||||
const data = await res.json();
|
||||
cache = buildMaps(data.models);
|
||||
return cache;
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep null so a later mount can retry
|
||||
return { byFull: {}, byId: {} };
|
||||
})
|
||||
.finally(() => { inflight = null; });
|
||||
return inflight;
|
||||
}
|
||||
|
||||
// Resolve caps from a "provider/model" string or a bare model id.
|
||||
function resolveCaps(byFull, byId, key) {
|
||||
if (!key) return null;
|
||||
if (byFull[key]) return byFull[key];
|
||||
const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
|
||||
if (byId[bare]) return byId[bare];
|
||||
const provider = key.includes("/") ? key.slice(0, key.indexOf("/")) : null;
|
||||
const c = getCapabilitiesForModel(provider, bare);
|
||||
return { vision: c.vision, search: c.search, reasoning: c.reasoning };
|
||||
}
|
||||
|
||||
export function useModelCaps() {
|
||||
const [byFull, setByFull] = useState({});
|
||||
const [byId, setById] = useState({});
|
||||
const [byFull, setByFull] = useState(() => cache?.byFull || {});
|
||||
const [byId, setById] = useState(() => cache?.byId || {});
|
||||
|
||||
useEffect(() => {
|
||||
if (cache) {
|
||||
setByFull(cache.byFull);
|
||||
setById(cache.byId);
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/models");
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const full = {};
|
||||
const id = {};
|
||||
for (const m of data.models || []) {
|
||||
if (!m.caps) continue;
|
||||
if (m.fullModel) full[m.fullModel] = m.caps;
|
||||
if (m.model) id[m.model] = m.caps;
|
||||
}
|
||||
if (alive) { setByFull(full); setById(id); }
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
loadModelCaps().then((maps) => {
|
||||
if (alive) { setByFull(maps.byFull); setById(maps.byId); }
|
||||
});
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
|
||||
// Resolve caps from a "provider/model" string or a bare model id.
|
||||
const getCaps = (key) => {
|
||||
if (!key) return null;
|
||||
if (byFull[key]) return byFull[key];
|
||||
const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
|
||||
if (byId[bare]) return byId[bare];
|
||||
// Fallback: compute caps for dynamic models (passthrough/custom/suggested) not in static list
|
||||
const provider = key.includes("/") ? key.slice(0, key.indexOf("/")) : null;
|
||||
const c = getCapabilitiesForModel(provider, bare);
|
||||
return { vision: c.vision, search: c.search, reasoning: c.reasoning };
|
||||
};
|
||||
const getCaps = useCallback(
|
||||
(key) => resolveCaps(byFull, byId, key),
|
||||
[byFull, byId],
|
||||
);
|
||||
|
||||
return { getCaps };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user