"use client"; /** * FallbackChainsEditor — Batch D * * Editor for model fallback chains. Each chain maps a model name * to a prioritized list of providers that can serve it. * API: /api/fallback/chains */ import { useState, useEffect, useCallback } from "react"; import { Card, Button, Input, EmptyState } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; import { useTranslations } from "next-intl"; const CHAIN_COLORS = [ "#6366f1", "#22c55e", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899", "#14b8a6", ]; export default function FallbackChainsEditor() { const [chains, setChains] = useState({}); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); const [newModel, setNewModel] = useState(""); const [newProviders, setNewProviders] = useState(""); const [saving, setSaving] = useState(false); const notify = useNotificationStore(); const t = useTranslations("settings"); const tc = useTranslations("common"); const fetchChains = useCallback(async () => { try { const res = await fetch("/api/fallback/chains"); if (res.ok) { const data = await res.json(); setChains(data.chains || data || {}); } } catch { // silent } finally { setLoading(false); } }, []); useEffect(() => { fetchChains(); }, [fetchChains]); const handleCreate = async () => { if (!newModel.trim() || !newProviders.trim()) { notify.warning(t("fillModelAndProviders")); return; } const providers = newProviders .split(",") .map((p) => p.trim()) .filter(Boolean) .map((provider, i) => ({ provider, priority: i + 1, enabled: true })); if (providers.length === 0) { notify.warning(t("addAtLeastOneProvider")); return; } setSaving(true); try { const res = await fetch("/api/fallback/chains", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: newModel.trim(), chain: providers }), }); if (res.ok) { notify.success(t("chainCreated", { model: newModel.trim() })); setNewModel(""); setNewProviders(""); setShowCreate(false); await fetchChains(); } else { notify.error(t("failedCreateChain")); } } catch { notify.error(t("failedCreateChain")); } finally { setSaving(false); } }; const handleDelete = async (model) => { if (!confirm(t("deleteChainConfirm", { model }))) return; try { const res = await fetch("/api/fallback/chains", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model }), }); if (res.ok) { notify.success(t("chainDeleted", { model })); await fetchChains(); } else { notify.error(t("failedDeleteChain")); } } catch { notify.error(t("failedDeleteChain")); } }; if (loading) { return ( timeline {t("loadingFallbackChains")} ); } const chainEntries = Object.entries(chains); return ( timeline {t("fallbackChainsTitle")} {t("fallbackChainsDesc")} setShowCreate(!showCreate)}> {showCreate ? tc("cancel") : t("addChain")} {/* Create Form */} {showCreate && ( setNewModel(e.target.value)} /> setNewProviders(e.target.value)} /> {t("createChain")} )} {/* Chains List */} {chainEntries.length === 0 ? ( ) : ( {chainEntries.map(([model, chain]) => ( {model} arrow_forward {(Array.isArray(chain) ? chain : []).map((entry, i) => ( {i + 1}. {entry.provider} ))} handleDelete(model)} className="text-text-muted hover:text-red-400 transition-colors ml-2" title={t("deleteChain")} > close ))} )} ); }
{t("fallbackChainsDesc")}