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