"use client" import { useEffect, useMemo, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { Controller, Resolver, useForm } from "react-hook-form" import { z } from "zod/v4" import { ProjectDialog } from "@/components/project-dialog" import { Button } from "@/components/ui/button" import { Field, FieldContent, FieldError, FieldLabel, } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { type AIConfig, type CreateAIConfigPayload, fetchAIConfig } from "@/lib/api/admin" import { AIModelType, AIProvider, } from "@/lib/generated/enums" import { useI18n } from "@/i18n/provider" import { OptionCombobox } from "./option-combobox" type AIConfigEditDialogProps = { open: boolean saving: boolean itemId: number | null onOpenChange: (open: boolean) => void onSubmit: (payload: CreateAIConfigPayload) => Promise } type TFunction = (key: string, values?: Record) => string function getProviderOptions(t: TFunction) { return [{ value: String(AIProvider.OpenAI), label: t("aiConfig.providerOpenAI") }] } function getModelTypeOptions(t: TFunction) { return [ { value: String(AIModelType.LLM), label: t("aiConfig.modelTypeLlm") }, { value: String(AIModelType.Embedding), label: t("aiConfig.modelTypeEmbedding") }, { value: String(AIModelType.Rerank), label: t("aiConfig.modelTypeRerank") }, ] } const emptyForm: EditForm = { name: "", provider: AIProvider.OpenAI, baseUrl: "", apiKey: "", modelType: AIModelType.LLM, modelName: "", dimension: "0", maxContextTokens: "0", maxOutputTokens: "0", timeoutMs: "120000", maxRetryCount: "0", rpmLimit: "0", tpmLimit: "0", remark: "", } type EditForm = { name: string provider: string baseUrl: string apiKey: string modelType: string modelName: string dimension: string maxContextTokens: string maxOutputTokens: string timeoutMs: string maxRetryCount: string rpmLimit: string tpmLimit: string remark: string } function buildForm(item: AIConfig | null): EditForm { if (!item) { return emptyForm } return { name: item.name, provider: item.provider, baseUrl: item.baseUrl, apiKey: item.apiKey, modelType: item.modelType, modelName: item.modelName, dimension: String(item.dimension), maxContextTokens: String(item.maxContextTokens), maxOutputTokens: String(item.maxOutputTokens), timeoutMs: String(item.timeoutMs), maxRetryCount: String(item.maxRetryCount), rpmLimit: String(item.rpmLimit), tpmLimit: String(item.tpmLimit), remark: item.remark ?? "", } } function buildPayload(form: EditForm): CreateAIConfigPayload { return { name: form.name.trim(), provider: form.provider, baseUrl: form.baseUrl.trim(), apiKey: form.apiKey.trim(), modelType: form.modelType, modelName: form.modelName.trim(), dimension: Number(form.dimension), maxContextTokens: Number(form.maxContextTokens), maxOutputTokens: Number(form.maxOutputTokens), timeoutMs: Number(form.timeoutMs), maxRetryCount: Number(form.maxRetryCount), rpmLimit: Number(form.rpmLimit), tpmLimit: Number(form.tpmLimit), remark: form.remark.trim(), } } export function EditDialog({ open, saving, itemId, onOpenChange, onSubmit, }: AIConfigEditDialogProps) { if (!open) { return null } return ( ) } type AIConfigEditDialogBodyProps = AIConfigEditDialogProps function AIConfigEditDialogBody({ open, saving, itemId, onOpenChange, onSubmit, }: AIConfigEditDialogBodyProps) { const formId = "ai-config-edit-form" const t = useI18n() const [loading, setLoading] = useState(false) const providerOptions = useMemo(() => getProviderOptions(t), [t]) const modelTypeOptions = useMemo(() => getModelTypeOptions(t), [t]) const aiConfigFormSchema = useMemo( () => z.object({ name: z.string().trim().min(1, t("aiConfig.nameRequired")), provider: z.string().trim().min(1, t("aiConfig.providerRequired")), baseUrl: z.string().trim().min(1, t("aiConfig.baseUrlRequired")), apiKey: z.string().trim(), modelType: z.string().trim().min(1, t("aiConfig.modelTypeRequired")), modelName: z.string().trim().min(1, t("aiConfig.modelNameRequired")), dimension: z.string().trim().regex(/^\d+$/, t("aiConfig.dimensionInvalid")), maxContextTokens: z.string().trim().regex(/^\d+$/, t("aiConfig.maxContextInvalid")), maxOutputTokens: z.string().trim().regex(/^\d+$/, t("aiConfig.maxOutputInvalid")), timeoutMs: z.string().trim().regex(/^\d+$/, t("aiConfig.timeoutInvalid")), maxRetryCount: z.string().trim().regex(/^\d+$/, t("aiConfig.retryInvalid")), rpmLimit: z.string().trim().regex(/^\d+$/, t("aiConfig.rpmInvalid")), tpmLimit: z.string().trim().regex(/^\d+$/, t("aiConfig.tpmInvalid")), remark: z.string().trim(), }), [t], ) const editFormResolver = useMemo( () => zodResolver(aiConfigFormSchema as never) as Resolver, [aiConfigFormSchema], ) const form = useForm({ resolver: editFormResolver, defaultValues: emptyForm, }) const { control, handleSubmit, reset, register, watch, formState: { errors }, } = form const modelType = watch("modelType") useEffect(() => { async function loadDetail() { if (!itemId) { reset(emptyForm) return } setLoading(true) try { const data = await fetchAIConfig(itemId) reset(buildForm(data)) } catch (error) { console.error("Failed to load AI config:", error) } finally { setLoading(false) } } void loadDetail() }, [itemId, reset]) async function onFormSubmit(values: EditForm) { await onSubmit(buildPayload(values)) } return ( } > {loading ? (
{t("aiConfig.loading")}
) : (
{t("aiConfig.name")}
{t("aiConfig.provider")} ( )} /> {t("aiConfig.modelType")} ( )} />
{t("aiConfig.baseUrl")} API Key
{t("aiConfig.modelName")} {t("aiConfig.dimensionLabel")}
{t("aiConfig.maxContextTokens")} {t("aiConfig.maxOutputTokens")}
{t("aiConfig.timeoutMs")} {t("aiConfig.maxRetryCount")}
{t("aiConfig.rpmLimit")} {t("aiConfig.tpmLimit")}
{t("aiConfig.remark")}