+
+
+ 基础参数
+
+
+
-
+
-
+
+
+
+ 结果与异常处理
+
+
+ 断言失败表示回复已生成但未通过评估;执行错误包括超时、模型或 pipeline 异常。
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/pages/TestCasesPage.tsx b/frontend/src/components/pages/TestCasesPage.tsx
index 5a7b76d..a3344bb 100644
--- a/frontend/src/components/pages/TestCasesPage.tsx
+++ b/frontend/src/components/pages/TestCasesPage.tsx
@@ -56,25 +56,19 @@ import {
SelectValue,
} from "@/components/ui/select";
import {
- cloneOverallExpectation,
+ cloneOverallCriteria,
cloneTurns,
- cloneUserSimulation,
- cloneVoiceSettings,
- createDefaultUserSimulation,
- createDefaultVoiceSettings,
createEmptyFixedInputTurn,
createTestCase,
createTestSuite,
DEFAULT_INPUT_MODE,
duplicateTestCase,
duplicateTestSuite,
+ getTestCaseValidationMessage,
getTestSuite,
- isFixedScriptMode,
- isVoiceMode,
- kindFromInputMode,
listTestCases,
listTestSuites,
- normalizeOverallExpectation,
+ normalizeOverallCriteria,
removeTestCase,
removeTestCases,
removeTestSuite,
@@ -97,12 +91,18 @@ import { assistantsApi, type Assistant } from "@/lib/api";
export type TestCasesPageProps =
| { mode: "list" }
| { mode: "create" }
- | { mode: "detail"; suiteId: string };
+ | { mode: "detail"; suiteId: string; initialCaseId?: string };
export function TestCasesPage(props: TestCasesPageProps) {
if (props.mode === "list") return
;
if (props.mode === "create") return
;
- return
;
+ return (
+
+ );
}
// ─── Suite 列表 ──────────────────────────────────────────────────────────────
@@ -441,12 +441,9 @@ function caseToDraft(item: TestCase): CaseEditorDraft {
return {
name: item.name,
inputMode: item.inputMode,
- kind: item.kind,
contextTurns: item.contextTurns.map((turn) => ({ ...turn })),
turns: cloneTurns(item.turns),
- voiceSettings: cloneVoiceSettings(item.voiceSettings),
- userSimulation: cloneUserSimulation(item.userSimulation),
- overallExpectation: cloneOverallExpectation(item.overallExpectation),
+ overallCriteria: cloneOverallCriteria(item.overallCriteria),
};
}
@@ -458,12 +455,9 @@ function createEmptyCaseDraft(): CaseEditorDraft {
return {
name: "未命名用例",
inputMode: DEFAULT_INPUT_MODE,
- kind: kindFromInputMode(DEFAULT_INPUT_MODE),
contextTurns: [],
turns: [createEmptyFixedInputTurn()],
- voiceSettings: null,
- userSimulation: null,
- overallExpectation: null,
+ overallCriteria: [],
};
}
@@ -471,25 +465,23 @@ function applyInputMode(
draft: CaseEditorDraft,
mode: TestCaseInputMode,
): CaseEditorDraft {
- const next: CaseEditorDraft = {
- ...draft,
- inputMode: mode,
- kind: kindFromInputMode(mode),
- };
- if (isVoiceMode(mode) && !next.voiceSettings) {
- next.voiceSettings = createDefaultVoiceSettings();
- }
- if (!isFixedScriptMode(mode) && !next.userSimulation) {
- next.userSimulation = createDefaultUserSimulation();
- }
- return next;
+ return { ...draft, inputMode: mode };
}
-function SuiteDetailView({ suiteId }: { suiteId: string }) {
+function SuiteDetailView({
+ suiteId,
+ initialCaseId,
+}: {
+ suiteId: string;
+ initialCaseId?: string;
+}) {
const router = useRouter();
const [initial] = useState(() => {
const initialCases = listTestCases(suiteId);
- const initialCase = initialCases[0] ?? null;
+ const initialCase =
+ initialCases.find((item) => item.id === initialCaseId) ??
+ initialCases[0] ??
+ null;
const initialDraft = initialCase ? caseToDraft(initialCase) : null;
return {
suite: getTestSuite(suiteId),
@@ -562,6 +554,60 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
savedSnapshot === null ||
!draftsEqual(draft, savedSnapshot));
+ const validationMessage = draft
+ ? getTestCaseValidationMessage(draft)
+ : "请先选择测试用例";
+ const canSave =
+ Boolean(draft?.name.trim()) && dirty && validationMessage === null;
+
+ useEffect(() => {
+ if (!dirty) return;
+
+ function handleBeforeUnload(event: BeforeUnloadEvent) {
+ event.preventDefault();
+ event.returnValue = "";
+ }
+
+ function handleInternalLink(event: MouseEvent) {
+ if (
+ event.defaultPrevented ||
+ event.button !== 0 ||
+ event.metaKey ||
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.altKey
+ ) {
+ return;
+ }
+ const target = event.target;
+ if (!(target instanceof Element)) return;
+ const link = target.closest
("a[href]");
+ if (!link || link.target === "_blank" || link.hasAttribute("download")) {
+ return;
+ }
+ const nextUrl = new URL(link.href, window.location.href);
+ if (nextUrl.origin !== window.location.origin) return;
+ if (
+ nextUrl.pathname === window.location.pathname &&
+ nextUrl.search === window.location.search
+ ) {
+ return;
+ }
+ if (window.confirm("当前用例有未保存修改,离开将丢弃。继续?")) {
+ return;
+ }
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ document.addEventListener("click", handleInternalLink, true);
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ document.removeEventListener("click", handleInternalLink, true);
+ };
+ }, [dirty]);
+
function selectCase(item: TestCase) {
if (dirty && !window.confirm("当前用例有未保存修改,切换将丢弃。继续?")) {
return;
@@ -584,18 +630,13 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
}
function handleSave() {
- if (!draft) return;
+ if (!draft || validationMessage) return;
const patch = {
name: draft.name.trim() || "未命名用例",
inputMode: draft.inputMode,
- kind: kindFromInputMode(draft.inputMode),
contextTurns: draft.contextTurns,
turns: draft.turns,
- voiceSettings: draft.voiceSettings,
- userSimulation: draft.userSimulation,
- overallExpectation: normalizeOverallExpectation(
- draft.overallExpectation?.criteria,
- ),
+ overallCriteria: normalizeOverallCriteria(draft.overallCriteria),
};
let saved: TestCase | null;
@@ -807,7 +848,15 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
/>
- {dirty ? (
+ {dirty && validationMessage ? (
+
+ {validationMessage}
+
+ ) : dirty ? (
未保存
) : statusMessage ? (
@@ -817,7 +866,8 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {