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