"use client"; import { useState, useCallback, useEffect } from "react"; import PropTypes from "prop-types"; import { Card, Button, Modal } from "@/shared/components"; import { getModelsByProviderId, getModelKind } from "@/shared/constants/models"; import { getProviderAlias } from "@/shared/constants/providers"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; // ── ModelRow ─────────────────────────────────────────────────── export function ModelRow({ model, fullModel, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting }) { const borderColor = testStatus === "ok" ? "border-green-500/40" : testStatus === "error" ? "border-red-500/40" : "border-border"; const iconColor = testStatus === "ok" ? "#22c55e" : testStatus === "error" ? "#ef4444" : undefined; return (
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
{fullModel} {model.name && {model.name}}
{onTest && (
{isTesting ? "Testing..." : "Test"}
)}
{copied === `model-${model.id}` ? "Copied!" : "Copy"}
{isFree && FREE} {isCustom && ( )}
); } ModelRow.propTypes = { model: PropTypes.shape({ id: PropTypes.string.isRequired }).isRequired, fullModel: PropTypes.string.isRequired, copied: PropTypes.string, onCopy: PropTypes.func.isRequired, testStatus: PropTypes.oneOf(["ok", "error"]), isCustom: PropTypes.bool, isFree: PropTypes.bool, onDeleteAlias: PropTypes.func, onTest: PropTypes.func, isTesting: PropTypes.bool, }; // ── AddCustomModelModal ──────────────────────────────────────── function AddCustomModelModal({ isOpen, onSave, onClose }) { const [modelId, setModelId] = useState(""); const handleSave = () => { if (!modelId.trim()) return; onSave(modelId.trim()); setModelId(""); }; return (
setModelId(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSave()} placeholder="e.g. tts-1-hd" autoFocus />
); } AddCustomModelModal.propTypes = { isOpen: PropTypes.bool.isRequired, onSave: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, }; // ── ModelsCard ───────────────────────────────────────────────── // Self-contained card: shows models for a provider, filtered by optional `kindFilter`. // kindFilter: if provided, only shows models with matching type/kinds field. export default function ModelsCard({ providerId, kindFilter, providerAliasOverride }) { const { copied, copy } = useCopyToClipboard(); const [modelAliases, setModelAliases] = useState({}); const [customModels, setCustomModels] = useState([]); const [modelTestResults, setModelTestResults] = useState({}); const [testingModelId, setTestingModelId] = useState(null); const [testError, setTestError] = useState(""); const [showAddCustomModel, setShowAddCustomModel] = useState(false); const [connections, setConnections] = useState([]); const providerAlias = providerAliasOverride || getProviderAlias(providerId); const effectiveType = kindFilter || "llm"; const fetchData = useCallback(async () => { try { const [aliasRes, connRes, customRes] = await Promise.all([ fetch("/api/models/alias"), fetch("/api/providers", { cache: "no-store" }), fetch("/api/models/custom", { cache: "no-store" }), ]); const aliasData = await aliasRes.json(); const connData = await connRes.json(); const customData = await customRes.json(); if (aliasRes.ok) setModelAliases(aliasData.aliases || {}); if (connRes.ok) setConnections((connData.connections || []).filter((c) => c.provider === providerId)); if (customRes.ok) setCustomModels(customData.models || []); } catch (e) { console.log("ModelsCard fetch error:", e); } }, [providerId]); useEffect(() => { fetchData(); }, [fetchData]); const handleSetAlias = async (modelId, alias) => { const fullModel = `${providerAlias}/${modelId}`; try { const res = await fetch("/api/models/alias", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: fullModel, alias }), }); if (res.ok) await fetchData(); } catch (e) { console.log("set alias error:", e); } }; const handleDeleteAlias = async (alias) => { try { const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { method: "DELETE" }); if (res.ok) await fetchData(); } catch (e) { console.log("delete alias error:", e); } }; const handleAddCustomModel = async (modelId) => { try { const res = await fetch("/api/models/custom", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerAlias, id: modelId, type: effectiveType }), }); if (res.ok) { await fetchData(); window.dispatchEvent(new CustomEvent("customModelChanged")); } } catch (e) { console.log("add custom model error:", e); } }; const handleDeleteCustomModel = async (modelId) => { try { const params = new URLSearchParams({ providerAlias, id: modelId, type: effectiveType }); const res = await fetch(`/api/models/custom?${params}`, { method: "DELETE" }); if (res.ok) { await fetchData(); window.dispatchEvent(new CustomEvent("customModelChanged")); } } catch (e) { console.log("delete custom model error:", e); } }; const handleTestModel = async (modelId) => { if (testingModelId) return; setTestingModelId(modelId); try { const res = await fetch("/api/models/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: `${providerAlias}/${modelId}`, kind: kindFilter }), }); const data = await res.json(); setModelTestResults((prev) => ({ ...prev, [modelId]: data.ok ? "ok" : "error" })); setTestError(data.ok ? "" : (data.error || "Model not reachable")); } catch { setModelTestResults((prev) => ({ ...prev, [modelId]: "error" })); setTestError("Network error"); } finally { setTestingModelId(null); } }; // Built-in models — filter by kindFilter if provided const allBuiltIn = getModelsByProviderId(providerId); const builtInModels = kindFilter ? allBuiltIn.filter((m) => { if (m.kinds) return m.kinds.includes(kindFilter); return getModelKind(m, "llm") === kindFilter; }) : allBuiltIn; // Custom models for this provider + kind, dedupe vs built-in const myCustomModels = customModels.filter( (m) => m.providerAlias === providerAlias && getModelKind(m, "llm") === effectiveType && !builtInModels.some((b) => b.id === m.id) ); const displayModels = builtInModels; return ( <>

Models{kindFilter ? ` — ${kindFilter.toUpperCase()}` : ""}

{testError &&

{testError}

}
{displayModels.map((model) => { const fullModel = `${providerAlias}/${model.id}`; const existingAlias = Object.entries(modelAliases).find(([, m]) => m === fullModel)?.[0]; return ( handleSetAlias(model.id, alias)} onDeleteAlias={() => handleDeleteAlias(existingAlias)} testStatus={modelTestResults[model.id]} onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isFree={model.isFree} /> ); })} {myCustomModels.map((model) => ( {}} onDeleteAlias={() => handleDeleteCustomModel(model.id)} testStatus={modelTestResults[model.id]} onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isCustom /> ))}
{ await handleAddCustomModel(modelId); setShowAddCustomModel(false); }} onClose={() => setShowAddCustomModel(false)} /> ); } ModelsCard.propTypes = { providerId: PropTypes.string.isRequired, kindFilter: PropTypes.string, // e.g. "tts", "embedding" — filters models shown providerAliasOverride: PropTypes.string, // override alias (e.g. for custom-embedding nodes using prefix) };