"use client"; import Link from "next/link"; import { useEffect, useMemo, useRef, useState } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowLeft, ChevronDown, ChevronRight, GripVertical, Image as ImageIcon, Layers, Loader2, MonitorPlay, Package, Plus, Save, Search, Settings2, Sparkles, Trash2, Upload, UserCircle, Video, } from "lucide-react"; import { AGENT_WORKFLOW_MAX_TREE_DEPTH, countAgentWorkflowOptions, countAgentWorkflowSteps, defaultAgentWorkflowUiConfig, findAgentWorkflowStepByKey, flattenAgentWorkflowSteps, normalizeAgentWorkflowStepOrdering, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; import { defaultAgentWorkflowBrandModelConfig, normalizeAgentWorkflowBrandModelConfig, } from "@/lib/agents/workflow-brand-model-config"; import { defaultAgentWorkflowShowcaseConfig, normalizeAgentWorkflowShowcaseConfig, } from "@/lib/agents/workflow-showcase-config"; import { defaultAgentWorkflowMaterialApiConfig, normalizeAgentWorkflowMaterialApiConfig, } from "@/lib/agents/workflow-material-config"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import type { AgentWorkflowBrandModelConfig, AgentWorkflowMaterialApiConfig, AgentWorkflowOptionUiConfig, AgentWorkflowShowcaseConfig, AgentWorkflowStepUiConfig, AgentWorkflowUiConfig, } from "@/types/agent"; const fetcher = (url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json()); // Animation variants const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05 } } }; const itemVariants = { hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0 } }; const WORKFLOW_LEVEL_LABELS = ["一级模块", "二级模块", "三级模块"]; function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig { return { ...config, steps: normalizeAgentWorkflowStepOrdering(config.steps), }; } function createWorkflowSlug(value: string, fallback: string) { const normalized = value .trim() .replace(/\s+/g, "_") .replace(/[^\w-]/g, "_") .replace(/_+/g, "_") .replace(/^_+|_+$/g, "") .toLowerCase(); return normalized || fallback; } function ensureUniqueWorkflowKey(baseKey: string, existingKeys: string[]) { const normalizedBase = createWorkflowSlug(baseKey, "item"); const keySet = new Set(existingKeys); if (!keySet.has(normalizedBase)) { return normalizedBase; } let suffix = 2; let candidate = `${normalizedBase}_${suffix}`; while (keySet.has(candidate)) { suffix += 1; candidate = `${normalizedBase}_${suffix}`; } return candidate; } function createWorkflowOptionDraft( stepKey: string, optionIndex: number, existingOptionKeys: string[] ): AgentWorkflowOptionUiConfig { const optionKey = ensureUniqueWorkflowKey( `${stepKey}_option_${optionIndex + 1}`, existingOptionKeys ); return { key: optionKey, buttonName: `选项${optionIndex + 1}`, description: "", previewImageUrl: "", enabled: true, sortOrder: optionIndex, promptFragment: `请按选项${optionIndex + 1}执行。`, }; } function createWorkflowStepDraft( depth: number, stepIndex: number, existingStepKeys: string[] ): AgentWorkflowStepUiConfig { const levelLabel = WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, 2))] || "模块"; const stepKey = ensureUniqueWorkflowKey(`step_${depth}_${stepIndex + 1}`, existingStepKeys); return { key: stepKey, title: `新${levelLabel}${stepIndex + 1}`, description: "", enabled: true, sortOrder: stepIndex, options: [createWorkflowOptionDraft(stepKey, 0, [])], children: [], }; } function updateWorkflowStepTree( steps: AgentWorkflowStepUiConfig[], stepKey: string, updater: (step: AgentWorkflowStepUiConfig) => AgentWorkflowStepUiConfig ): AgentWorkflowStepUiConfig[] { return steps.map((step) => { if (step.key === stepKey) { return updater(step); } return { ...step, children: updateWorkflowStepTree(step.children || [], stepKey, updater), }; }); } function removeWorkflowStepTree( steps: AgentWorkflowStepUiConfig[], stepKey: string ): AgentWorkflowStepUiConfig[] { return steps .filter((step) => step.key !== stepKey) .map((step) => ({ ...step, children: removeWorkflowStepTree(step.children || [], stepKey), })); } function collectWorkflowStepKeys(steps: AgentWorkflowStepUiConfig[]): string[] { return flattenAgentWorkflowSteps(steps).map(({ step }) => step.key); } function collectDescendantStepKeys(step: AgentWorkflowStepUiConfig): string[] { return [step.key, ...collectWorkflowStepKeys(step.children || [])]; } interface AgentWorkflowSettingsClientProps { backHref?: string; backLabel?: string; } export function AgentWorkflowSettingsClient({ backHref = "/settings", backLabel = "返回设置", }: AgentWorkflowSettingsClientProps = {}) { const { data, isLoading, mutate } = useSWR<{ agentWorkflowUiConfig?: AgentWorkflowUiConfig; agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig; agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; }>("/api/site-settings", fetcher); const [config, setConfig] = useState(defaultAgentWorkflowUiConfig); const [materialApiConfig, setMaterialApiConfig] = useState( defaultAgentWorkflowMaterialApiConfig ); const [showcaseConfig, setShowcaseConfig] = useState( defaultAgentWorkflowShowcaseConfig ); const [brandModelConfig, setBrandModelConfig] = useState( defaultAgentWorkflowBrandModelConfig ); const [isSaving, setIsSaving] = useState(false); const [uploadingKey, setUploadingKey] = useState(null); const [expandedSteps, setExpandedSteps] = useState>(new Set()); const [activeTab, setActiveTab] = useState("showcase"); const [brandDraft, setBrandDraft] = useState(""); const [storeDraft, setStoreDraft] = useState(""); const fileInputRefs = useRef>({}); useEffect(() => { if (!data) return; const normalizedUiConfig = normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig); setConfig(normalizedUiConfig); setMaterialApiConfig( normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig) ); setShowcaseConfig( normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig) ); setBrandModelConfig( normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig) ); if (normalizedUiConfig.steps[0]) { setExpandedSteps(new Set([normalizedUiConfig.steps[0].key])); } }, [data]); const configuredBrands = useMemo( () => config.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [], [config.inputOptions?.brands] ); const brandModelBrands = useMemo(() => { return Array.from( new Set([ ...configuredBrands, ...brandModelConfig.entries.map((entry) => entry.brandName), ]) ).sort((left, right) => left.localeCompare(right, "zh-CN")); }, [brandModelConfig.entries, configuredBrands]); const sortedSteps = useMemo( () => [...config.steps].sort((left, right) => left.sortOrder - right.sortOrder), [config.steps] ); const brandOptions = config.inputOptions?.brands || []; const storeOptions = config.inputOptions?.stores || []; const updateInputOptions = (updates: Partial<{ brands: string[]; stores: string[] }>) => { setConfig((prev) => ({ ...prev, inputOptions: { brands: updates.brands ?? prev.inputOptions?.brands ?? [], stores: updates.stores ?? prev.inputOptions?.stores ?? [], }, })); }; const normalizeInputOptionValue = (value: string) => value .replace(/\s+/g, " ") .trim(); const handleAddInputOption = (kind: "brands" | "stores") => { const draft = kind === "brands" ? brandDraft : storeDraft; const nextValue = normalizeInputOptionValue(draft); if (!nextValue) { toast.error(kind === "brands" ? "请输入品牌名称" : "请输入店铺名称"); return; } const currentList = kind === "brands" ? brandOptions : storeOptions; if (currentList.some((item) => item.toLowerCase() === nextValue.toLowerCase())) { toast.error(kind === "brands" ? "该品牌已存在" : "该店铺已存在"); return; } if (kind === "brands") { updateInputOptions({ brands: [...brandOptions, nextValue] }); setBrandDraft(""); return; } updateInputOptions({ stores: [...storeOptions, nextValue] }); setStoreDraft(""); }; const handleRemoveInputOption = (kind: "brands" | "stores", value: string) => { if (kind === "brands") { updateInputOptions({ brands: brandOptions.filter((item) => item !== value), }); return; } updateInputOptions({ stores: storeOptions.filter((item) => item !== value), }); }; const enabledStepsCount = useMemo( () => flattenAgentWorkflowSteps(sortedSteps).filter(({ step }) => step.enabled).length, [sortedSteps] ); const totalOptionsCount = useMemo( () => flattenAgentWorkflowSteps(sortedSteps).reduce( (acc, { step }) => acc + step.options.filter((option) => option.enabled).length, 0 ), [sortedSteps] ); const toggleStepExpansion = (stepKey: string) => { setExpandedSteps((prev) => { const next = new Set(prev); if (next.has(stepKey)) { next.delete(stepKey); } else { next.add(stepKey); } return next; }); }; const updateStep = ( stepKey: string, updater: (step: AgentWorkflowUiConfig["steps"][number]) => AgentWorkflowUiConfig["steps"][number] ) => { setConfig((prev) => normalizeConfigOrdering({ ...prev, steps: updateWorkflowStepTree(prev.steps, stepKey, updater), }) ); }; const updateOption = ( stepKey: string, optionKey: string, updater: ( option: AgentWorkflowUiConfig["steps"][number]["options"][number] ) => AgentWorkflowUiConfig["steps"][number]["options"][number] ) => { updateStep(stepKey, (step) => ({ ...step, options: step.options.map((option) => option.key === optionKey ? updater(option) : option ), })); }; const handleAddStep = (parentStepKey?: string) => { setConfig((prev) => { const existingStepKeys = collectWorkflowStepKeys(prev.steps); if (!parentStepKey) { const newStep = createWorkflowStepDraft(1, prev.steps.length, existingStepKeys); setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key])); return normalizeConfigOrdering({ ...prev, steps: [...prev.steps, newStep], }); } const parentStep = findAgentWorkflowStepByKey(prev.steps, parentStepKey); const parentDepth = flattenAgentWorkflowSteps(prev.steps).find( ({ step }) => step.key === parentStepKey )?.depth; if (!parentStep || !parentDepth || parentDepth >= AGENT_WORKFLOW_MAX_TREE_DEPTH) { return prev; } const newChildStep = createWorkflowStepDraft( parentDepth + 1, (parentStep.children || []).length, existingStepKeys ); setExpandedSteps( (prevExpanded) => new Set([...prevExpanded, parentStepKey, newChildStep.key]) ); return normalizeConfigOrdering({ ...prev, steps: updateWorkflowStepTree(prev.steps, parentStepKey, (step) => ({ ...step, children: [...(step.children || []), newChildStep], })), }); }); setActiveTab("workflow"); }; const handleRemoveStep = (stepKey: string) => { const targetStep = findAgentWorkflowStepByKey(config.steps, stepKey); setConfig((prev) => normalizeConfigOrdering({ ...prev, steps: removeWorkflowStepTree(prev.steps, stepKey), }) ); setExpandedSteps((prev) => { const next = new Set(prev); for (const key of targetStep ? collectDescendantStepKeys(targetStep) : [stepKey]) { next.delete(key); } return next; }); }; const handleAddOption = (stepKey: string) => { updateStep(stepKey, (step) => ({ ...step, options: [ ...step.options, createWorkflowOptionDraft(step.key, step.options.length, step.options.map((item) => item.key)), ], })); }; const handleRemoveOption = (stepKey: string, optionKey: string) => { updateStep(stepKey, (step) => ({ ...step, options: step.options.filter((option) => option.key !== optionKey), })); }; const handleUploadPreview = async (stepKey: string, optionKey: string, file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } const refKey = `${stepKey}:${optionKey}`; try { setUploadingKey(refKey); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } updateOption(stepKey, optionKey, (option) => ({ ...option, previewImageUrl: result.url, })); toast.success("预览图上传成功"); } catch (error) { toast.error(error instanceof Error ? error.message : "预览图上传失败"); } finally { setUploadingKey(null); } }; const handleSave = async () => { try { setIsSaving(true); const response = await fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentWorkflowUiConfig: config, agentWorkflowMaterialApiConfig: materialApiConfig, agentWorkflowBrandModelConfig: brandModelConfig, agentWorkflowShowcaseConfig: showcaseConfig, }), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("智能体工作流配置已保存"); await mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } finally { setIsSaving(false); } }; const renderWorkflowStepCard = ( step: AgentWorkflowStepUiConfig, depth: number, indexPath: number[] ) => { const stepLabel = indexPath.join("."); const levelLabel = WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, WORKFLOW_LEVEL_LABELS.length - 1))] || `第 ${depth} 级模块`; const childSteps = [...(step.children || [])].sort((left, right) => left.sortOrder - right.sortOrder); const directEnabledOptionCount = step.options.filter((option) => option.enabled).length; const childStepCount = countAgentWorkflowSteps(childSteps); const canAddChild = depth < AGENT_WORKFLOW_MAX_TREE_DEPTH; return ( 1 && "pl-4 sm:pl-6")} > toggleStepExpansion(step.key)} > 1 && "border-l-4 border-l-emerald-500/30" )} >
{stepLabel}
updateStep(step.key, (currentStep) => ({ ...currentStep, title: e.target.value, })) } className="h-8 min-w-[180px] max-w-[260px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0" placeholder="模块标题" /> {levelLabel} {step.key}
{directEnabledOptionCount} 选项 {childStepCount} 子模块 {canAddChild ? ( ) : ( 已达三级 )} updateStep(step.key, (currentStep) => ({ ...currentStep, enabled: checked, })) } />