Expand test case fixed-input editor and batch case detail split view.
Unify multi-turn behaviors (reply/tool), overall expectation, and input-mode picker while keeping non-text modes as coming soon; batch completed/running use a half-and-half detail panel below the top bar. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* 测试用例管理:
|
||||
* - 列表页:Test Suite 一级容器
|
||||
* - 详情页:左列表 + 右「单步回复」编辑器(只编辑,不运行)
|
||||
* - 详情页:左列表 + 右编辑器(固定脚本 / 用户模拟,只编辑不运行)
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
ListPageSection,
|
||||
} from "@/components/layout/list-page-layout";
|
||||
import { TopbarPortal } from "@/components/layout/topbar-portal";
|
||||
import { InputModePicker } from "@/components/test-cases/input-mode-picker";
|
||||
import {
|
||||
NextReplyEditorBody,
|
||||
type CaseEditorDraft,
|
||||
@@ -55,25 +56,36 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
cloneOverallExpectation,
|
||||
cloneTurns,
|
||||
cloneUserSimulation,
|
||||
cloneVoiceSettings,
|
||||
createDefaultUserSimulation,
|
||||
createDefaultVoiceSettings,
|
||||
createEmptyFixedInputTurn,
|
||||
createTestCase,
|
||||
createTestSuite,
|
||||
DEFAULT_INPUT_MODE,
|
||||
duplicateTestCase,
|
||||
duplicateTestSuite,
|
||||
getTestSuite,
|
||||
isFixedScriptMode,
|
||||
isVoiceMode,
|
||||
kindFromInputMode,
|
||||
listTestCases,
|
||||
listTestSuites,
|
||||
normalizeOverallExpectation,
|
||||
removeTestCase,
|
||||
removeTestCases,
|
||||
removeTestSuite,
|
||||
reorderTestCases,
|
||||
suiteCaseStats,
|
||||
SUPPORTED_TEST_CASE_KIND,
|
||||
TEST_CASE_KIND_LABEL,
|
||||
TEST_CASE_KIND_OPTIONS,
|
||||
TEST_CASE_INPUT_MODE_LABEL,
|
||||
TEST_CASE_INPUT_MODE_SHORT_LABEL,
|
||||
updateTestCase,
|
||||
updateTestSuite,
|
||||
type TestCase,
|
||||
type TestCaseKind,
|
||||
type TestCaseInputMode,
|
||||
type TestSuite,
|
||||
} from "@/data/test-suites";
|
||||
import { assistantsApi, type Assistant } from "@/lib/api";
|
||||
@@ -147,7 +159,7 @@ function SuiteListView() {
|
||||
return (
|
||||
<ListPageLayout
|
||||
title="测试用例"
|
||||
description="管理用于助手调试的单步回复测试场景。"
|
||||
description="管理用于助手调试的固定输入测试场景。"
|
||||
action={
|
||||
<Button
|
||||
className="w-full shrink-0 gap-2 sm:w-auto"
|
||||
@@ -336,7 +348,7 @@ function SuiteCreateView() {
|
||||
<ListPageLayout
|
||||
title="新建测试集"
|
||||
className="max-w-[1180px]"
|
||||
description="测试集是单步回复用例的业务分组。确认后进入用例编辑。"
|
||||
description="测试集是固定输入用例的业务分组。确认后进入用例编辑。"
|
||||
action={
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -428,13 +440,13 @@ function SuiteCreateView() {
|
||||
function caseToDraft(item: TestCase): CaseEditorDraft {
|
||||
return {
|
||||
name: item.name,
|
||||
inputMode: item.inputMode,
|
||||
kind: item.kind,
|
||||
contextTurns: item.contextTurns.map((turn) => ({ ...turn })),
|
||||
userInput: item.userInput,
|
||||
assertionType: item.assertionType,
|
||||
keywords: [...item.keywords],
|
||||
keywordMatchMode: item.keywordMatchMode,
|
||||
llmCriteria: item.llmCriteria,
|
||||
turns: cloneTurns(item.turns),
|
||||
voiceSettings: cloneVoiceSettings(item.voiceSettings),
|
||||
userSimulation: cloneUserSimulation(item.userSimulation),
|
||||
overallExpectation: cloneOverallExpectation(item.overallExpectation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -445,16 +457,34 @@ function draftsEqual(a: CaseEditorDraft, b: CaseEditorDraft) {
|
||||
function createEmptyCaseDraft(): CaseEditorDraft {
|
||||
return {
|
||||
name: "未命名用例",
|
||||
kind: SUPPORTED_TEST_CASE_KIND,
|
||||
inputMode: DEFAULT_INPUT_MODE,
|
||||
kind: kindFromInputMode(DEFAULT_INPUT_MODE),
|
||||
contextTurns: [],
|
||||
userInput: "",
|
||||
assertionType: "keyword",
|
||||
keywords: [],
|
||||
keywordMatchMode: "any",
|
||||
llmCriteria: "",
|
||||
turns: [createEmptyFixedInputTurn()],
|
||||
voiceSettings: null,
|
||||
userSimulation: null,
|
||||
overallExpectation: null,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
const router = useRouter();
|
||||
const [initial] = useState(() => {
|
||||
@@ -516,7 +546,10 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
const keyword = search.trim().toLowerCase();
|
||||
return cases.filter((item) => {
|
||||
if (!keyword) return true;
|
||||
return [item.name, TEST_CASE_KIND_LABEL[item.kind]]
|
||||
return [
|
||||
item.name,
|
||||
TEST_CASE_INPUT_MODE_SHORT_LABEL[item.inputMode],
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(keyword);
|
||||
@@ -554,13 +587,15 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
if (!draft) return;
|
||||
const patch = {
|
||||
name: draft.name.trim() || "未命名用例",
|
||||
kind: draft.kind,
|
||||
inputMode: draft.inputMode,
|
||||
kind: kindFromInputMode(draft.inputMode),
|
||||
contextTurns: draft.contextTurns,
|
||||
userInput: draft.userInput,
|
||||
assertionType: draft.assertionType,
|
||||
keywords: draft.keywords,
|
||||
keywordMatchMode: draft.keywordMatchMode,
|
||||
llmCriteria: draft.llmCriteria,
|
||||
turns: draft.turns,
|
||||
voiceSettings: draft.voiceSettings,
|
||||
userSimulation: draft.userSimulation,
|
||||
overallExpectation: normalizeOverallExpectation(
|
||||
draft.overallExpectation?.criteria,
|
||||
),
|
||||
};
|
||||
|
||||
let saved: TestCase | null;
|
||||
@@ -766,35 +801,10 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
allowEmpty
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={draft.kind}
|
||||
onValueChange={(value) =>
|
||||
setDraft({
|
||||
...draft,
|
||||
kind: value as TestCaseKind,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-[132px] shrink-0 border-hairline-strong bg-background text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TEST_CASE_KIND_OPTIONS.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={!option.available}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
{!option.available && (
|
||||
<span className="ml-auto text-[11px] font-normal text-muted-soft">
|
||||
即将支持
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputModePicker
|
||||
value={draft.inputMode}
|
||||
onChange={(mode) => setDraft(applyInputMode(draft, mode))}
|
||||
/>
|
||||
|
||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||
{dirty ? (
|
||||
@@ -831,15 +841,15 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{draft.kind === SUPPORTED_TEST_CASE_KIND ? (
|
||||
{draft.inputMode === "fixed_script_text" ? (
|
||||
<NextReplyEditorBody draft={draft} onChange={setDraft} />
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 px-6 text-center">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{TEST_CASE_KIND_LABEL[draft.kind]} · 开发中
|
||||
{TEST_CASE_INPUT_MODE_LABEL[draft.inputMode]} · 开发中
|
||||
</div>
|
||||
<p className="max-w-sm text-xs leading-5 text-muted-foreground">
|
||||
当前仅支持「单步回复」编辑器,其它类型稍后接入。
|
||||
当前仅支持「固定脚本 · 文字」编辑器,其它输入模式稍后接入。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user