import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type React from 'react'; import { Link } from 'react-router-dom'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Brain, CircleAlert, CircleCheck, FileJson2, Info, LoaderCircle, Power, RefreshCw, Trash2, Unplug, } from 'lucide-react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; import { PipelineConfigStage, PipelineConfigTab, } from '@/app/infra/entities/pipeline'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import EmojiPicker from '@/components/ui/emoji-picker'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; interface AgentFormComponentProps { agentId: string; onFinish: () => void; onDeleted: () => void; onDirtyChange?: (dirty: boolean) => void; onSavingChange?: (saving: boolean) => void; } interface SectionItem { label: string; name: 'basic' | 'runner' | 'events'; icon: React.ElementType; } export default function AgentFormComponent({ agentId, onFinish, onDeleted, onDirtyChange, onSavingChange, }: AgentFormComponentProps) { const { t } = useTranslation(); const [activeSection, setActiveSection] = useState('basic'); const [runnerConfigSchema, setRunnerConfigSchema] = useState(null); const [pluginSystemStatus, setPluginSystemStatus] = useState(null); const [pluginStatusLoading, setPluginStatusLoading] = useState(true); const [pluginStatusError, setPluginStatusError] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [isSaving, setIsSaving] = useState(false); const isSavingRef = useRef(false); const formSchema = z.object({ basic: z.object({ name: z.string().min(1, { message: t('agents.nameRequired') }), description: z.string().optional(), emoji: z.string().optional(), enabled: z.boolean().optional(), }), runner: z.record(z.string(), z.any()), runner_config: z.record(z.string(), z.any()), supported_event_patterns_text: z.string(), }); type FormValues = z.infer; const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { basic: { name: '', description: '', emoji: '🤖', enabled: true, }, runner: {}, runner_config: {}, supported_event_patterns_text: '*', }, }); const savedSnapshotRef = useRef(''); const initializedStagesRef = useRef>(new Set()); const watchedValues = form.watch(); const hasUnsavedChanges = (() => { if (!savedSnapshotRef.current) return false; return JSON.stringify(watchedValues) !== savedSnapshotRef.current; })(); useEffect(() => { onDirtyChange?.(hasUnsavedChanges); }, [hasUnsavedChanges, onDirtyChange]); useEffect(() => { let cancelled = false; Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)]) .then(([metadata, resp]) => { if (cancelled) return; setRunnerConfigSchema(metadata.runner_config ?? null); const agent = resp.agent; const config = (agent.config ?? {}) as Record; const loadedValues: FormValues = { basic: { name: agent.name ?? '', description: agent.description ?? '', emoji: agent.emoji || '🤖', enabled: agent.enabled ?? true, }, runner: (config.runner as Record) ?? {}, runner_config: (config.runner_config as Record) ?? {}, supported_event_patterns_text: ( agent.supported_event_patterns ?? agent.capability?.supported_event_patterns ?? ['*'] ).join('\n'), }; form.reset(loadedValues); savedSnapshotRef.current = JSON.stringify(loadedValues); initializedStagesRef.current.clear(); }) .catch((err) => { toast.error(t('agents.loadError') + err.msg); }); return () => { cancelled = true; }; }, [agentId, form, t]); const loadPluginSystemStatus = useCallback(async () => { setPluginStatusLoading(true); setPluginStatusError(false); try { setPluginSystemStatus(await httpClient.getPluginSystemStatus()); } catch { setPluginSystemStatus(null); setPluginStatusError(true); } finally { setPluginStatusLoading(false); } }, []); useEffect(() => { void loadPluginSystemStatus(); }, [loadPluginSystemStatus]); const sections: SectionItem[] = [ { label: t('agents.basicInfo'), name: 'basic', icon: Info }, { label: t('agents.runnerSettings'), name: 'runner', icon: Brain }, { label: t('agents.advanced'), name: 'events', icon: FileJson2 }, ]; const currentRunner = (form.watch('runner') as Record)?.id; const runnerOptions = useMemo(() => { const runnerStage = runnerConfigSchema?.stages.find( (stage) => stage.name === 'runner', ); return ( runnerStage?.config.find((item) => item.name === 'id')?.options ?? [] ); }, [runnerConfigSchema]); const selectedRunnerOption = runnerOptions.find( (option) => option.name === currentRunner, ); function renderRunnerStatusActions(showRetry = true) { return (
{showRetry && ( )}
); } function renderRunnerStatus() { if (pluginStatusLoading) { return ( {t('agents.runnerStatusLoading')} ); } if (pluginStatusError || !pluginSystemStatus) { return ( {t('agents.runnerStatusCheckFailed')} {t('agents.runnerStatusCheckFailedDescription')} {renderRunnerStatusActions()} ); } if (!pluginSystemStatus.is_enable) { return ( {t('plugins.systemDisabled')} {t('plugins.systemDisabledDesc')} {renderRunnerStatusActions(false)} ); } if (!pluginSystemStatus.is_connected) { return ( {t('plugins.connectionError')} {t('plugins.connectionErrorDesc')} {renderRunnerStatusActions()} ); } if (runnerOptions.length === 0) { return ( {t('agents.noRunnersAvailable')} {t('agents.noRunnersAvailableDescription')} {renderRunnerStatusActions()} ); } if (!currentRunner || !selectedRunnerOption) { return ( {t('agents.selectedRunnerUnavailable')} {t('agents.selectedRunnerUnavailableDescription', { runner: currentRunner || t('agents.noRunnerSelected'), })} {renderRunnerStatusActions()} ); } return ( {t('agents.runnerReady')} {t('agents.runnerReadyDescription', { runner: extractI18nObject(selectedRunnerOption.label), })} ); } function updateSnapshotIfInitial(stageKey: string) { if (!initializedStagesRef.current.has(stageKey)) { initializedStagesRef.current.add(stageKey); if (!hasUnsavedChanges) { savedSnapshotRef.current = JSON.stringify(form.getValues()); } } } function handleDynamicFormEmit( formName: 'runner' | 'runner_config', stageName: string, values: object, ) { if (formName === 'runner') { form.setValue('runner', values, { shouldDirty: true }); updateSnapshotIfInitial(`runner.${stageName}`); return; } const currentRunnerConfigs = (form.getValues('runner_config') as Record) || {}; form.setValue( 'runner_config', { ...currentRunnerConfigs, [stageName]: values, }, { shouldDirty: true }, ); updateSnapshotIfInitial(`runner_config.${stageName}`); } function renderDynamicStage(stage: PipelineConfigStage) { const isRunnerSelector = stage.name === 'runner'; if (!isRunnerSelector && stage.name !== currentRunner) return null; const initialValues = isRunnerSelector ? (form.watch('runner') as Record) || {} : ((form.watch('runner_config') as Record) || {})[ stage.name ] || {}; return ( {extractI18nObject(stage.label)} {stage.description && ( {extractI18nObject(stage.description)} )} handleDynamicFormEmit( isRunnerSelector ? 'runner' : 'runner_config', stage.name, values, ) } /> ); } function normalizeEventPatterns(value: string): string[] { const patterns = value .split(/[\n,]/) .map((item) => item.trim()) .filter(Boolean); return patterns.length > 0 ? patterns : ['*']; } function handleSubmit(values: FormValues) { if (isSavingRef.current) return; const submittedSnapshot = JSON.stringify(values); const runner = values.runner || {}; const agent: Partial = { name: values.basic.name, description: values.basic.description ?? '', emoji: values.basic.emoji, enabled: values.basic.enabled ?? true, component_ref: (runner.id as string) || null, supported_event_patterns: normalizeEventPatterns( values.supported_event_patterns_text, ), config: { runner, runner_config: values.runner_config ?? {}, }, }; isSavingRef.current = true; setIsSaving(true); onSavingChange?.(true); httpClient .updateAgent(agentId, agent) .then(() => { savedSnapshotRef.current = submittedSnapshot; onFinish(); toast.success(t('agents.saveSuccess')); }) .catch((err) => { toast.error(t('agents.saveError') + err.msg); }) .finally(() => { isSavingRef.current = false; setIsSaving(false); onSavingChange?.(false); }); } function confirmDelete() { httpClient .deleteAgent(agentId) .then(() => { toast.success(t('agents.deleteSuccess')); setShowDeleteConfirm(false); onDeleted(); }) .catch((err) => { toast.error(t('agents.deleteError') + err.msg); }); } return ( <>
{activeSection === 'basic' && (
{t('agents.basicInfo')} {t('agents.basicInfoDescription')}
( {t('common.name')} * )} /> ( {t('common.icon')} )} />
( {t('common.description')} )} /> (
{t('agents.enabled')} {t('agents.enabledDescription')}
)} />
{t('agents.dangerZone')} {t('agents.dangerZoneDescription')}

{t('agents.deleteAgentAction')}

{t('agents.deleteAgentHint')}

)} {activeSection === 'runner' && (
{renderRunnerStatus()} {runnerConfigSchema?.stages.map((stage) => renderDynamicStage(stage), )} {!runnerConfigSchema && ( {t('agents.runnerSettings')} {t('agents.noRunnerMetadata')} )}
)} {activeSection === 'events' && ( {t('agents.bindableEvents')} {t('agents.bindableEventsDescription')} ( {t('agents.supportedEvents')}