From 92b7af118e41f09d9da7e93c9c31c48e2c0c4a9a Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 14 Jul 2026 14:08:04 +0800 Subject: [PATCH] feat: add model and skill admin pages --- .../src/components/app/AppTraceTimeline.tsx | 28 ++- .../ai-platform-components.test.tsx | 32 +++ .../ai-platform/components/JsonEditor.tsx | 85 ++++++++ .../components/MobileEditGuard.tsx | 16 ++ .../components/SchemaInputEditor.tsx | 32 +++ frontend/src/features/ai-platform/status.tsx | 20 ++ .../features/modelhub/ModelProviderPage.tsx | 192 ++++++++++++++++++ frontend/src/features/modelhub/api.ts | 11 + frontend/src/features/modelhub/types.ts | 40 ++++ .../src/features/skills/SkillDetailPage.tsx | 84 ++++++++ .../src/features/skills/SkillListPage.tsx | 57 ++++++ frontend/src/features/skills/api.ts | 11 + frontend/src/features/skills/types.ts | 24 +++ frontend/src/lib/api.test.ts | 17 ++ frontend/src/lib/api.ts | 29 ++- 15 files changed, 669 insertions(+), 9 deletions(-) create mode 100644 frontend/src/features/ai-platform/ai-platform-components.test.tsx create mode 100644 frontend/src/features/ai-platform/components/JsonEditor.tsx create mode 100644 frontend/src/features/ai-platform/components/MobileEditGuard.tsx create mode 100644 frontend/src/features/ai-platform/components/SchemaInputEditor.tsx create mode 100644 frontend/src/features/ai-platform/status.tsx create mode 100644 frontend/src/features/modelhub/ModelProviderPage.tsx create mode 100644 frontend/src/features/modelhub/api.ts create mode 100644 frontend/src/features/modelhub/types.ts create mode 100644 frontend/src/features/skills/SkillDetailPage.tsx create mode 100644 frontend/src/features/skills/SkillListPage.tsx create mode 100644 frontend/src/features/skills/api.ts create mode 100644 frontend/src/features/skills/types.ts create mode 100644 frontend/src/lib/api.test.ts diff --git a/frontend/src/components/app/AppTraceTimeline.tsx b/frontend/src/components/app/AppTraceTimeline.tsx index 2518a6f..9a02ff9 100644 --- a/frontend/src/components/app/AppTraceTimeline.tsx +++ b/frontend/src/components/app/AppTraceTimeline.tsx @@ -1,12 +1,26 @@ +import type * as React from 'react' import { CheckCircle2, CircleAlert, Clock3 } from 'lucide-react' import { Badge } from '@/components/ui/badge' -import type { AgentStep } from '@/features/prototype/types' + +export type TraceTimelineItem = { + id: React.Key + step_name: string + step_type?: 'model' | 'retrieval' | 'skill' | 'tool' | 'approval' + status: 'success' | 'succeeded' | 'failed' | 'waiting' | 'queued' | 'running' | 'cancelled' + input_summary?: string + output_summary?: string + latency_ms?: number +} const statusConfig = { success: { label: '成功', variant: 'success' }, failed: { label: '失败', variant: 'danger' }, - waiting: { label: '等待确认', variant: 'warning' } + waiting: { label: '等待确认', variant: 'warning' }, + succeeded: { label: '成功', variant: 'success' }, + queued: { label: '排队中', variant: 'secondary' }, + running: { label: '执行中', variant: 'info' }, + cancelled: { label: '已取消', variant: 'secondary' } } as const const typeLabels = { @@ -17,7 +31,7 @@ const typeLabels = { approval: '人工确认' } as const -export function AppTraceTimeline({ items }: { items: AgentStep[] }) { +export function AppTraceTimeline({ items }: { items: TraceTimelineItem[] }) { return (
    {items.map((item) => { @@ -32,12 +46,12 @@ export function AppTraceTimeline({ items }: { items: AgentStep[] }) {

    {item.step_name}

    - {typeLabels[item.step_type]} + {item.step_type ? typeLabels[item.step_type] : 'Skill'} {status.label}
    -

    输入:{item.input_summary}

    -

    输出:{item.output_summary}

    -

    耗时 {item.latency_ms} ms

    + {item.input_summary ?

    输入:{item.input_summary}

    : null} + {item.output_summary ?

    输出:{item.output_summary}

    : null} + {item.latency_ms !== undefined ?

    耗时 {item.latency_ms} ms

    : null}
    ) diff --git a/frontend/src/features/ai-platform/ai-platform-components.test.tsx b/frontend/src/features/ai-platform/ai-platform-components.test.tsx new file mode 100644 index 0000000..aced224 --- /dev/null +++ b/frontend/src/features/ai-platform/ai-platform-components.test.tsx @@ -0,0 +1,32 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { AppTraceTimeline } from '@/components/app/AppTraceTimeline' +import { JsonEditor, parseJsonObject } from './components/JsonEditor' +import { SchemaInputEditor } from './components/SchemaInputEditor' + +afterEach(cleanup) + +describe('AI 平台编辑组件', () => { + it('格式化并校验 JSON,同时拒绝非对象输入', () => { + const onChange = vi.fn() + render() + fireEvent.click(screen.getByRole('button', { name: '格式化' })) + expect(onChange).toHaveBeenCalledWith('{\n "type": "object"\n}') + expect(screen.getByText('JSON 语法有效')).toBeInTheDocument() + expect(() => parseJsonObject('[]', '输入')).toThrow('输入必须是 JSON 对象') + }) + + it('根据简单 Schema 生成表单并同步 JSON', () => { + const onChange = vi.fn() + render() + fireEvent.change(screen.getByLabelText('problem'), { target: { value: '新问题' } }) + expect(onChange).toHaveBeenCalledWith('{\n "problem": "新问题"\n}') + }) + + it('运行轨迹组件不依赖 prototype 类型', () => { + render() + expect(screen.getByText('问题结构化')).toBeInTheDocument() + expect(screen.getByText('执行中')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/features/ai-platform/components/JsonEditor.tsx b/frontend/src/features/ai-platform/components/JsonEditor.tsx new file mode 100644 index 0000000..8f2ca6a --- /dev/null +++ b/frontend/src/features/ai-platform/components/JsonEditor.tsx @@ -0,0 +1,85 @@ +import * as React from 'react' +import { Braces, CheckCircle2, WandSparkles } from 'lucide-react' + +import { AppField } from '@/components/app/AppField' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' + +type JsonEditorProps = { + id: string + label: string + value: string + onChange: (value: string) => void + description?: string + rows?: number +} + +export function JsonEditor({ id, label, value, onChange, description, rows = 12 }: JsonEditorProps) { + const [message, setMessage] = React.useState<{ kind: 'success' | 'error'; text: string } | null>(null) + + function parse() { + try { + const parsed = JSON.parse(value) as unknown + setMessage({ kind: 'success', text: 'JSON 语法有效' }) + return parsed + } catch (reason) { + setMessage({ kind: 'error', text: reason instanceof Error ? reason.message : 'JSON 语法无效' }) + return null + } + } + + function format() { + const parsed = parse() + if (parsed !== null) onChange(JSON.stringify(parsed, null, 2)) + } + + return ( +
    + +