feat: run fixed research pipelines within durable budgets
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Banner, Button, Pagination } from "@douyinfe/semi-ui-19";
|
||||
import { api, formatTime } from "../api";
|
||||
import type { PageContext, UIAction } from "../ai/types";
|
||||
import { FlowLaunchForm } from "./FlowLaunchForm";
|
||||
import { FlowRunView } from "./FlowRunView";
|
||||
import type { FlowRun } from "./flowTypes";
|
||||
import { flowStatus } from "./flowTypes";
|
||||
import "./workspace.css";
|
||||
export function PipelinePage({
|
||||
active,
|
||||
onAction,
|
||||
onContext,
|
||||
}: {
|
||||
active: boolean;
|
||||
onAction: (a: UIAction) => void;
|
||||
onContext: (c: PageContext) => void;
|
||||
}) {
|
||||
const [items, setItems] = useState<FlowRun[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [selected, setSelected] = useState<string | null>(() =>
|
||||
localStorage.getItem("research-selected-flow"),
|
||||
);
|
||||
const [run, setRun] = useState<FlowRun | null>(null);
|
||||
const [creating, setCreating] = useState(!selected);
|
||||
const [revision, setRevision] = useState(0);
|
||||
const [error, setError] = useState("");
|
||||
useEffect(() => {
|
||||
if (active) onContext({ page: "pipeline", research_run_id: selected });
|
||||
}, [active, selected, onContext]);
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const c = new AbortController();
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
async function refresh() {
|
||||
try {
|
||||
const list = await api<{ items: FlowRun[]; total: number }>(
|
||||
`/research/flows/runs?offset=${(page - 1) * 25}`,
|
||||
{ signal: c.signal },
|
||||
);
|
||||
setItems(list.items);
|
||||
setTotal(list.total);
|
||||
if (selected)
|
||||
setRun(
|
||||
await api(`/research/flows/runs/${selected}`, { signal: c.signal }),
|
||||
);
|
||||
setError("");
|
||||
} catch (e) {
|
||||
if (!c.signal.aborted) setError((e as Error).message);
|
||||
} finally {
|
||||
if (!c.signal.aborted) timer = setTimeout(() => void refresh(), 1500);
|
||||
}
|
||||
}
|
||||
void refresh();
|
||||
return () => {
|
||||
c.abort();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [active, page, selected, revision]);
|
||||
function select(id: string) {
|
||||
setSelected(id);
|
||||
localStorage.setItem("research-selected-flow", id);
|
||||
setCreating(false);
|
||||
}
|
||||
return (
|
||||
<section className="research-workspace">
|
||||
<div className="section-toolbar">
|
||||
<div>
|
||||
<h2>研究流水线</h2>
|
||||
<p className="muted">
|
||||
生成 → 校验与设参 → 回测 → 评估决策 → 增强 → 重新展开
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setCreating(true)}>新建自动研究</Button>
|
||||
</div>
|
||||
{error && <Banner type="danger" description={error} />}
|
||||
<div className="research-layout">
|
||||
<aside className="research-library">
|
||||
<h3>研究运行</h3>
|
||||
{items.map((item) => (
|
||||
<button
|
||||
className={`research-library-item ${selected === item.id ? "selected" : ""}`}
|
||||
key={item.id}
|
||||
onClick={() => select(item.id)}
|
||||
>
|
||||
<strong>{item.name}</strong>
|
||||
<span>
|
||||
{flowStatus[item.status]} · 第 {item.round} 轮
|
||||
</span>
|
||||
<span>{formatTime(item.created_at)}</span>
|
||||
</button>
|
||||
))}
|
||||
<Pagination
|
||||
size="small"
|
||||
total={total}
|
||||
currentPage={page}
|
||||
pageSize={25}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</aside>
|
||||
<div className="research-main">
|
||||
{creating ? (
|
||||
<FlowLaunchForm
|
||||
key="new-flow"
|
||||
onStarted={(r) => {
|
||||
select(r.id);
|
||||
setRun(r);
|
||||
setRevision((v) => v + 1);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
run && (
|
||||
<FlowRunView
|
||||
run={run}
|
||||
onRefresh={() => setRevision((v) => v + 1)}
|
||||
onAction={onAction}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user