feat: add AI descriptions and platform submission checks
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user