Files
9router/src/app/(dashboard)/dashboard/media-providers/[kind]/page.js
2026-05-03 14:55:43 +07:00

167 lines
6.1 KiB
JavaScript

"use client";
import { useParams, notFound, useRouter } from "next/navigation";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Card, Badge, Button, AddCustomEmbeddingModal } from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, getProvidersByKind } from "@/shared/constants/providers";
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 }) {
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 renderStatus = () => {
if (isNoAuth) return <Badge variant="success" size="sm">Ready</Badge>;
if (allDisabled) return <Badge variant="default" size="sm">Disabled</Badge>;
if (total === 0) return <span className="text-xs text-text-muted">No connections</span>;
return (
<>
{connected > 0 && <Badge variant="success" size="sm" dot>{connected} Connected</Badge>}
{error > 0 && <Badge variant="error" size="sm" dot>{error} Error</Badge>}
{connected === 0 && error === 0 && <Badge variant="default" size="sm">{total} Added</Badge>}
</>
);
};
return (
<Link href={`/dashboard/media-providers/${kind}/${provider.id}`} className="group">
<Card
padding="xs"
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
>
<div className="flex min-w-0 items-center gap-3">
<div
className="size-8 rounded-lg flex items-center justify-center shrink-0"
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
>
<ProviderIcon
src={`/providers/${provider.id}.png`}
alt={provider.name}
size={30}
className="object-contain rounded-lg max-w-[30px] max-h-[30px]"
fallbackText={provider.textIcon || provider.id.slice(0, 2).toUpperCase()}
fallbackColor={provider.color}
/>
</div>
<div>
<h3 className="font-semibold text-sm">{provider.name}</h3>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
{isCustom && <Badge variant="default" size="sm">Custom</Badge>}
{renderStatus()}
</div>
</div>
</div>
</Card>
</Link>
);
}
export default function MediaProviderKindPage() {
const { kind } = useParams();
const router = useRouter();
const [connections, setConnections] = useState([]);
const [customNodes, setCustomNodes] = 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";
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(() => {});
}
}, [isEmbedding, kindConfig]);
if (!kindConfig) return notFound();
const providers = getProvidersByKind(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];
return (
<div className="flex flex-col gap-6">
{isEmbedding && (
<div className="flex items-center justify-end">
<Button size="sm" icon="add" onClick={() => setShowAddCustomEmbedding(true)}>
Add Custom Embedding
</Button>
</div>
)}
{allProviders.length === 0 ? (
<div className="text-center py-12 border border-dashed border-border rounded-xl text-text-muted text-sm">
No providers support <strong>{kindConfig.label}</strong> yet.
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{providers.map((provider) => (
<MediaProviderCard
key={provider.id}
provider={provider}
kind={kind}
connections={connections}
/>
))}
{customProviders.map((provider) => (
<MediaProviderCard
key={provider.id}
provider={provider}
kind={kind}
connections={connections}
isCustom
/>
))}
</div>
)}
{isEmbedding && (
<AddCustomEmbeddingModal
isOpen={showAddCustomEmbedding}
onClose={() => setShowAddCustomEmbedding(false)}
onCreated={(node) => {
setCustomNodes((prev) => [...prev, node]);
setShowAddCustomEmbedding(false);
}}
/>
)}
</div>
);
}