"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, InfoIcon, PlusIcon, Trash2Icon, } from "lucide-react"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { Controller, Resolver, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod/v4"; import { OptionCombobox } from "@/components/option-combobox"; import { ProjectDialog } from "@/components/project-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Field, FieldContent, FieldError, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverDescription, PopoverTrigger, } from "@/components/ui/popover"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { fetchAIAgent, fetchAIConfigsAll, fetchAgentTeamsAll, fetchKnowledgeBasesAll, fetchMCPCatalog, fetchSkillDefinitionsAll, type AIAgent, type AIConfig, type AdminAgentTeam, type CreateAIAgentPayload, type KnowledgeBase, type MCPToolCatalogItem, type MCPToolSourceType, type SkillDefinition, } from "@/lib/api/admin"; import { useI18n } from "@/i18n/provider"; import { AIAgentFallbackMode, AIAgentHandoffMode, AIModelType, IMConversationServiceMode, Status, } from "@/lib/generated/enums"; type DirectToolItem = CreateAIAgentPayload["directTools"][number]; type DirectToolOption = { value: string; label: string; meta: DirectToolItem; sourceType: MCPToolSourceType; autoInjected: boolean; groupLabel: string; }; type GraphToolOption = { value: string; label: string; }; type EditDialogProps = { open: boolean; saving: boolean; itemId: number | null; onOpenChange: (open: boolean) => void; onSubmit: (payload: CreateAIAgentPayload) => Promise; }; type TFunction = (key: string, values?: Record) => string; type EditForm = { name: string; description: string; aiConfigId: string; serviceMode: string; systemPrompt: string; welcomeMessage: string; replyTimeoutSeconds: number; handoffMode: string; fallbackMode: string; fallbackMessage: string; }; function getServiceModeOptions(t: TFunction) { return [ { value: String(IMConversationServiceMode.AIOnly), label: t("aiAgent.serviceAiOnly") }, { value: String(IMConversationServiceMode.HumanOnly), label: t("aiAgent.serviceHumanOnly") }, { value: String(IMConversationServiceMode.AIFirst), label: t("aiAgent.serviceAiFirst") }, ]; } function getHandoffModeOptions(t: TFunction) { return [ { value: String(AIAgentHandoffMode.WaitPool), label: t("aiAgent.handoffWaitPool") }, { value: String(AIAgentHandoffMode.DefaultTeamPool), label: t("aiAgent.handoffDefaultTeamPool") }, { value: String(AIAgentHandoffMode.AIHoldAndNotify), label: t("aiAgent.handoffAiHoldAndNotify") }, ]; } function getFallbackModeOptions(t: TFunction) { return [ { value: String(AIAgentFallbackMode.NoAnswer), label: t("aiAgent.fallbackNoAnswer") }, { value: String(AIAgentFallbackMode.SuggestRetry), label: t("aiAgent.fallbackSuggestRetry") }, ]; } function buildForm(item: AIAgent | null): EditForm { if (!item) { return { name: "", description: "", aiConfigId: "", serviceMode: String(IMConversationServiceMode.AIFirst), systemPrompt: "", welcomeMessage: "", replyTimeoutSeconds: 180, handoffMode: String(AIAgentHandoffMode.WaitPool), fallbackMode: String(AIAgentFallbackMode.NoAnswer), fallbackMessage: "", }; } return { name: item.name, description: item.description || "", aiConfigId: item.aiConfigId > 0 ? String(item.aiConfigId) : "", serviceMode: String(item.serviceMode), systemPrompt: item.systemPrompt || "", welcomeMessage: item.welcomeMessage || "", replyTimeoutSeconds: item.replyTimeoutSeconds ?? 180, handoffMode: String(item.handoffMode), fallbackMode: String(item.fallbackMode), fallbackMessage: item.fallbackMessage || "", }; } function buildPayload( form: EditForm, knowledgeIds: number[], teamIds: number[], skillIds: number[], directTools: CreateAIAgentPayload["directTools"], graphTools: CreateAIAgentPayload["graphTools"], ): CreateAIAgentPayload { return { name: form.name.trim(), description: form.description.trim(), aiConfigId: Number(form.aiConfigId), serviceMode: Number(form.serviceMode), systemPrompt: form.systemPrompt.trim(), welcomeMessage: form.welcomeMessage.trim(), replyTimeoutSeconds: Number(form.replyTimeoutSeconds), teamIds, handoffMode: Number(form.handoffMode), fallbackMode: Number(form.fallbackMode), fallbackMessage: form.fallbackMessage.trim(), knowledgeIds, skillIds, directTools, graphTools, }; } export function EditDialog({ open, saving, itemId, onOpenChange, onSubmit, }: EditDialogProps) { if (!open) { return null; } return ( ); } function EditDialogBody({ open, saving, itemId, onOpenChange, onSubmit, }: EditDialogProps) { const formId = "ai-agent-edit-form"; const t = useI18n(); const [loading, setLoading] = useState(false); const schema = useMemo( () => z.object({ name: z.string().trim().min(1, t("aiAgent.nameRequired")), description: z.string().trim(), aiConfigId: z.string().trim().regex(/^\d+$/, t("aiAgent.aiConfigRequired")), serviceMode: z.string().trim().min(1, t("aiAgent.serviceModeRequired")), systemPrompt: z.string().trim(), welcomeMessage: z.string().trim(), replyTimeoutSeconds: z .number() .min(0, t("aiAgent.replyTimeoutInvalid")), handoffMode: z.string().trim().min(1, t("aiAgent.handoffModeRequired")), fallbackMode: z.string().trim().min(1, t("aiAgent.fallbackModeRequired")), fallbackMessage: z.string().trim(), }), [t], ); const resolver = useMemo( () => zodResolver(schema as never) as Resolver, [schema], ); const serviceModeOptions = useMemo(() => getServiceModeOptions(t), [t]); const handoffModeOptions = useMemo(() => getHandoffModeOptions(t), [t]); const fallbackModeOptions = useMemo(() => getFallbackModeOptions(t), [t]); const form = useForm({ resolver, defaultValues: buildForm(null), }); const { control, handleSubmit, register, reset, watch, formState: { errors }, } = form; const [selectedKnowledgeIds, setSelectedKnowledgeIds] = useState( [], ); const [selectedTeamIds, setSelectedTeamIds] = useState([]); const [selectedSkillIds, setSelectedSkillIds] = useState([]); const [knowledgeToAdd, setKnowledgeToAdd] = useState(""); const [teamToAdd, setTeamToAdd] = useState(""); const [skillToAdd, setSkillToAdd] = useState(""); const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState(""); const [directToolToAdd, setDirectToolToAdd] = useState(""); const [graphToolToAdd, setGraphToolToAdd] = useState(""); const [aiConfigs, setAIConfigs] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [agentTeams, setAgentTeams] = useState([]); const [skills, setSkills] = useState([]); const [directTools, setDirectTools] = useState([]); const [graphTools, setGraphTools] = useState([]); const [directToolOptions, setDirectToolOptions] = useState( [], ); const [graphToolOptions, setGraphToolOptions] = useState( [], ); const [toolCatalog, setToolCatalog] = useState([]); useEffect(() => { async function loadDetail() { if (!itemId) { reset(buildForm(null)); setSelectedKnowledgeIds([]); setSelectedTeamIds([]); setSelectedSkillIds([]); setDirectTools([]); setGraphTools([]); setKnowledgeToAdd(""); setTeamToAdd(""); setSkillToAdd(""); setDirectToolGroupToAdd(""); setDirectToolToAdd(""); setGraphToolToAdd(""); return; } setLoading(true); try { const data = await fetchAIAgent(itemId); reset(buildForm(data)); setSelectedKnowledgeIds(data.knowledgeIds ?? []); setSelectedTeamIds((data.teams ?? []).map((team) => team.id)); setSelectedSkillIds(data.skillIds ?? []); setDirectTools(data.directTools ?? []); setGraphTools(data.graphTools ?? []); setKnowledgeToAdd(""); setTeamToAdd(""); setSkillToAdd(""); setDirectToolGroupToAdd(""); setDirectToolToAdd(""); setGraphToolToAdd(""); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.loadDetailFailed"), ); } finally { setLoading(false); } } void loadDetail(); }, [itemId, reset, t]); useEffect(() => { async function loadAIConfigs() { try { const data = await fetchAIConfigsAll({ modelType: AIModelType.LLM, }); setAIConfigs(data); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.loadAiConfigFailed"), ); } } void loadAIConfigs(); }, [t]); useEffect(() => { async function loadAgentTeams() { try { const data = await fetchAgentTeamsAll(); setAgentTeams(data); } catch (error) { toast.error(error instanceof Error ? error.message : t("aiAgent.loadTeamsFailed")); } } void loadAgentTeams(); }, [t]); useEffect(() => { async function loadKnowledgeBases() { try { const data = await fetchKnowledgeBasesAll({ status: Status.Ok, }); setKnowledgeBases(data); } catch (error) { toast.error(error instanceof Error ? error.message : t("aiAgent.loadKnowledgeFailed")); } } void loadKnowledgeBases(); }, [t]); useEffect(() => { async function loadSkills() { try { const data = await fetchSkillDefinitionsAll({ status: Status.Ok, }); setSkills(data); } catch (error) { toast.error(error instanceof Error ? error.message : t("aiAgent.loadSkillsFailed")); } } void loadSkills(); }, [t]); useEffect(() => { async function loadDirectToolOptions() { try { const catalog = await fetchMCPCatalog(); setToolCatalog(catalog); setDirectToolOptions( catalog .filter((tool) => !tool.autoInjected && tool.sourceType === "mcp") .map((tool) => ({ value: tool.toolCode, label: `${tool.title || tool.toolName} · ${tool.toolCode}`, sourceType: tool.sourceType, autoInjected: tool.autoInjected, groupLabel: tool.sourceType === "builtin" ? t("aiAgent.builtinTools") : tool.serverCode, meta: { toolCode: tool.toolCode, serverCode: tool.serverCode, toolName: tool.toolName, title: tool.title || tool.toolName, description: tool.description || "", arguments: undefined, }, })), ); setGraphToolOptions( catalog .filter((tool) => tool.sourceType === "graph") .map((tool) => ({ value: tool.toolCode, label: `${tool.title || tool.toolName} · ${tool.toolCode}`, })), ); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.loadDirectToolsFailed"), ); } } void loadDirectToolOptions(); }, [t]); const aiConfigOptions = useMemo( () => aiConfigs.map((item) => ({ value: String(item.id), label: `${item.name} · ${item.modelName}`, })), [aiConfigs], ); const teamOptions = useMemo( () => agentTeams.map((item: AdminAgentTeam) => ({ value: String(item.id), label: item.name, })), [agentTeams], ); const knowledgeOptions = useMemo( () => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name, })), [knowledgeBases], ); const skillOptions = useMemo( () => skills.map((item) => ({ value: String(item.id), label: item.name, })), [skills], ); const addableKnowledgeOptions = useMemo( () => knowledgeOptions.filter( (option) => !selectedKnowledgeIds.includes(Number(option.value)), ), [knowledgeOptions, selectedKnowledgeIds], ); const selectedKnowledgeOptions = useMemo( () => selectedKnowledgeIds .map((id) => knowledgeOptions.find((option) => Number(option.value) === id), ) .filter( (option): option is { value: string; label: string } => !!option, ), [knowledgeOptions, selectedKnowledgeIds], ); const addableSkillOptions = useMemo( () => skillOptions.filter( (option) => !selectedSkillIds.includes(Number(option.value)), ), [selectedSkillIds, skillOptions], ); const selectedSkillOptions = useMemo( () => selectedSkillIds .map((id) => skillOptions.find((option) => Number(option.value) === id)) .filter( (option): option is { value: string; label: string } => !!option, ), [selectedSkillIds, skillOptions], ); const addableDirectToolOptions = useMemo( () => directToolOptions.filter( (option) => option.groupLabel === directToolGroupToAdd && !directTools.some((tool) => tool.toolCode === option.value), ), [directToolOptions, directToolGroupToAdd, directTools], ); const directToolGroupOptions = useMemo( () => Array.from( new Map( directToolOptions.map((option) => [ option.groupLabel, { value: option.groupLabel, label: option.groupLabel, }, ]), ).values(), ), [directToolOptions], ); const directToolsGrouped = useMemo(() => { const groups = new Map(); for (const tool of directTools) { const groupLabel = tool.serverCode || t("aiAgent.ungrouped"); const current = groups.get(groupLabel) ?? []; current.push(tool); groups.set(groupLabel, current); } return Array.from(groups.entries()); }, [directTools, t]); const addableGraphToolOptions = useMemo( () => graphToolOptions.filter( (option) => !graphTools.includes(option.value), ), [graphToolOptions, graphTools], ); const addableTeamOptions = useMemo( () => teamOptions.filter( (option) => !selectedTeamIds.includes(Number(option.value)), ), [teamOptions, selectedTeamIds], ); const selectedTeamOptions = useMemo( () => selectedTeamIds .map((id) => teamOptions.find((option) => Number(option.value) === id)) .filter( (option): option is { value: string; label: string } => !!option, ), [selectedTeamIds, teamOptions], ); const handoffMode = watch("handoffMode"); const selectedHandoffModeLabel = handoffModeOptions.find((item) => item.value === handoffMode)?.label ?? t("aiAgent.notSelected"); async function onFormSubmit(values: EditForm) { await onSubmit( buildPayload( values, selectedKnowledgeIds, selectedTeamIds, selectedSkillIds, directTools, graphTools, ), ); } function handleAddTeam(value: string) { const id = Number(value); if (!Number.isFinite(id) || id <= 0 || selectedTeamIds.includes(id)) { return; } setSelectedTeamIds((prev) => [...prev, id]); setTeamToAdd(""); } function handleRemoveTeam(id: number) { setSelectedTeamIds((prev) => prev.filter((item) => item !== id)); } function handleAddKnowledge(value: string) { const id = Number(value); if (!Number.isFinite(id) || id <= 0 || selectedKnowledgeIds.includes(id)) { return; } setSelectedKnowledgeIds((prev) => [...prev, id]); setKnowledgeToAdd(""); } function handleMoveKnowledge(index: number, direction: -1 | 1) { const targetIndex = index + direction; if (targetIndex < 0 || targetIndex >= selectedKnowledgeIds.length) { return; } setSelectedKnowledgeIds((prev) => { const next = [...prev]; const current = next[index]; next[index] = next[targetIndex]; next[targetIndex] = current; return next; }); } function handleRemoveKnowledge(id: number) { setSelectedKnowledgeIds((prev) => prev.filter((item) => item !== id)); } function handleAddSkill(value: string) { const id = Number(value); if (!Number.isFinite(id) || id <= 0 || selectedSkillIds.includes(id)) { return; } setSelectedSkillIds((prev) => [...prev, id]); setSkillToAdd(""); } function handleRemoveSkill(id: number) { setSelectedSkillIds((prev) => prev.filter((item) => item !== id)); } function handleAddDirectTool(value: string) { const option = directToolOptions.find((item) => item.value === value); if (!option) { return; } setDirectTools((prev) => { if ( prev.some( (item) => item.toolCode === option.meta.toolCode, ) ) { return prev; } return [...prev, option.meta]; }); setDirectToolGroupToAdd(option.groupLabel); setDirectToolToAdd(""); } function handleRemoveDirectTool(value: string) { setDirectTools((prev) => prev.filter((item) => item.toolCode !== value)); } function handleAddGraphTool(value: string) { if (!value || graphTools.includes(value)) { return; } setGraphTools((prev) => [...prev, value]); setGraphToolToAdd(""); } function handleRemoveGraphTool(value: string) { setGraphTools((prev) => prev.filter((item) => item !== value)); } return ( } > {loading ? (
{t("aiAgent.loading")}
) : (
{t("aiAgent.name")} {t("aiAgent.aiConfig")} ( )} /> {t("aiAgent.columnServiceMode")} ( )} />
{t("aiAgent.description")}