From 7ed79d3c1f157023d2aac55ab1384aefb4e662e8 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 17 Aug 2026 17:30:47 +0200 Subject: [PATCH] feat(F): F-UI-EDIT AgentEditor component + automation API client --- frontend/src/api/automation.ts | 46 ++ .../src/components/agents/AgentEditor.tsx | 396 ++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 frontend/src/components/agents/AgentEditor.tsx diff --git a/frontend/src/api/automation.ts b/frontend/src/api/automation.ts index 1f05089..cfcb153 100644 --- a/frontend/src/api/automation.ts +++ b/frontend/src/api/automation.ts @@ -15,6 +15,9 @@ import type { AgentTool, AutomationSettings, MiniAppDef, + AgentToolInfo, + AgentSkillInfo, + AgentRunFull, } from '@/types/automation'; // ── Automation Hooks ── @@ -243,6 +246,49 @@ export function useAgentTools() { }); } +// ── Agent Editor / Chat / Log / Monitor Hooks ── + +export function useAgentToolsFull() { + return useQuery({ + queryKey: ['agentToolsFull'], + queryFn: async () => { + const res = await apiGet('/agents/tools'); + return Array.isArray(res) ? res : res.items ?? []; + }, + }); +} + +export function useAgentSkills() { + return useQuery({ + queryKey: ['agentSkills'], + queryFn: async () => { + const res = await apiGet('/agents/skills'); + return Array.isArray(res) ? res : res.items ?? []; + }, + }); +} + +export function useAgentRunsFull(agentId: string) { + return useQuery({ + queryKey: ['agentRunsFull', agentId], + queryFn: async () => { + const res = await apiGet(`/agents/${agentId}/runs`); + return Array.isArray(res) ? res : res.items ?? []; + }, + enabled: !!agentId, + }); +} + +export function useRecentAgentRuns(limit = 20) { + return useQuery({ + queryKey: ['recentAgentRuns', limit], + queryFn: async () => { + const res = await apiGet(`/agents/runs/recent?limit=${limit}`); + return Array.isArray(res) ? res : res.items ?? []; + }, + }); +} + // ── Agent Communication Hooks ── export function useSendAgentMessage() { diff --git a/frontend/src/components/agents/AgentEditor.tsx b/frontend/src/components/agents/AgentEditor.tsx new file mode 100644 index 0000000..55950cb --- /dev/null +++ b/frontend/src/components/agents/AgentEditor.tsx @@ -0,0 +1,396 @@ +import React, { useEffect, useMemo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { Select } from '@/components/ui/Select'; +import { useToast } from '@/components/ui/Toast'; +import { + useCreateAgent, + useUpdateAgent, + useTestRunAgent, + useAgentToolsFull, + useAgentSkills, +} from '@/api/automation'; +import type { AgentDefinitionFull } from '@/types/automation'; +import { Play, Save, X, Loader2 } from 'lucide-react'; + +const agentEditorSchema = z.object({ + name: z.string().min(1, 'required').max(120), + description: z.string().max(500).default(''), + system_prompt: z.string().default(''), + llm_model: z.string().min(1, 'required').max(100), + tool_ids: z.array(z.string()).default([]), + skill_ids: z.array(z.string()).default([]), + max_steps: z.coerce.number().int().min(1).max(100).default(20), + max_duration_seconds: z.coerce.number().int().min(1).max(86400).default(300), + budget_limit_usd: z.coerce.number().min(0).max(10000).default(1.0), + temperature: z.coerce.number().min(0).max(2).default(0.3), + max_tokens: z.coerce.number().int().min(1).max(100000).default(1000), + trace_mode: z.enum(['standard', 'extended']).default('standard'), + mode: z.enum(['proactive', 'reactive']).default('reactive'), + is_active: z.boolean().default(true), + trigger_config: z.record(z.string(), z.unknown()).default({}), + ai_use_case_metadata: z.record(z.string(), z.unknown()).default({}), +}); + +type AgentEditorFormData = z.infer; + +export interface AgentEditorProps { + agent?: AgentDefinitionFull | null; + onSaved?: (agent: AgentDefinitionFull) => void; + onCancel?: () => void; +} + +const modeOptions = [ + { value: 'reactive', label: 'Reactive' }, + { value: 'proactive', label: 'Proactive' }, +]; + +const traceModeOptions = [ + { value: 'standard', label: 'Standard' }, + { value: 'extended', label: 'Extended' }, +]; + +const commonModels = [ + 'ollama/deepseek-v4-flash', + 'gpt-4', + 'gpt-4-turbo', + 'gpt-3.5-turbo', + 'claude-3-opus', + 'claude-3-sonnet', + 'claude-3-haiku', + 'llama-3-70b', + 'llama-3-8b', + 'mistral-large', + 'mixtral-8x7b', +]; + +export function AgentEditor({ agent, onSaved, onCancel }: AgentEditorProps) { + const { t } = useTranslation(); + const { toast } = useToast(); + const { data: tools = [] } = useAgentToolsFull(); + const { data: skills = [] } = useAgentSkills(); + const createAgent = useCreateAgent(); + const updateAgent = useUpdateAgent(); + const testRunAgent = useTestRunAgent(); + + const defaultValues = useMemo(() => { + if (agent) { + return { + name: agent.name, + description: agent.description || '', + system_prompt: agent.system_prompt || '', + llm_model: agent.llm_model, + tool_ids: agent.tool_ids || [], + skill_ids: agent.skill_ids || [], + max_steps: agent.max_steps, + max_duration_seconds: agent.max_duration_seconds, + budget_limit_usd: agent.budget_limit_usd, + temperature: agent.temperature, + max_tokens: agent.max_tokens, + trace_mode: agent.trace_mode, + mode: agent.mode, + is_active: agent.is_active, + trigger_config: agent.trigger_config || {}, + ai_use_case_metadata: agent.ai_use_case_metadata || {}, + }; + } + return { + name: '', + description: '', + system_prompt: '', + llm_model: 'ollama/deepseek-v4-flash', + tool_ids: [], + skill_ids: [], + max_steps: 20, + max_duration_seconds: 300, + budget_limit_usd: 1.0, + temperature: 0.3, + max_tokens: 1000, + trace_mode: 'standard', + mode: 'reactive', + is_active: true, + trigger_config: {}, + ai_use_case_metadata: {}, + }; + }, [agent]); + + const { + register, + handleSubmit, + control, + reset, + watch, + setValue, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(agentEditorSchema), + defaultValues, + }); + + const selectedToolIds = watch('tool_ids'); + const selectedSkillIds = watch('skill_ids'); + + useEffect(() => { + reset(defaultValues); + }, [reset, defaultValues]); + + const onSubmit = async (data: AgentEditorFormData) => { + try { + if (agent) { + const updated = await updateAgent.mutateAsync({ id: agent.id, data }); + toast({ title: t('agent.saved'), variant: 'success' }); + onSaved?.(updated); + } else { + const created = await createAgent.mutateAsync(data); + toast({ title: t('agent.created'), variant: 'success' }); + onSaved?.(created); + } + } catch { + toast({ title: t('agent.saveFailed'), variant: 'error' }); + } + }; + + const handleTestRun = async () => { + if (!agent) { + toast({ title: t('agent.saveBeforeTest'), variant: 'warning' }); + return; + } + try { + await testRunAgent.mutateAsync(agent.id); + toast({ title: t('agent.testRunOk'), variant: 'success' }); + } catch { + toast({ title: t('agent.testRunFailed'), variant: 'error' }); + } + }; + + const toggleArrayValue = (field: 'tool_ids' | 'skill_ids', value: string) => { + const current = field === 'tool_ids' ? selectedToolIds : selectedSkillIds; + const next = (current || []).includes(value) + ? (current || []).filter((v) => v !== value) + : [...(current || []), value]; + setValue(field, next, { shouldDirty: true, shouldValidate: true }); + }; + + return ( +
+ {/* Basic info */} +
+ + +
+ + {/* System prompt */} +
+ +