feat: add AI descriptions and platform submission checks

This commit is contained in:
yuxuanhui
2026-09-09 20:24:47 +08:00
parent 6b4990f100
commit 53b01eb770
16 changed files with 1007 additions and 4 deletions
+11
View File
@@ -32,6 +32,7 @@ import type {
} from "../types";
import { PnlChart } from "./PnlChart";
import { SelfCorrelationPanel } from "./SelfCorrelationPanel";
import { SubmissionPanel } from "./SubmissionPanel";
import { EvaluationPanel } from "../research/EvaluationPanel";
import { LineagePanel } from "../research/LineagePanel";
import { ComparisonPanel } from "../research/ComparisonPanel";
@@ -337,6 +338,16 @@ export function AlphaDetail({
/>
)}
</TabPane>
<TabPane tab="Description 与提交检查" itemKey="submission">
{tab === "submission" && id && (
<SubmissionPanel
key={id}
id={id}
version={version}
onTask={onTask}
/>
)}
</TabPane>
<TabPane tab="基线比较" itemKey="compare">
{tab === "compare" && (
<ComparisonPanel key={detail.id} baseline={detail.id} />
+237
View File
@@ -0,0 +1,237 @@
import { useEffect, useRef, useState } from "react";
import { Banner, Button, Spin, TextArea, Toast } from "@douyinfe/semi-ui-19";
import { api, formatTime, jobStateLabels, post } from "../api";
import type { Job } from "../types";
type Description = {
idea: string;
data_rationale: string;
operator_rationale: string;
};
type Submission = {
snapshot: string;
sections: Record<string, { code: string | null; description: string }>;
descriptions: Record<string, Description>;
model: string;
can_generate: boolean;
can_check: boolean;
job: Job | null;
};
const fields = [
["idea", "Idea"],
["data_rationale", "Rationale for data used"],
["operator_rationale", "Rationale for operators used"],
] as const;
const render = (value: Description) =>
fields.map(([key, title]) => `${title}: ${value[key].trim()}`).join("\n");
const activeStates = [
"queued",
"running",
"waiting_auth",
"waiting_connection",
];
export function SubmissionPanel({
id,
version,
onTask,
}: {
id: string;
version: string;
onTask: () => void;
}) {
const [data, setData] = useState<Submission | null>(null);
const [draft, setDraft] = useState<Record<string, Description>>({});
const [snapshot, setSnapshot] = useState("");
const [busy, setBusy] = useState("");
const [error, setError] = useState("");
const dirty = useRef(false);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
useEffect(() => {
let active = true;
api<Submission>(`/alphas/${id}/submission`)
.then((value) => {
if (!active) return;
setData(value);
if (!dirty.current) {
setDraft(value.descriptions);
setSnapshot(value.snapshot);
}
})
.catch((e) => {
if (active) setError(e.message);
});
return () => {
active = false;
};
}, [id, version]);
async function generate() {
setBusy("generate");
setError("");
try {
const result = await post<{ descriptions: Record<string, Description> }>(
`/alphas/${id}/description/generate`,
{ snapshot },
);
if (!mounted.current) return;
dirty.current = true;
setDraft(result.descriptions);
Toast.success("Description 已生成,可修改后写回");
} catch (e) {
if (mounted.current) setError((e as Error).message);
} finally {
if (mounted.current) setBusy("");
}
}
async function check() {
setBusy("check");
setError("");
try {
const job = await post<Job>(`/alphas/${id}/submission-check`, {
snapshot,
descriptions: draft,
});
if (!mounted.current) return;
// Keep the reviewed draft visible while the durable task runs.
dirty.current = true;
setData((current) => (current ? { ...current, job } : current));
onTask();
} catch (e) {
if (mounted.current) setError((e as Error).message);
} finally {
if (mounted.current) setBusy("");
}
}
if (!data)
return error ? <Banner type="danger" description={error} /> : <Spin />;
const running = !!data.job && activeStates.includes(data.job.status);
const conflict = snapshot !== data.snapshot;
const valid =
Object.keys(draft).length > 0 &&
Object.values(draft).every(
(value) =>
fields.every(([key]) => value[key].trim()) &&
Array.from(render(value)).length >= 100,
);
return (
<div className="detail-section">
<h3>Description 与 Check submission</h3>
<p className="muted">
AI 生成后可逐段修改。写回并检查会更新 BRAIN 的
Description,随后获取平台提交检查结果,不会正式提交 Alpha。
</p>
{error && <Banner type="danger" description={error} />}
{!data.can_check && (
<Banner
type="info"
description="写回并检查需要待提交 Alpha,并先取得有效、样本完整且低于阈值的本地自相关结果。"
/>
)}
{!data.can_generate && (
<p className="muted">
请在个人信息 → 大模型服务中配置 Description 模型。也可以手动填写。
</p>
)}
<div className="inline-actions">
<Button
onClick={() => void generate()}
loading={busy === "generate"}
disabled={!!busy || running || conflict || !data.can_generate}
>
AI 生成三段 Description
</Button>
{data.model && <span className="muted">模型:{data.model}</span>}
</div>
{conflict && (
<Banner
type="warning"
description="平台快照已更新,当前草稿仍保留。请复制需要的内容,再载入最新描述后核对。"
/>
)}
{Object.entries(draft).map(([section, value]) => (
<section key={section} className="detail-section">
<h4>
{section === "regular"
? "Regular"
: section === "selection"
? "Selection"
: "Combo"}
</h4>
{data.sections[section]?.description && (
<details>
<summary>已同步的 Description</summary>
<pre className="code-block">
{data.sections[section].description}
</pre>
</details>
)}
{fields.map(([key, title]) => (
<label key={key}>
{title}:
<TextArea
aria-label={`${section} ${title}`}
value={value[key]}
rows={3}
maxCount={6000}
disabled={!!busy || running}
onChange={(text) => {
dirty.current = true;
setDraft((current) => ({
...current,
[section]: { ...current[section], [key]: text },
}));
}}
/>
</label>
))}
<p className="muted">
总长度:{Array.from(render(value)).length} 字符,至少 100
字符(含标题和换行)。三段内容均需填写。
</p>
</section>
))}
<div className="inline-actions">
<Button
theme="solid"
loading={busy === "check"}
disabled={!!busy || running || conflict || !valid || !data.can_check}
onClick={() => void check()}
>
写回 Description 并检查
</Button>
<Button
disabled={!!busy || running}
onClick={() => {
dirty.current = false;
setDraft(data.descriptions);
setSnapshot(data.snapshot);
setError("");
}}
>
载入最新描述
</Button>
</div>
{data.job && (
<div role="status">
<p>
最近任务:{jobStateLabels[data.job.status] ?? data.job.status}
{data.job.status === "completed" && ",请在“指标与检查”查看结果"}
{data.job.next_retry_at &&
` · 下次轮询:${formatTime(data.job.next_retry_at)}`}
</p>
{data.job.error && (
<Banner type="danger" description={data.job.error} />
)}
<Button onClick={onTask}>查看后台任务</Button>
</div>
)}
</div>
);
}