feat: integrate chatbox research with datasets and backtests
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.research-source {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.research-source .inline-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
Reference in New Issue
Block a user