Add prompt improvement button in drawer
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
FlaskConical,
|
||||
Loader2,
|
||||
Play,
|
||||
Sparkles,
|
||||
Square,
|
||||
TriangleAlert,
|
||||
Wrench,
|
||||
@@ -27,6 +28,11 @@ import {
|
||||
DebugTimeline,
|
||||
type DebugTimelineItem,
|
||||
} from "@/components/debug-timeline";
|
||||
import {
|
||||
PromptImprovementPanel,
|
||||
type PromptImprovementEditor,
|
||||
type PromptImprovementState,
|
||||
} from "@/components/assistant-editor/prompt-improvement-panel";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -339,10 +345,16 @@ export function AutoTestLivePanel({
|
||||
testCase,
|
||||
state,
|
||||
onStart,
|
||||
improvement,
|
||||
promptEditor,
|
||||
onImprove,
|
||||
}: {
|
||||
testCase: TestCase | null;
|
||||
state: AutoTestRunState;
|
||||
onStart: () => void;
|
||||
improvement: PromptImprovementState;
|
||||
promptEditor?: PromptImprovementEditor;
|
||||
onImprove: () => void;
|
||||
}) {
|
||||
if (!testCase) {
|
||||
return (
|
||||
@@ -445,6 +457,8 @@ export function AutoTestLivePanel({
|
||||
`${turn.id}:${turn.runtimeRecords?.length ?? 0}:${turn.toolCalls.length}:${turn.evaluations.length}`,
|
||||
),
|
||||
state.runCase?.overallCriteria.length ?? 0,
|
||||
improvement.phase,
|
||||
improvement.phase === "ready" ? improvement.result.diagnosis : "",
|
||||
].join("|");
|
||||
const autoFollowResetKey = state.run?.id ?? state.selectedId ?? "auto-test";
|
||||
|
||||
@@ -524,6 +538,14 @@ export function AutoTestLivePanel({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.phase === "failed" && promptEditor && improvement.phase !== "idle" && (
|
||||
<PromptImprovementPanel
|
||||
state={improvement}
|
||||
editor={promptEditor}
|
||||
onRegenerate={onImprove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AutoFollowViewport>
|
||||
);
|
||||
@@ -864,6 +886,9 @@ export function AutoTestRunBar({
|
||||
onClearCase,
|
||||
onStop,
|
||||
onRerun,
|
||||
improvement,
|
||||
canImprovePrompt,
|
||||
onImprovePrompt,
|
||||
}: {
|
||||
state: AutoTestRunState;
|
||||
testCase: TestCase | null;
|
||||
@@ -874,6 +899,9 @@ export function AutoTestRunBar({
|
||||
onClearCase: () => void;
|
||||
onStop: () => void;
|
||||
onRerun: () => void;
|
||||
improvement: PromptImprovementState;
|
||||
canImprovePrompt: boolean;
|
||||
onImprovePrompt: () => void;
|
||||
}) {
|
||||
const pickerTrigger = (
|
||||
<button
|
||||
@@ -926,6 +954,59 @@ export function AutoTestRunBar({
|
||||
);
|
||||
}
|
||||
|
||||
if (state.phase === "failed" && canImprovePrompt) {
|
||||
return (
|
||||
<div className="space-y-2 rounded-[1.4rem] border border-hairline-strong bg-background px-3 py-2.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-destructive/15 text-destructive">
|
||||
<X size={12} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
|
||||
{testCase?.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs font-medium text-destructive">
|
||||
FAIL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-8 min-w-0 flex-1 gap-1.5 rounded-full px-3 text-xs"
|
||||
disabled={
|
||||
improvement.phase === "generating" ||
|
||||
Boolean(state.blockedMessage)
|
||||
}
|
||||
onClick={onImprovePrompt}
|
||||
title={state.blockedMessage || "分析失败并生成 Prompt 修改建议"}
|
||||
>
|
||||
{improvement.phase === "generating" ? (
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
) : (
|
||||
<Sparkles size={12} />
|
||||
)}
|
||||
{improvement.phase === "generating"
|
||||
? "正在分析"
|
||||
: improvement.phase === "ready"
|
||||
? "重新分析"
|
||||
: "分析并改进 Prompt"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-8 shrink-0 rounded-full px-3 text-xs"
|
||||
disabled={Boolean(state.blockedMessage)}
|
||||
onClick={onRerun}
|
||||
title={state.blockedMessage || "再次运行"}
|
||||
>
|
||||
再次运行
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
state.phase === "passed" ||
|
||||
state.phase === "failed" ||
|
||||
@@ -1038,6 +1119,10 @@ export function useAutoTestRunner({
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [runError, setRunError] = useState("");
|
||||
const [improvement, setImprovement] = useState<PromptImprovementState>({
|
||||
phase: "idle",
|
||||
});
|
||||
const improvementRequestIdRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
activeRunRef.current = run;
|
||||
@@ -1188,6 +1273,8 @@ export function useAutoTestRunner({
|
||||
setRunError("");
|
||||
setStarting(false);
|
||||
setStopping(false);
|
||||
improvementRequestIdRef.current += 1;
|
||||
setImprovement({ phase: "idle" });
|
||||
},
|
||||
[run],
|
||||
);
|
||||
@@ -1199,6 +1286,8 @@ export function useAutoTestRunner({
|
||||
setRunError("");
|
||||
setStarting(false);
|
||||
setStopping(false);
|
||||
improvementRequestIdRef.current += 1;
|
||||
setImprovement({ phase: "idle" });
|
||||
}, [run]);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
@@ -1214,6 +1303,8 @@ export function useAutoTestRunner({
|
||||
setStarting(true);
|
||||
setRun(null);
|
||||
setRunError("");
|
||||
improvementRequestIdRef.current += 1;
|
||||
setImprovement({ phase: "idle" });
|
||||
try {
|
||||
const snapshot = await batchRunsApi.create({
|
||||
assistantId,
|
||||
@@ -1251,14 +1342,39 @@ export function useAutoTestRunner({
|
||||
}
|
||||
setRun(null);
|
||||
setRunError("");
|
||||
improvementRequestIdRef.current += 1;
|
||||
setImprovement({ phase: "idle" });
|
||||
}, [cancelActiveRun, run]);
|
||||
|
||||
const rerun = useCallback(() => {
|
||||
setRun(null);
|
||||
setRunError("");
|
||||
improvementRequestIdRef.current += 1;
|
||||
setImprovement({ phase: "idle" });
|
||||
window.setTimeout(() => void start(), 0);
|
||||
}, [start]);
|
||||
|
||||
const improvePrompt = useCallback(async () => {
|
||||
if (!run || runCase?.status !== "fail" || improvement.phase === "generating") {
|
||||
return;
|
||||
}
|
||||
const requestId = improvementRequestIdRef.current + 1;
|
||||
improvementRequestIdRef.current = requestId;
|
||||
setImprovement({ phase: "generating" });
|
||||
try {
|
||||
const result = await batchRunsApi.improvePrompt(run.id);
|
||||
if (improvementRequestIdRef.current !== requestId) return;
|
||||
setImprovement({ phase: "ready", result });
|
||||
} catch (error) {
|
||||
if (improvementRequestIdRef.current !== requestId) return;
|
||||
setImprovement({
|
||||
phase: "error",
|
||||
message:
|
||||
error instanceof Error ? error.message : "生成 Prompt 改进建议失败",
|
||||
});
|
||||
}
|
||||
}, [improvement.phase, run, runCase?.status]);
|
||||
|
||||
return {
|
||||
state,
|
||||
suites,
|
||||
@@ -1270,5 +1386,7 @@ export function useAutoTestRunner({
|
||||
start: () => void start(),
|
||||
stop,
|
||||
rerun,
|
||||
improvement,
|
||||
improvePrompt: () => void improvePrompt(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
DebugModeTabs,
|
||||
useAutoTestRunner,
|
||||
} from "@/components/assistant-editor/debug-auto-test";
|
||||
import type { PromptImprovementEditor } from "@/components/assistant-editor/prompt-improvement-panel";
|
||||
import {
|
||||
DebugTimeline,
|
||||
type DebugTimelineItem,
|
||||
@@ -176,6 +177,7 @@ export function DebugDrawer({
|
||||
vision = false,
|
||||
dynamicVariablesEnabled = false,
|
||||
dynamicVariableDefinitions = {},
|
||||
promptImprovementEditor,
|
||||
}: {
|
||||
assistantId: string | null;
|
||||
overlay?: boolean;
|
||||
@@ -185,6 +187,7 @@ export function DebugDrawer({
|
||||
vision?: boolean;
|
||||
dynamicVariablesEnabled?: boolean;
|
||||
dynamicVariableDefinitions?: Record<string, DynamicVariableDefinition>;
|
||||
promptImprovementEditor?: PromptImprovementEditor;
|
||||
}) {
|
||||
const preview = useVoicePreview(assistantId, onNodeActive);
|
||||
const camera = useCameraPreview();
|
||||
@@ -347,7 +350,10 @@ export function DebugDrawer({
|
||||
)}
|
||||
</div>
|
||||
{debugMode === "auto" ? (
|
||||
<AutoTestPanel autoTest={autoTest} />
|
||||
<AutoTestPanel
|
||||
autoTest={autoTest}
|
||||
promptImprovementEditor={promptImprovementEditor}
|
||||
/>
|
||||
) : (
|
||||
<DebugVoicePanel
|
||||
view={view}
|
||||
@@ -369,8 +375,10 @@ export function DebugDrawer({
|
||||
|
||||
function AutoTestPanel({
|
||||
autoTest,
|
||||
promptImprovementEditor,
|
||||
}: {
|
||||
autoTest: ReturnType<typeof useAutoTestRunner>;
|
||||
promptImprovementEditor?: PromptImprovementEditor;
|
||||
}) {
|
||||
const {
|
||||
state,
|
||||
@@ -383,6 +391,8 @@ function AutoTestPanel({
|
||||
suites,
|
||||
cases,
|
||||
catalogLoading,
|
||||
improvement,
|
||||
improvePrompt,
|
||||
} = autoTest;
|
||||
|
||||
return (
|
||||
@@ -392,6 +402,9 @@ function AutoTestPanel({
|
||||
testCase={selectedCase}
|
||||
state={state}
|
||||
onStart={start}
|
||||
improvement={improvement}
|
||||
promptEditor={promptImprovementEditor}
|
||||
onImprove={improvePrompt}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 border-t border-hairline bg-card p-3">
|
||||
@@ -405,6 +418,9 @@ function AutoTestPanel({
|
||||
onClearCase={clearCase}
|
||||
onStop={stop}
|
||||
onRerun={rerun}
|
||||
improvement={improvement}
|
||||
canImprovePrompt={Boolean(promptImprovementEditor)}
|
||||
onImprovePrompt={improvePrompt}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -984,6 +984,10 @@ export function PromptEditor({
|
||||
vision={form.visionEnabled}
|
||||
dynamicVariablesEnabled
|
||||
dynamicVariableDefinitions={dynamicVariableDefinitions}
|
||||
promptImprovementEditor={{
|
||||
prompt: form.prompt,
|
||||
onChange: (prompt) => updateForm("prompt", prompt),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, RotateCcw, Sparkles, TriangleAlert } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { PromptImprovementResult } from "@/data/batch-run";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type PromptImprovementEditor = {
|
||||
prompt: string;
|
||||
onChange: (prompt: string) => void;
|
||||
};
|
||||
|
||||
export type PromptImprovementState =
|
||||
| { phase: "idle" }
|
||||
| { phase: "generating" }
|
||||
| { phase: "ready"; result: PromptImprovementResult }
|
||||
| { phase: "error"; message: string };
|
||||
|
||||
const MANUAL_RECOMMENDATION_LABEL: Record<
|
||||
PromptImprovementResult["manualRecommendations"][number]["category"],
|
||||
string
|
||||
> = {
|
||||
tool: "工具配置",
|
||||
test_case: "测试用例",
|
||||
other: "其他建议",
|
||||
};
|
||||
|
||||
export function PromptImprovementPanel({
|
||||
state,
|
||||
editor,
|
||||
onRegenerate,
|
||||
}: {
|
||||
state: PromptImprovementState;
|
||||
editor: PromptImprovementEditor;
|
||||
onRegenerate: () => void;
|
||||
}) {
|
||||
if (state.phase === "generating") {
|
||||
return (
|
||||
<div
|
||||
className="rounded-2xl border border-hairline bg-surface-card p-4"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-surface-strong text-muted-foreground">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
</span>
|
||||
正在分析失败轨迹…
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
||||
评估模型正在对照 Prompt、工具设置和失败断言生成最小修改建议。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.phase === "error") {
|
||||
return (
|
||||
<div
|
||||
className="rounded-2xl border border-hairline bg-surface-card p-4"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-surface-strong text-muted-foreground">
|
||||
<TriangleAlert size={14} />
|
||||
</span>
|
||||
无法生成改进建议
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
||||
{state.message}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-3 h-8 rounded-full px-3 text-xs"
|
||||
onClick={onRegenerate}
|
||||
>
|
||||
重试分析
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.phase !== "ready") return null;
|
||||
|
||||
const { result } = state;
|
||||
const change = result.promptChange;
|
||||
const isApplied = Boolean(change && editor.prompt === change.after);
|
||||
const canApply = Boolean(change && editor.prompt === change.before);
|
||||
const hasConflict = Boolean(change && !isApplied && !canApply);
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-hairline bg-surface-card p-4 shadow-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<Sparkles size={15} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
Prompt 改进建议
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-soft">
|
||||
由 {result.evaluatorName} 根据本次失败生成
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 whitespace-pre-wrap text-xs leading-5 text-muted-foreground">
|
||||
{result.diagnosis}
|
||||
</p>
|
||||
|
||||
{change ? (
|
||||
<div className="mt-3 rounded-xl border border-hairline bg-canvas-soft/60 p-3">
|
||||
<div className="text-xs font-medium text-foreground">
|
||||
建议修改 Prompt
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-5 text-muted-foreground">
|
||||
{change.reason}
|
||||
</p>
|
||||
|
||||
{change.evidence.length > 0 && (
|
||||
<div className="mt-2 space-y-1 border-l border-hairline-strong pl-2.5">
|
||||
{change.evidence.map((evidence, index) => (
|
||||
<p
|
||||
key={`${index}-${evidence}`}
|
||||
className="text-[11px] leading-4 text-muted-soft"
|
||||
>
|
||||
{evidence}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<details className="group mt-3 rounded-lg border border-hairline bg-background/70">
|
||||
<summary className="cursor-pointer list-none px-3 py-2 text-[11px] font-medium text-foreground">
|
||||
查看修改内容
|
||||
<span className="ml-1 text-muted-soft transition-colors group-open:text-foreground">
|
||||
· 修改前 / 修改后
|
||||
</span>
|
||||
</summary>
|
||||
<div className="space-y-2 border-t border-hairline p-2.5">
|
||||
<PromptTextPreview label="当前 Prompt" value={change.before} />
|
||||
<PromptTextPreview
|
||||
label="建议 Prompt"
|
||||
value={change.after}
|
||||
strong
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{hasConflict && (
|
||||
<p className="mt-2 rounded-lg border border-hairline bg-background/70 px-2.5 py-2 text-[11px] leading-4 text-muted-foreground">
|
||||
当前 Prompt 已在测试后发生变化。为避免覆盖新编辑,请保存并重新测试后再生成建议。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{isApplied ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 rounded-full px-3 text-xs"
|
||||
onClick={() => {
|
||||
if (editor.prompt === change.after) {
|
||||
editor.onChange(change.before);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
撤销应用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-8 rounded-full px-3 text-xs"
|
||||
disabled={!canApply}
|
||||
onClick={() => {
|
||||
if (editor.prompt === change.before) {
|
||||
editor.onChange(change.after);
|
||||
}
|
||||
}}
|
||||
>
|
||||
应用修改
|
||||
</Button>
|
||||
)}
|
||||
{isApplied && (
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
已写入编辑器,请检查并保存
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 rounded-xl border border-hairline bg-canvas-soft/60 px-3 py-2.5 text-xs leading-5 text-muted-foreground">
|
||||
本次失败不适合通过修改 Prompt 解决,请按下面的建议检查配置。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{result.manualRecommendations.length > 0 && (
|
||||
<div className="mt-3 space-y-2 border-t border-hairline pt-3">
|
||||
<div className="text-[11px] font-medium text-muted-soft">需手动处理</div>
|
||||
{result.manualRecommendations.map((item, index) => (
|
||||
<div
|
||||
key={`${item.category}-${index}-${item.title}`}
|
||||
className="rounded-xl border border-hairline bg-canvas-soft/50 px-3 py-2.5"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="rounded-full border border-hairline px-2 py-0.5 text-[10px] text-muted-soft">
|
||||
{MANUAL_RECOMMENDATION_LABEL[item.category]}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-5 text-muted-foreground">
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isApplied && !hasConflict && (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 text-[11px] text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline"
|
||||
onClick={onRegenerate}
|
||||
>
|
||||
重新生成建议
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptTextPreview({
|
||||
label,
|
||||
value,
|
||||
strong = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
strong?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 text-[10px] font-medium tracking-wide text-muted-soft uppercase">
|
||||
{label}
|
||||
</div>
|
||||
<pre
|
||||
className={cn(
|
||||
"scrollbar-subtle max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-lg border border-hairline p-2.5 font-mono text-[10px] leading-4 text-muted-foreground",
|
||||
strong ? "bg-surface-strong/65" : "bg-canvas-soft/60",
|
||||
)}
|
||||
>
|
||||
{value || "(空 Prompt)"}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -126,6 +126,22 @@ export type BatchRunSnapshot = {
|
||||
stopReason: "manual" | "assertion_failure" | "execution_error" | null;
|
||||
};
|
||||
|
||||
export type PromptImprovementResult = {
|
||||
diagnosis: string;
|
||||
promptChange: {
|
||||
before: string;
|
||||
after: string;
|
||||
reason: string;
|
||||
evidence: string[];
|
||||
} | null;
|
||||
manualRecommendations: Array<{
|
||||
category: "tool" | "test_case" | "other";
|
||||
title: string;
|
||||
detail: string;
|
||||
}>;
|
||||
evaluatorName: string;
|
||||
};
|
||||
|
||||
export function firstEvaluationFailureReason(item: BatchRunCase): string {
|
||||
const evaluations = [
|
||||
...item.turns.flatMap((turn) => turn.evaluations),
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import { loginPathWithReturnTo } from "@/lib/auth-redirect";
|
||||
import type { BatchRunSnapshot } from "@/data/batch-run";
|
||||
import type {
|
||||
BatchRunSnapshot,
|
||||
PromptImprovementResult,
|
||||
} from "@/data/batch-run";
|
||||
import type {
|
||||
ContextTurn,
|
||||
FixedInputTurn,
|
||||
@@ -958,4 +961,9 @@ export const batchRunsApi = {
|
||||
request<BatchRunSnapshot>(`/api/test-runs/${id}/cancel`, {
|
||||
method: "POST",
|
||||
}),
|
||||
improvePrompt: (id: string) =>
|
||||
request<PromptImprovementResult>(
|
||||
`/api/test-runs/${id}/prompt-improvement`,
|
||||
{ method: "POST" },
|
||||
),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user