224 lines
7.1 KiB
TypeScript
224 lines
7.1 KiB
TypeScript
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 Submission = {
|
|
snapshot: string;
|
|
sections: Record<string, { code: string | null; description: string }>;
|
|
descriptions: Record<string, string>;
|
|
model: string;
|
|
can_generate: boolean;
|
|
can_check: boolean;
|
|
job: Job | null;
|
|
};
|
|
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, string>>({});
|
|
const [snapshot, setSnapshot] = useState("");
|
|
const [busy, setBusy] = useState("");
|
|
const [error, setError] = useState("");
|
|
const dirty = useRef(false);
|
|
const submittedJob = useRef<string | null>(null);
|
|
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);
|
|
// Rebase only our completed writeback and its exact resulting snapshot.
|
|
// A different expression/settings/description must still require review.
|
|
if (
|
|
value.job?.id === submittedJob.current &&
|
|
value.job?.status === "completed" &&
|
|
value.job.checkpoint.review_snapshot === value.snapshot
|
|
) {
|
|
setSnapshot(value.snapshot);
|
|
submittedJob.current = null;
|
|
}
|
|
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, string> }>(
|
|
`/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;
|
|
submittedJob.current = job.id;
|
|
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;
|
|
return (
|
|
<div className="detail-section">
|
|
<h3>Description 与提交检查</h3>
|
|
<p className="muted">
|
|
AI 一次生成完整 Description,可在下方统一修改。平台检查会更新 BRAIN 的
|
|
Description,随后获取平台提交检查结果,不会正式提交 Alpha。
|
|
</p>
|
|
{error && <Banner type="danger" description={error} />}
|
|
{!data.can_check && (
|
|
<Banner
|
|
type="info"
|
|
description="平台检查需要待提交 Alpha。有本地比较基准时,需取得有效、完整且低于阈值的自相关结果;没有本地基准时可直接继续平台检查。"
|
|
/>
|
|
)}
|
|
{!data.can_generate && (
|
|
<p className="muted">
|
|
请在个人信息 → 大模型服务中配置 基础信息处理模型。也可以手动填写。
|
|
</p>
|
|
)}
|
|
<div className="inline-actions">
|
|
<Button
|
|
disabled={!!busy || running}
|
|
onClick={() => {
|
|
dirty.current = false;
|
|
setDraft(data.descriptions);
|
|
setSnapshot(data.snapshot);
|
|
setError("");
|
|
}}
|
|
>
|
|
载入最新描述
|
|
</Button>
|
|
<Button
|
|
onClick={() => void generate()}
|
|
loading={busy === "generate"}
|
|
disabled={!!busy || running || conflict || !data.can_generate}
|
|
>
|
|
AI 生成 Description
|
|
</Button>
|
|
<Button
|
|
theme="solid"
|
|
loading={busy === "check"}
|
|
disabled={!!busy || running || conflict || !data.can_check}
|
|
onClick={() => void check()}
|
|
>
|
|
平台检查
|
|
</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>
|
|
)}
|
|
<label>
|
|
完整 Description
|
|
<TextArea
|
|
aria-label={`${section} Description`}
|
|
value={value}
|
|
rows={7}
|
|
placeholder={
|
|
"Idea: 策略假设\n\nRationale for data used: 数据选择理由\n\nRationale for operators used: 算子使用理由"
|
|
}
|
|
disabled={!!busy || running}
|
|
onChange={(text) => {
|
|
dirty.current = true;
|
|
setDraft((current) => ({ ...current, [section]: text }));
|
|
}}
|
|
/>
|
|
</label>
|
|
<p className="muted">
|
|
总长度:{Array.from(value).length} 字符。可直接修改生成内容后写回。
|
|
</p>
|
|
</section>
|
|
))}
|
|
{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} />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|