diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index e3f9899..57d06ac 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -15,6 +15,13 @@ import { TicketCreatePage } from '@/features/tickets/TicketCreatePage' import { TicketDetailPage } from '@/features/tickets/TicketDetailPage' import { TicketListPage } from '@/features/tickets/TicketListPage' import { WorkbenchPage } from '@/features/workbench/WorkbenchPage' +import { ModelProviderPage } from '@/features/modelhub/ModelProviderPage' +import { SkillListPage } from '@/features/skills/SkillListPage' +import { SkillDetailPage } from '@/features/skills/SkillDetailPage' +import { AgentListPage } from '@/features/agents/AgentListPage' +import { AgentDetailPage } from '@/features/agents/AgentDetailPage' +import { AgentRunListPage } from '@/features/agents/AgentRunListPage' +import { AgentRunDetailPage } from '@/features/agents/AgentRunDetailPage' function getRoute() { return window.location.hash.replace(/^#/, '') || '/' @@ -57,6 +64,37 @@ export function Router() { return } + if (route === '/modelhub/providers') { + return + } + + if (route === '/skills') { + return + } + + const skillMatch = route.match(/^\/skills\/(\d+)$/) + if (skillMatch) { + return + } + + if (route === '/agents') { + return + } + + const agentMatch = route.match(/^\/agents\/(\d+)$/) + if (agentMatch) { + return + } + + if (route === '/agent-runs') { + return + } + + const runMatch = route.match(/^\/agent-runs\/([0-9a-f-]+)$/i) + if (runMatch) { + return + } + if (route === '/knowledge') { return } diff --git a/frontend/src/features/agents/AgentDetailPage.tsx b/frontend/src/features/agents/AgentDetailPage.tsx new file mode 100644 index 0000000..37ea9a2 --- /dev/null +++ b/frontend/src/features/agents/AgentDetailPage.tsx @@ -0,0 +1,103 @@ +import * as React from 'react' +import { ArrowDown, ArrowLeft, ArrowUp, CopyPlus, Play, Plus, Save, Send, Trash2 } from 'lucide-react' +import { toast } from 'sonner' + +import { AppEmptyState } from '@/components/app/AppEmptyState' +import { AppField } from '@/components/app/AppField' +import { AppPage } from '@/components/app/AppPage' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { FieldGroup } from '@/components/ui/field' +import { Input } from '@/components/ui/input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Textarea } from '@/components/ui/textarea' +import { JsonEditor, parseJsonObject } from '@/features/ai-platform/components/JsonEditor' +import { MobileEditGuard } from '@/features/ai-platform/components/MobileEditGuard' +import { SchemaInputEditor } from '@/features/ai-platform/components/SchemaInputEditor' +import { StatusBadge } from '@/features/ai-platform/status' +import { listDeployments } from '@/features/modelhub/api' +import type { ModelDeployment } from '@/features/modelhub/types' +import { getSkill, listSkills } from '@/features/skills/api' +import type { SkillVersion } from '@/features/skills/types' +import { formatDateTime } from '@/lib/format' + +import { ensureAgentDraft, getAgent, publishAgent, rollbackAgent, startAgentRun, updateAgent, updateAgentDraft } from './api' +import type { AgentDetail, AgentVersion } from './types' + +type EditableStep = { code: string; skill_version_id: string; mapping: string } +type SkillOption = SkillVersion & { definitionName: string } + +export function AgentDetailPage({ agentId }: { agentId: number }) { + const [detail, setDetail] = React.useState(null) + const [draft, setDraft] = React.useState(null) + const [deployments, setDeployments] = React.useState([]) + const [skills, setSkills] = React.useState([]) + const [meta, setMeta] = React.useState({ name: '', description: '' }) + const [modelId, setModelId] = React.useState('') + const [systemPrompt, setSystemPrompt] = React.useState('') + const [temperature, setTemperature] = React.useState('') + const [maxTokens, setMaxTokens] = React.useState('') + const [inputSchema, setInputSchema] = React.useState('{}') + const [exampleInput, setExampleInput] = React.useState('{}') + const [steps, setSteps] = React.useState([]) + const [testVersion, setTestVersion] = React.useState('draft') + const [testInput, setTestInput] = React.useState('{}') + const [error, setError] = React.useState('') + + const hydrateDraft = React.useCallback((version: AgentVersion | null) => { + setDraft(version) + if (!version) return + setModelId(String(version.model_deployment_id)); setSystemPrompt(version.system_prompt) + setTemperature(version.temperature_override === null ? '' : String(version.temperature_override)) + setMaxTokens(version.max_output_tokens_override === null ? '' : String(version.max_output_tokens_override)) + setInputSchema(JSON.stringify(version.input_schema, null, 2)); setExampleInput(JSON.stringify(version.example_input, null, 2)); setTestInput(JSON.stringify(version.example_input, null, 2)) + setSteps(version.steps.map((step) => ({ code: step.code, skill_version_id: String(step.skill_version_id), mapping: JSON.stringify(step.input_mapping, null, 2) }))) + }, []) + + const load = React.useCallback(async () => { + try { + const [item, nextDeployments, definitions] = await Promise.all([getAgent(agentId), listDeployments(), listSkills()]) + const skillDetails = await Promise.all(definitions.map((skill) => getSkill(skill.id))) + setDetail(item); setMeta({ name: item.name, description: item.description }); setDeployments(nextDeployments) + setSkills(skillDetails.flatMap((skill) => skill.versions.map((version) => ({ ...version, definitionName: skill.name })))) + hydrateDraft(item.versions.find((version) => version.status === 'draft') ?? null) + setTestVersion(item.draft_version ? 'draft' : String(item.active_version ?? '')) + setError('') + } catch (reason) { setError(reason instanceof Error ? reason.message : '加载 Agent 失败') } + }, [agentId, hydrateDraft]) + React.useEffect(() => { void load() }, [load]) + + function updateStep(index: number, patch: Partial) { setSteps((current) => current.map((step, i) => i === index ? { ...step, ...patch } : step)) } + function moveStep(index: number, direction: -1 | 1) { setSteps((current) => { const next = [...current]; const target = index + direction; if (target < 0 || target >= next.length) return current; [next[index], next[target]] = [next[target], next[index]]; return next }) } + + async function save() { + try { + await Promise.all([ + updateAgent(agentId, meta), + updateAgentDraft(agentId, { model_deployment_id: Number(modelId), system_prompt: systemPrompt, input_schema: parseJsonObject(inputSchema, 'Agent 输入 Schema'), example_input: parseJsonObject(exampleInput, '示例输入'), temperature_override: temperature === '' ? null : Number(temperature), max_output_tokens_override: maxTokens === '' ? null : Number(maxTokens), steps: steps.map((step) => ({ code: step.code, skill_version_id: Number(step.skill_version_id), input_mapping: parseJsonObject(step.mapping, `步骤 ${step.code} 输入映射`) })) }) + ]) + toast.success('Agent 草稿已保存'); await load() + } catch (reason) { toast.error(reason instanceof Error ? reason.message : '保存失败') } + } + async function createDraft() { try { await ensureAgentDraft(agentId); toast.success('已创建新草稿'); await load() } catch (reason) { toast.error(reason instanceof Error ? reason.message : '创建草稿失败') } } + async function publish() { try { await publishAgent(agentId); toast.success('Agent 版本已发布'); await load() } catch (reason) { toast.error(reason instanceof Error ? reason.message : '发布失败') } } + async function rollback(version: number) { try { await rollbackAgent(agentId, version); toast.success(`已回滚到 v${version}`); await load() } catch (reason) { toast.error(reason instanceof Error ? reason.message : '回滚失败') } } + async function run() { try { const result = await startAgentRun(agentId, { version: testVersion === 'draft' ? 'draft' : Number(testVersion), input: parseJsonObject(testInput, '测试输入') }); toast.success('Agent 运行已进入队列'); window.location.hash = `/agent-runs/${result.public_id}` } catch (reason) { toast.error(reason instanceof Error ? reason.message : '启动运行失败') } } + + if (error) return + if (!detail) return
+ return + 当前 v{detail.active_version} : 尚未发布} actions={<>{draft ? <> : }} /> + 基础与模型步骤输入 Schema测试版本 + {draft ?
setMeta({ ...meta, name: e.target.value })} /> setMeta({ ...meta, description: e.target.value })} /> setTemperature(e.target.value)} /> setMaxTokens(e.target.value)} />