"use client"; import { useState, useCallback, useEffect } from "react"; import PropTypes from "prop-types"; import { Card, Button, Modal } from "@/shared/components"; import { getModelsByProviderId } 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} {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 }) { const { copied, copy } = useCopyToClipboard(); const [modelAliases, setModelAliases] = 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 = getProviderAlias(providerId); const fetchData = useCallback(async () => { try { const [aliasRes, connRes] = await Promise.all([ fetch("/api/models/alias"), fetch("/api/providers", { cache: "no-store" }), ]); const aliasData = await aliasRes.json(); const connData = await connRes.json(); if (aliasRes.ok) setModelAliases(aliasData.aliases || {}); if (connRes.ok) setConnections((connData.connections || []).filter((c) => c.provider === providerId)); } 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 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); } }; // Get models — filter by kindFilter if provided const allModels = getModelsByProviderId(providerId); const displayModels = kindFilter ? allModels.filter((m) => { if (m.kinds) return m.kinds.includes(kindFilter); if (m.type) return m.type === kindFilter; return kindFilter === "llm"; }) : allModels; // Custom models added via alias const customModels = Object.entries(modelAliases) .filter(([alias, fullModel]) => { const prefix = `${providerAlias}/`; if (!fullModel.startsWith(prefix)) return false; const modelId = fullModel.slice(prefix.length); return !displayModels.some((m) => m.id === modelId) && alias === modelId; }) .map(([alias, fullModel]) => ({ id: fullModel.slice(`${providerAlias}/`.length), alias, })); 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} /> ); })} {customModels.map((model) => ( {}} onDeleteAlias={() => handleDeleteAlias(model.alias)} testStatus={modelTestResults[model.id]} onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isCustom /> ))}
{ await handleSetAlias(modelId, modelId); setShowAddCustomModel(false); }} onClose={() => setShowAddCustomModel(false)} /> ); } ModelsCard.propTypes = { providerId: PropTypes.string.isRequired, kindFilter: PropTypes.string, // e.g. "tts", "embedding" — filters models shown };