feat: integrate chatbox research with datasets and backtests

This commit is contained in:
yuxuanhui
2026-09-08 12:43:00 +08:00
parent 43336ad960
commit aef8e1d310
37 changed files with 1421 additions and 42 deletions
+84
View File
@@ -0,0 +1,84 @@
import { useEffect, useState } from "react";
import { Banner, Button, Empty, Pagination, Spin } from "@douyinfe/semi-ui-19";
import { api, formatTime } from "../api";
import type { Source, Page } from "../backtests/types";
import type { UIAction } from "../ai/types";
import { SourceDetails } from "./SourceDetails";
type Origin = {
item_id: string;
backtest_run_id: string;
name: string;
source: Source;
observed_at: string;
};
export function AlphaSources({
id,
version,
timezone,
onAction,
}: {
id: string;
version: string;
timezone?: string;
onAction: (action: UIAction) => void;
}) {
const [page, setPage] = useState(1);
const [data, setData] = useState<Page<Origin> | null>(null);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
setError("");
setData(null);
api<Page<Origin>>(
`/alphas/${encodeURIComponent(id)}/sources?offset=${(page - 1) * 25}`,
{ signal: controller.signal },
)
.then((value) => {
if (!controller.signal.aborted) setData(value);
})
.catch((e) => {
if (!controller.signal.aborted) setError(e.message);
});
return () => controller.abort();
}, [id, page, version]);
if (error) return <Banner type="danger" description={error} />;
if (!data) return <Spin />;
return (
<>
<p className="muted">
保留每次已保存回测的研究来源;平台同步不会覆盖这些记录。
</p>
{data.items.map((item) => (
<article key={item.item_id}>
<Button
onClick={() =>
onAction({
type: "open_backtest",
run_id: item.backtest_run_id,
nonce: Date.now(),
})
}
>
{item.name}
</Button>
<span className="muted">
{" "}
· {formatTime(item.observed_at, timezone)}
</span>
<SourceDetails source={item.source} onAction={onAction} />
</article>
))}
{!data.total && <Empty description="暂无本地回测研究来源" />}
{data.total > 25 && (
<Pagination
currentPage={page}
pageSize={25}
total={data.total}
onPageChange={setPage}
/>
)}
</>
);
}
+93
View File
@@ -0,0 +1,93 @@
import { Button } from "@douyinfe/semi-ui-19";
import type { ToolCard, UIAction } from "../ai/types";
import type { Source } from "../backtests/types";
import { SourceDetails } from "./SourceDetails";
export function CatalogToolCard({
call,
onAction,
}: {
call: ToolCard;
onAction: (action: UIAction) => void;
}) {
if (call.status !== "completed" || !call.result || call.result.error)
return null;
const result = call.result;
const items = Array.isArray(result.items)
? (result.items as Record<string, unknown>[])
: [];
if (call.name === "get_alpha_sources")
return (
<>
<p>
本页 {items.length} / 共 {String(result.total)} 条来源记录
</p>
{items.map((item) => (
<div key={String(item.item_id)}>
<Button
onClick={() =>
onAction({
type: "open_backtest",
run_id: String(item.backtest_run_id),
nonce: Date.now(),
})
}
>
{String(item.name)}
</Button>
<SourceDetails source={item.source as Source} onAction={onAction} />
</div>
))}
</>
);
const paged = [
"search_catalog",
"get_research_input",
"prepare_research_input",
].includes(call.name);
if (paged)
return (
<>
<p>
{String(result.dataset_id ?? "数据目录")} · 本页 {items.length} / 匹配{" "}
{String(result.total)} 条
</p>
{typeof result.field_count === "number" && (
<p>固定输入共 {result.field_count} 个字段</p>
)}
<ul>
{items.map((item) => (
<li key={String(item.id)}>
{String(item.id)} · {String(item.name ?? "未命名")}{" "}
{item.field_type ? `· ${item.field_type}` : ""}
</li>
))}
</ul>
{call.name !== "search_catalog" && typeof result.id === "string" && (
<Button
onClick={() =>
onAction({
type: "open_research_input",
input_id: String(result.id),
nonce: Date.now(),
})
}
>
查看固定研究输入
</Button>
)}
</>
);
if (
["get_catalog_detail", "get_catalog_scopes", "get_backtest_draft"].includes(
call.name,
)
)
return (
<details>
<summary>查看读取内容</summary>
<pre>{JSON.stringify(result, null, 2)}</pre>
</details>
);
return null;
}
+70
View File
@@ -0,0 +1,70 @@
import { Button } from "@douyinfe/semi-ui-19";
import type { Source } from "../backtests/types";
import type { UIAction } from "../ai/types";
import "./style.css";
export const sourceLabel = (kind: string) =>
({ chatbox: "Chatbox 研究", manual: "手工研究", ai: "AI 研究(历史)" })[
kind
] ?? kind;
export function SourceDetails({
source,
onAction,
}: {
source: Source;
onAction: (action: UIAction) => void;
}) {
return (
<div className="detail-section research-source">
<p>研究来源:{sourceLabel(source.kind)}</p>
{source.hypothesis && <p>{source.hypothesis}</p>}
{source.batch_id && <p>业务批次:{source.batch_id}</p>}
{source.research_id && <p>研究编号:{source.research_id}</p>}
{source.reference && source.kind !== "chatbox" && (
<p>来源引用:{source.reference}</p>
)}
<div className="inline-actions">
{source.kind === "chatbox" && source.reference && (
<Button
onClick={() =>
onAction({
type: "open_conversation",
conversation_id: source.reference!,
nonce: Date.now(),
})
}
>
打开研究会话
</Button>
)}
{source.template_input_id && (
<Button
onClick={() =>
onAction({
type: "open_research_input",
input_id: source.template_input_id!,
nonce: Date.now(),
})
}
>
查看研究输入
</Button>
)}
{source.parent_run_id && (
<Button
onClick={() =>
onAction({
type: "open_backtest",
run_id: source.parent_run_id!,
nonce: Date.now(),
})
}
>
查看原回测
</Button>
)}
</div>
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
.research-source {
min-width: 0;
overflow-wrap: anywhere;
}
.research-source .inline-actions {
flex-wrap: wrap;
}