"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 (
);
}
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)
};