"use client";
import { useParams, notFound, useRouter } from "next/navigation";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Card, Badge, Button, Toggle, AddCustomEmbeddingModal } from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, getProvidersByKind } from "@/shared/constants/providers";
// Kinds that support combos (currently disabled for image/tts — temporarily hidden).
// webSearch/webFetch handled by /web page.
const COMBO_KINDS = new Set([]);
const COMBO_BASE_NAMES = { image: "image-combo", tts: "tts-combo" };
function getEffectiveStatus(conn) {
const isCooldown = Object.entries(conn).some(
([k, v]) => k.startsWith("modelLock_") && v && new Date(v).getTime() > Date.now()
);
return conn.testStatus === "unavailable" && !isCooldown ? "active" : conn.testStatus;
}
function MediaProviderCard({ provider, kind, connections, isCustom, onToggle }) {
const providerInfo = AI_PROVIDERS[provider.id];
const isNoAuth = !!providerInfo?.noAuth;
const providerConns = connections.filter((c) => c.provider === provider.id);
const connected = providerConns.filter((c) => { const s = getEffectiveStatus(c); return s === "active" || s === "success"; }).length;
const error = providerConns.filter((c) => { const s = getEffectiveStatus(c); return s === "error" || s === "expired" || s === "unavailable"; }).length;
const total = providerConns.length;
const allDisabled = total > 0 && providerConns.every((c) => c.isActive === false);
const handleToggleClick = (e) => {
e.preventDefault();
e.stopPropagation();
if (onToggle) onToggle(provider.id, allDisabled);
};
const renderStatus = () => {
if (isNoAuth) return Ready;
if (allDisabled) return Disabled;
if (total === 0) return No connections;
return (
<>
{connected > 0 && {connected} Connected}
{error > 0 && {error} Error}
{connected === 0 && error === 0 && {total} Added}
>
);
};
return (
7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
>
{provider.name}
{isCustom && Custom}
{renderStatus()}
{total > 0 && (
{}}
title={allDisabled ? "Enable provider" : "Disable provider"}
/>
)}
);
}
function ComboList({ combos }) {
if (combos.length === 0) return null;
return (
{combos.map((combo) => (
layers
{combo.name}
{combo.models.slice(0, 6).map((entry, i) => {
const pid = typeof entry === "string" ? entry.split("/")[0] : "";
const p = AI_PROVIDERS[pid];
return (
);
})}
{combo.models.length > 6 && (
+{combo.models.length - 6}
)}
{combo.models.length}
chevron_right
))}
);
}
export default function MediaProviderKindPage() {
const { kind } = useParams();
const router = useRouter();
const [connections, setConnections] = useState([]);
const [customNodes, setCustomNodes] = useState([]);
const [combos, setCombos] = useState([]);
const [showAddCustomEmbedding, setShowAddCustomEmbedding] = useState(false);
// webSearch/webFetch listing pages are merged into /web
useEffect(() => {
if (kind === "webSearch" || kind === "webFetch") {
router.replace("/dashboard/media-providers/web");
}
}, [kind, router]);
const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind);
const isEmbedding = kind === "embedding";
const supportsCombo = COMBO_KINDS.has(kind);
useEffect(() => {
if (!kindConfig) return;
fetch("/api/providers", { cache: "no-store" })
.then((r) => r.json())
.then((d) => setConnections(d.connections || []))
.catch(() => {});
if (isEmbedding) {
fetch("/api/provider-nodes", { cache: "no-store" })
.then((r) => r.json())
.then((d) => setCustomNodes((d.nodes || []).filter((n) => n.type === "custom-embedding")))
.catch(() => {});
}
if (supportsCombo) {
fetch("/api/combos", { cache: "no-store" })
.then((r) => r.json())
.then((d) => setCombos(d.combos || []))
.catch(() => {});
}
}, [isEmbedding, supportsCombo, kindConfig]);
if (!kindConfig) return notFound();
const providers = getProvidersByKind(kind);
const kindCombos = combos.filter((c) => c.kind === kind);
// Map custom nodes to MediaProviderCard shape
const customProviders = customNodes.map((n) => ({
id: n.id,
name: n.name || "Custom Embedding",
color: "#6366F1",
textIcon: "CE",
}));
const allProviders = [...providers, ...customProviders];
const handleToggleProvider = async (providerId, newActive) => {
const providerConns = connections.filter((c) => c.provider === providerId);
setConnections((prev) =>
prev.map((c) => (c.provider === providerId ? { ...c, isActive: newActive } : c))
);
await Promise.allSettled(
providerConns.map((c) =>
fetch(`/api/providers/${c.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isActive: newActive }),
})
)
);
};
const handleCreateCombo = async () => {
const base = COMBO_BASE_NAMES[kind] || `${kind}-combo`;
let name = base;
let i = 1;
const existing = new Set(combos.map((c) => c.name));
while (existing.has(name)) { name = `${base}-${i++}`; }
const res = await fetch("/api/combos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, models: [], kind }),
});
if (res.ok) {
const created = await res.json();
router.push(`/dashboard/media-providers/combo/${created.id}`);
} else {
const err = await res.json();
alert(err.error || "Failed to create combo");
}
};
return (
{(isEmbedding || supportsCombo) && (
{supportsCombo && (
)}
{isEmbedding && (
)}
)}
{supportsCombo && kindCombos.length > 0 && (
)}
{allProviders.length === 0 ? (
No providers support {kindConfig.label} yet.
) : (
{providers.map((provider) => (
))}
{customProviders.map((provider) => (
))}
)}
{isEmbedding && (
setShowAddCustomEmbedding(false)}
onCreated={(node) => {
setCustomNodes((prev) => [...prev, node]);
setShowAddCustomEmbedding(false);
}}
/>
)}
);
}