diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1d61ee3..4860fa9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "dependencies": { "@daily-co/daily-js": "^0.90.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@pipecat-ai/client-js": "^1.12.0", "@pipecat-ai/small-webrtc-transport": "^1.10.5", "@xyflow/react": "^12.11.0", @@ -477,6 +480,59 @@ "node": ">=22.14.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.71.0", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.71.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 020a51d..b8c95c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,9 @@ }, "dependencies": { "@daily-co/daily-js": "^0.90.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@pipecat-ai/client-js": "^1.12.0", "@pipecat-ai/small-webrtc-transport": "^1.10.5", "@xyflow/react": "^12.11.0", diff --git a/frontend/src/app/test/cases/[id]/page.tsx b/frontend/src/app/test/cases/[id]/page.tsx new file mode 100644 index 0000000..3aa359a --- /dev/null +++ b/frontend/src/app/test/cases/[id]/page.tsx @@ -0,0 +1,10 @@ +import { TestCasesPage } from "@/components/pages/TestCasesPage"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ; +} diff --git a/frontend/src/app/test/cases/new/page.tsx b/frontend/src/app/test/cases/new/page.tsx new file mode 100644 index 0000000..8b7ba49 --- /dev/null +++ b/frontend/src/app/test/cases/new/page.tsx @@ -0,0 +1,5 @@ +import { TestCasesPage } from "@/components/pages/TestCasesPage"; + +export default function Page() { + return ; +} diff --git a/frontend/src/app/test/cases/page.tsx b/frontend/src/app/test/cases/page.tsx index a367117..1c726a4 100644 --- a/frontend/src/app/test/cases/page.tsx +++ b/frontend/src/app/test/cases/page.tsx @@ -1,5 +1,5 @@ import { TestCasesPage } from "@/components/pages/TestCasesPage"; export default function Page() { - return ; + return ; } diff --git a/frontend/src/components/assistant-editor/debug-auto-test.tsx b/frontend/src/components/assistant-editor/debug-auto-test.tsx index cbf2a60..b730b4a 100644 --- a/frontend/src/components/assistant-editor/debug-auto-test.tsx +++ b/frontend/src/components/assistant-editor/debug-auto-test.tsx @@ -35,7 +35,8 @@ export type TestCaseKind = | "user_simulation" | "tool_call" | "fixed_dialogue" - | "single_turn"; + | "single_turn" + | "voice_run"; export type ChatScriptTurn = { role: "user" | "assistant"; @@ -103,6 +104,7 @@ const KIND_LABEL: Record = { tool_call: "工具调用", fixed_dialogue: "固定对话", single_turn: "单轮响应", + voice_run: "语音运行", }; const RECENT_IDS = ["tc-dual-car", "tc-hello"]; diff --git a/frontend/src/components/editor/section-card.tsx b/frontend/src/components/editor/section-card.tsx index 8f0954e..6dede0a 100644 --- a/frontend/src/components/editor/section-card.tsx +++ b/frontend/src/components/editor/section-card.tsx @@ -25,12 +25,15 @@ export function SectionCard({ icon, title, description, + action, children, className, }: { icon?: ReactNode; title?: string; description?: string; + /** 标题行右侧操作区(如添加按钮) */ + action?: ReactNode; children: ReactNode; className?: string; }) { @@ -52,12 +55,13 @@ export function SectionCard({ {icon} )} -
+
{title} {description && }
+ {action &&
{action}
}
)} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 7d41ae3..2ea59f8 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -13,7 +13,6 @@ import { Clock3, Database, HelpCircle, - Layers, Wrench, Home, LogOut, @@ -55,7 +54,6 @@ const monitorSubItems: NavItem[] = [ const testSubItems: NavItem[] = [ { href: "/test/cases", label: "测试用例", icon: ClipboardList }, - { href: "/test/batch", label: "批量测试", icon: Layers }, ]; export function Sidebar({ collapsed, onToggle }: SidebarProps) { diff --git a/frontend/src/components/pages/TestCasesPage.tsx b/frontend/src/components/pages/TestCasesPage.tsx index ba9f6cc..9fea23c 100644 --- a/frontend/src/components/pages/TestCasesPage.tsx +++ b/frontend/src/components/pages/TestCasesPage.tsx @@ -1,10 +1,795 @@ -import { PlaceholderPage } from "./PlaceholderPage"; +"use client"; + +/** + * 测试用例管理: + * - 列表页:Test Suite 一级容器 + * - 详情页:左列表 + 右「单步回复」编辑器(只编辑,不运行) + */ + +import { + ChevronLeft, + Loader2, + MoreHorizontal, + Pencil, + Plus, + Rocket, + Save, + Trash2, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; + +import { + AssistantIdentity, + EditableTitle, + EditorBackButton, +} from "@/components/assistant-editor/editor-controls"; +import { + ListPageLayout, + ListPageSection, +} from "@/components/layout/list-page-layout"; +import { TopbarPortal } from "@/components/layout/topbar-portal"; +import { + NextReplyEditorBody, + type CaseEditorDraft, +} from "@/components/test-cases/next-reply-editor"; +import { SuiteCaseList } from "@/components/test-cases/suite-case-list"; +import { Button } from "@/components/ui/button"; +import { DataList } from "@/components/ui/data-list"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { ListToolbar } from "@/components/ui/list-toolbar"; +import { SearchInput } from "@/components/ui/search-input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { + createTestCase, + createTestSuite, + formatSuiteResult, + getTestSuite, + listTestCases, + listTestSuites, + removeTestCase, + removeTestCases, + removeTestSuite, + reorderTestCases, + suiteCaseStats, + SUPPORTED_TEST_CASE_KIND, + TEST_CASE_KIND_LABEL, + TEST_CASE_KIND_OPTIONS, + updateTestCase, + updateTestSuite, + type TestCase, + type TestCaseKind, + type TestSuite, +} from "@/data/test-suites"; +import { assistantsApi, type Assistant } from "@/lib/api"; + +// 路由驱动: +// /test/cases → list +// /test/cases/new → create suite +// /test/cases/[id] → suite detail (cases) +export type TestCasesPageProps = + | { mode: "list" } + | { mode: "create" } + | { mode: "detail"; suiteId: string }; + +export function TestCasesPage(props: TestCasesPageProps) { + if (props.mode === "list") return ; + if (props.mode === "create") return ; + return ; +} + +// ─── Suite 列表 ────────────────────────────────────────────────────────────── + +function SuiteListView() { + const router = useRouter(); + const [suites, setSuites] = useState([]); + const [search, setSearch] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [deletingId, setDeletingId] = useState(null); + + useEffect(() => { + setSuites(listTestSuites()); + }, []); + + const filtered = useMemo(() => { + const keyword = search.trim().toLowerCase(); + return suites.filter((suite) => { + if (!keyword) return true; + return [suite.name, suite.description, suite.assistantName, suite.id] + .join(" ") + .toLowerCase() + .includes(keyword); + }); + }, [suites, search]); + + const pageSize = 5; + const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); + const safeCurrentPage = Math.min(currentPage, totalPages); + const pageStart = (safeCurrentPage - 1) * pageSize; + const pageEnd = pageStart + pageSize; + const paginated = filtered.slice(pageStart, pageEnd); + + function openSuite(suite: TestSuite) { + router.push(`/test/cases/${suite.id}`); + } + + function removeSuite(suite: TestSuite) { + if ( + !window.confirm( + `确定删除测试集“${suite.name}”及其全部测试用例吗?`, + ) + ) { + return; + } + setDeletingId(suite.id); + removeTestSuite(suite.id); + setSuites(listTestSuites()); + setDeletingId(null); + } -export function TestCasesPage() { return ( - + description="管理用于助手调试的单步回复测试场景。" + action={ + + } + > + + { + setSearch(value); + setCurrentPage(1); + }} + placeholder="搜索测试集..." + className="lg:w-[320px]" + /> + } + /> + + + rows={paginated} + rowKey={(suite) => suite.id} + onRowClick={openSuite} + empty={{ + title: suites.length === 0 ? "暂无测试集" : "未找到匹配的测试集", + description: + suites.length === 0 + ? "点击右上角「新建测试集」开始。" + : "请调整关键词后再试。", + }} + pagination={{ + page: safeCurrentPage, + totalPages, + onPageChange: setCurrentPage, + summary: + filtered.length === 0 + ? "没有数据" + : `显示 ${pageStart + 1}-${Math.min(pageEnd, filtered.length)} / 共 ${filtered.length} 个测试集`, + }} + columns={[ + { + key: "name", + header: "测试集名称", + width: "md:w-[320px]", + cell: (suite) => ( + <> +
+ {suite.name} +
+
+ {suite.description || suite.id} +
+ + ), + }, + { + key: "assistant", + header: "关联助手", + width: "md:w-[160px]", + cellClassName: "text-muted-foreground", + cell: (suite) => suite.assistantName || "—", + }, + { + key: "caseCount", + header: "用例数", + width: "md:w-[96px]", + cellClassName: "tabular-nums text-muted-foreground", + cell: (suite) => suiteCaseStats(suite.id).total, + }, + { + key: "lastResult", + header: "最近结果", + width: "md:w-[112px]", + cellClassName: "tabular-nums text-muted-foreground", + cell: (suite) => formatSuiteResult(suite.id), + }, + { + key: "actions", + header: "操作", + align: "right", + cell: (suite) => ( +
event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + > + + + + + + + { + event.preventDefault(); + window.setTimeout(() => removeSuite(suite), 0); + }} + > + {deletingId === suite.id ? ( + + ) : ( + + )} + 删除 + + + +
+ ), + }, + ]} + /> +
+ + ); +} + +// ─── 新建测试集 ────────────────────────────────────────────────────────────── + +function SuiteCreateView() { + const router = useRouter(); + const [assistants, setAssistants] = useState([]); + const [loadingAssistants, setLoadingAssistants] = useState(true); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [assistantName, setAssistantName] = useState(""); + const [creating, setCreating] = useState(false); + + useEffect(() => { + void (async () => { + try { + const list = await assistantsApi.list(); + setAssistants(list); + if (list[0]) setAssistantName(list[0].name); + } catch { + setAssistantName("视频快处助手"); + } finally { + setLoadingAssistants(false); + } + })(); + }, []); + + function confirmCreate() { + if (!name.trim() || creating) return; + setCreating(true); + const saved = createTestSuite({ + name, + description, + assistantName, + }); + router.push(`/test/cases/${saved.id}`); + } + + return ( + router.push("/test/cases")} + > + + 返回列表 + + } + > + + + + + +
+