"use client" import { useCallback, useEffect, useMemo, useRef, useState, useTransition, type ReactElement } from "react" import { useTranslations } from "next-intl" import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" import { Loader2, Save, Sparkles, Trash2 } from "lucide-react" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Button } from "@/shared/components/ui/button" import { Checkbox } from "@/shared/components/ui/checkbox" import { Label } from "@/shared/components/ui/label" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/shared/components/ui/alert-dialog" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, } from "@/shared/components/ui/form" import { TextField } from "@/shared/components/form-fields/text-field" import { SelectField } from "@/shared/components/form-fields/select-field" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/shared/components/ui/select" import { Badge } from "@/shared/components/ui/badge" import { deleteAiProviderAction, getAiProviderSummaries, testAiProviderAction, upsertAiProviderAction, type AiProviderSummary } from "@/modules/settings/actions" const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"]) const VisibilitySchema = z.enum(["public", "private"]) const AiProviderFormSchema = z.object({ id: z.string().optional(), provider: ProviderSchema, baseUrl: z.string().optional(), model: z.string().min(1, "Model is required"), apiKey: z.string().optional(), isDefault: z.boolean().optional(), visibility: VisibilitySchema.optional(), }) type AiProviderFormValues = z.infer const NEW_PROVIDER_VALUE = "__new__" type AiProviderSettingsCardProps = { onProvidersChanged?: (rows: AiProviderSummary[]) => void initialMode?: "new" | "first" isAdmin?: boolean currentUserId?: string } export function AiProviderSettingsCard({ onProvidersChanged, initialMode = "first", isAdmin = false, currentUserId, }: AiProviderSettingsCardProps) { const t = useTranslations("settings.ai.providers") const [isPending, startTransition] = useTransition() const [providers, setProviders] = useState([]) const [selectedId, setSelectedId] = useState("") const [testStatus, setTestStatus] = useState<"idle" | "testing" | "passed" | "failed">("idle") const [lastTestedSignature, setLastTestedSignature] = useState("") const loadedRef = useRef(false) const form = useForm({ resolver: zodResolver(AiProviderFormSchema), defaultValues: { id: "", provider: "openai", baseUrl: "", model: "", apiKey: "", isDefault: false, visibility: "private", }, }) const selectedProvider = useMemo( () => providers.find((item) => item.id === selectedId) ?? null, [providers, selectedId] ) const buildSignature = useCallback((values: AiProviderFormValues) => { return JSON.stringify({ provider: values.provider, baseUrl: values.baseUrl?.trim() || "", model: values.model.trim(), apiKey: values.apiKey?.trim() || "", }) }, []) const resetToNew = useCallback(() => { setSelectedId("") setTestStatus("idle") setLastTestedSignature("") form.reset({ id: "", provider: "openai", baseUrl: "", model: "", apiKey: "", isDefault: false, visibility: "private", }) }, [form]) useEffect(() => { if (loadedRef.current) return loadedRef.current = true startTransition(async () => { try { const result = await getAiProviderSummaries() if (!result.success || !result.data) { toast.error(result.message ?? t("loadFailure")) return } const rows = result.data setProviders(rows) onProvidersChanged?.(rows) if (initialMode === "new") { resetToNew() return } if (rows.length > 0 && !selectedId) { const next = rows[0] setSelectedId(next.id) form.reset({ id: next.id, provider: next.provider, baseUrl: next.baseUrl ?? "", model: next.model, apiKey: "", isDefault: next.isDefault, visibility: next.visibility, }) } } catch { toast.error(t("loadFailure")) } }) }, [form, selectedId, onProvidersChanged, initialMode, resetToNew, t]) const handleSelectChange = (value: string) => { if (value === NEW_PROVIDER_VALUE) { resetToNew() return } setSelectedId(value) setTestStatus("idle") setLastTestedSignature("") const next = providers.find((item) => item.id === value) if (!next) return form.reset({ id: next.id, provider: next.provider, baseUrl: next.baseUrl ?? "", model: next.model, apiKey: "", isDefault: next.isDefault, visibility: next.visibility, }) } useEffect(() => { const subscription = form.watch(() => { if (!lastTestedSignature) return const currentSignature = buildSignature(form.getValues()) if (currentSignature !== lastTestedSignature) { setTestStatus("idle") } }) return () => subscription.unsubscribe() }, [form, buildSignature, lastTestedSignature]) const handleTest = () => { const values = form.getValues() const apiKey = values.apiKey?.trim() if (!apiKey && !values.id?.trim()) { toast.error(t("needKey")) return } setTestStatus("testing") startTransition(async () => { const payload = { id: values.id?.trim() || undefined, provider: values.provider, baseUrl: values.baseUrl?.trim() || undefined, model: values.model.trim(), apiKey: apiKey || undefined, isDefault: values.isDefault ?? false, visibility: values.visibility, } const result = await testAiProviderAction(payload) if (result.success) { setTestStatus("passed") setLastTestedSignature(buildSignature(values)) toast.success(result.message ?? t("testSuccess")) } else { setTestStatus("failed") toast.error(result.message ?? t("testFailure")) } }) } const onSubmit = (values: AiProviderFormValues) => { const signature = buildSignature(values) if (testStatus !== "passed" || signature !== lastTestedSignature) { toast.error(t("needTest")) return } startTransition(async () => { const payload = { id: values.id?.trim() || undefined, provider: values.provider, baseUrl: values.baseUrl?.trim() || undefined, model: values.model.trim(), apiKey: values.apiKey?.trim() || undefined, isDefault: values.isDefault ?? false, visibility: values.visibility, } const result = await upsertAiProviderAction(payload) if (result.success) { toast.success(result.message ?? t("saveSuccess")) setTestStatus("idle") setLastTestedSignature("") const summariesResult = await getAiProviderSummaries() if (!summariesResult.success || !summariesResult.data) { toast.error(summariesResult.message ?? t("loadFailure")) return } const rows = summariesResult.data setProviders(rows) onProvidersChanged?.(rows) const nextId = result.data ?? payload.id ?? "" setSelectedId(nextId) const next = rows.find((item) => item.id === nextId) if (next) { form.reset({ id: next.id, provider: next.provider, baseUrl: next.baseUrl ?? "", model: next.model, apiKey: "", isDefault: next.isDefault, visibility: next.visibility, }) } } else { toast.error(result.message ?? t("saveFailure")) } }) } const handleDelete = () => { const id = form.getValues("id") if (!id?.trim()) { toast.error(t("deleteNeedSelect")) return } startTransition(async () => { const result = await deleteAiProviderAction({ id: id.trim() }) if (result.success) { toast.success(result.message ?? t("deleteSuccess")) const summariesResult = await getAiProviderSummaries() if (summariesResult.success && summariesResult.data) { const rows = summariesResult.data setProviders(rows) onProvidersChanged?.(rows) if (rows.length > 0) { const next = rows[0] setSelectedId(next.id) form.reset({ id: next.id, provider: next.provider, baseUrl: next.baseUrl ?? "", model: next.model, apiKey: "", isDefault: next.isDefault, visibility: next.visibility, }) } else { resetToNew() } } else { resetToNew() } } else { toast.error(result.message ?? t("deleteFailure")) } }) } const renderProviderLabel = (item: AiProviderSummary): string => { const parts = [item.provider, "·", item.model] if (item.isDefault) parts.push(`(${t("setDefault")})`) return parts.join(" ") } const renderVisibilityBadge = (item: AiProviderSummary): ReactElement => { const isOwner = currentUserId !== undefined && item.createdBy === currentUserId return ( {item.visibility === "public" ? ( {t("badgePublic")} ) : ( {t("badgePrivate")} )} {isOwner ? ( {t("badgeOwner")} ) : null} ) } return ( {t("title")} {t("description")}
{selectedProvider ? renderVisibilityBadge(selectedProvider) : null} {selectedProvider?.apiKeyLast4 ? `${t("stored")} • ****${selectedProvider.apiKeyLast4}` : t("noKey")}
( {t("visibility")} {isAdmin ? t("visibilityDesc") : t("visibilityReadOnly")} )} /> ( field.onChange(value === true)} /> {t("setDefault")} )} /> {t("deleteConfirmTitle")} {t("deleteConfirmDescription")} {t("deleteCancel")} {t("deleteConfirm")}
) }