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
+33 -16
View File
@@ -52,6 +52,9 @@ export default function App() {
const [backtestContext, setBacktestContext] = useState<PageContext>({
page: "backtests",
});
const [datasetContext, setDatasetContext] = useState<PageContext>({
page: "datasets",
});
const [aiAction, setAIAction] = useState<UIAction | null>(null);
const chatOffset = viewport >= 1440 && chatOpen ? chatWidth : 0;
const focusBusiness = useCallback(() => {
@@ -150,6 +153,25 @@ export default function App() {
location.hash = next;
setPage(next);
};
const handleAction = (action: UIAction) => {
if (action.type === "open_conversation") {
setChatOpen(true);
} else {
focusBusiness();
if (action.type === "open_research_input") {
setChatOpen(false);
changePage("datasets");
} else {
changePage(
action.type === "open_backtest" ||
action.type === "open_backtest_preview"
? "backtests"
: "alphas",
);
}
}
setAIAction(action);
};
const logout = async () => {
try {
await post("/auth/logout");
@@ -160,6 +182,8 @@ export default function App() {
setJobs([]);
setAIAction(null);
setAlphaContext({ page: "alphas" });
setDatasetContext({ page: "datasets" });
setBacktestContext({ page: "backtests" });
} catch (e) {
Toast.error((e as Error).message);
}
@@ -301,6 +325,8 @@ export default function App() {
</div>
<div className="alpha-page-view" hidden={page !== "datasets"}>
<DatasetPage
onContext={setDatasetContext}
action={aiAction}
account={account}
jobs={jobs}
active={page === "datasets"}
@@ -313,6 +339,7 @@ export default function App() {
</div>
<div className="alpha-page-view" hidden={page !== "alphas"}>
<AlphaPage
onAction={handleAction}
taskPanelOpen={showJobs}
account={account}
version={`${refreshKey}:${completedVersion}`}
@@ -342,11 +369,7 @@ export default function App() {
timezone={account?.timezone}
action={aiAction}
onContext={setBacktestContext}
onAction={(action) => {
focusBusiness();
changePage("alphas");
setAIAction(action);
}}
onAction={handleAction}
/>
)}
</div>
@@ -384,6 +407,7 @@ export default function App() {
)}
<div inert={catalogModal && !chatOpen}>
<ChatPanel
action={aiAction}
jobs={jobs}
open={chatOpen}
width={chatWidth}
@@ -394,7 +418,9 @@ export default function App() {
? alphaContext
: page === "backtests"
? backtestContext
: { page: page === "datasets" ? "datasets" : "account" }
: page === "datasets"
? datasetContext
: { page: "account" }
}
timezone={account?.timezone}
onSettings={() => {
@@ -407,16 +433,7 @@ export default function App() {
);
}}
onChanged={actionDone}
onAction={(action) => {
focusBusiness();
changePage(
action.type === "open_backtest" ||
action.type === "open_backtest_preview"
? "backtests"
: "alphas",
);
setAIAction(action);
}}
onAction={handleAction}
/>
</div>
</div>
+35 -2
View File
@@ -19,6 +19,7 @@ import {
stateLabels,
} from "../api";
import { BacktestToolCard } from "../backtests/BacktestToolCard";
import { CatalogToolCard } from "../research/CatalogToolCard";
import { PnlChart } from "../components/PnlChart";
import type { Alpha, Job, Pnl, Research } from "../types";
import { chatTransport } from "./transport";
@@ -45,6 +46,7 @@ export function ChatPanel({
onWidth,
timezone,
jobs,
action,
}: {
open: boolean;
context: PageContext;
@@ -56,6 +58,7 @@ export function ChatPanel({
onWidth: (width: number) => void;
timezone?: string;
jobs: Job[];
action: UIAction | null;
}) {
const [settings, setSettings] = useState<ModelSettings | null>(null);
const [conversations, setConversations] = useState<Conversation[]>([]);
@@ -179,6 +182,32 @@ export function ChatPanel({
setLoading(true);
void refreshConversation().finally(() => setLoading(false));
}, [conversationId, refreshConversation]);
useEffect(() => {
if (action?.type !== "open_conversation") return;
let live = true;
void api<ConversationDetail>(
`/ai/conversations/${encodeURIComponent(action.conversation_id)}`,
)
.then(async (detail) => {
if (!live) return;
await chat.stop();
if (!live) return;
setConversations((items) =>
items.some((item) => item.id === detail.id)
? items
: [{ id: detail.id, title: detail.title }, ...items],
);
setConversationId(detail.id);
setText("");
setFailure("");
})
.catch((e) => {
if (live) setFailure(e.message);
});
return () => {
live = false;
};
}, [action]);
const streaming = chat.status === "streaming" || chat.status === "submitted";
const activeRun = runs.find((run) =>
["running", "waiting_approval"].includes(run.status),
@@ -375,7 +404,10 @@ export function ChatPanel({
{!chat.messages.length && (
<div className="ai-empty">
<h3>从当前研究出发</h3>
<p>可以让我筛选 Alpha、解释已有指标,或提出研究记录修改。</p>
<p>
可以让我选择数据字段、构建候选并预览回测,也可以查询 Alpha
和已有结果。
</p>
{!conversationId && (
<Button
type="tertiary"
@@ -440,7 +472,7 @@ export function ChatPanel({
{context.page === "backtests"
? "上下文:回测研究"
: context.page === "datasets"
? "上下文:数据目录(未发送字段与备注)"
? `上下文:${context.dataset_id ?? "数据目录"}${context.catalog_scope ? ` · ${context.catalog_scope.region}/${context.catalog_scope.universe}/D${context.catalog_scope.delay}` : ""}${context.template_input_id ? " · 固定研究输入" : context.unsaved_field_selection ? " · 请先保存字段选择" : ""}(不发送未保存备注)`
: context.page === "account"
? "上下文:个人信息页"
: `上下文:${context.alpha_id ? `Alpha ${context.alpha_id}` : "Alpha 列表"}${context.selected_ids?.length ? ` · 已选 ${context.selected_ids.length} 条` : ""}`}
@@ -547,6 +579,7 @@ function BusinessCard({
{call.name.includes("backtest") && (
<BacktestToolCard call={call} onAction={onAction} />
)}
<CatalogToolCard call={call} onAction={onAction} />
{call.preview.targets?.map((target) => (
<details
key={target.alpha_id}
+21
View File
@@ -12,6 +12,17 @@ export type ModelSettings = {
};
export type PageContext = {
page: "alphas" | "account" | "datasets" | "backtests";
catalog_scope?: {
instrument_type: string;
region: string;
universe: string;
delay: number;
};
dataset_id?: string;
field_id?: string;
collection_version?: string;
template_input_id?: string;
unsaved_field_selection?: boolean;
backtest_run_id?: string;
backtest_preview_id?: string;
backtest_draft_id?: string;
@@ -23,6 +34,8 @@ export type AlphaUIAction =
| { type: "open_alpha"; alpha_id: string; nonce: number }
| { type: "apply_filters"; filters: Record<string, unknown>; nonce: number };
export type UIAction =
| { type: "open_conversation"; conversation_id: string; nonce: number }
| { type: "open_research_input"; input_id: string; nonce: number }
| { type: "open_backtest"; run_id: string; nonce: number }
| { type: "open_backtest_preview"; preview_id: string; nonce: number }
| { type: "open_alpha"; alpha_id: string; nonce: number }
@@ -75,6 +88,14 @@ export const runLabels: Record<string, string> = {
interrupted: "执行中断",
};
export const toolLabels: Record<string, string> = {
get_catalog_scopes: "读取研究范围",
search_catalog: "查询数据集与字段",
get_catalog_detail: "读取数据详情",
prepare_research_input: "固定研究输入",
get_research_input: "读取固定研究输入",
prepare_research_backtest: "构建研究候选与预览",
get_backtest_draft: "读取候选草稿",
get_alpha_sources: "查询 Alpha 研究来源",
get_backtest_capabilities: "读取回测能力",
prepare_backtest: "准备回测预览",
get_backtest_preview: "查看回测预览",
+35 -6
View File
@@ -28,6 +28,7 @@ import type {
Source,
} from "./types";
import "./style.css";
import { SourceDetails, sourceLabel } from "../research/SourceDetails";
export function BacktestPage({
active,
@@ -47,6 +48,8 @@ export function BacktestPage({
onContext: (context: PageContext) => void;
}) {
const [view, setView] = useState("runs");
const [sourceFilter, setSourceFilter] = useState("");
const [sourceKinds, setSourceKinds] = useState<string[]>([]);
const [page, setPage] = useState(1);
const [runs, setRuns] = useState<Page<Run>>({
items: [],
@@ -97,15 +100,19 @@ export function BacktestPage({
const [itemDetail, setItemDetail] = useState<Item | null>(null);
const refresh = useCallback(async () => {
const [r, d, c] = await Promise.all([
api<Page<Run>>(`/backtests/runs?offset=${(page - 1) * 25}`),
const [r, d, c, kinds] = await Promise.all([
api<Page<Run>>(
`/backtests/runs?offset=${(page - 1) * 25}&source=${encodeURIComponent(sourceFilter)}`,
),
api<Page<DraftSummary>>(`/backtests/drafts?offset=${(page - 1) * 25}`),
api<Scheduler>("/backtests/config"),
api<string[]>("/backtests/sources"),
]);
setRuns(r);
setDrafts(d);
setConfig(c);
}, [page]);
setSourceKinds(kinds);
}, [page, sourceFilter]);
useEffect(() => {
if (!active) return;
let alive = true;
@@ -293,6 +300,22 @@ export function BacktestPage({
]}
/>
<span className="muted">REGULAR · FASTEXPR</span>
{view === "runs" && (
<Select
aria-label="回测研究来源筛选"
value={sourceFilter || undefined}
placeholder="全部研究来源"
showClear
optionList={sourceKinds.map((kind) => ({
value: kind,
label: sourceLabel(kind),
}))}
onChange={(value) => {
setSourceFilter(String(value ?? ""));
setPage(1);
}}
/>
)}
<div className="backtest-spacer" />
<Button
type="tertiary"
@@ -337,7 +360,11 @@ export function BacktestPage({
</Button>
),
},
{ title: "来源", width: 120, render: (_, r) => r!.source.kind },
{
title: "来源",
width: 120,
render: (_, r) => sourceLabel(r!.source.kind),
},
{
title: "状态",
width: 160,
@@ -438,8 +465,9 @@ export function BacktestPage({
</div>
<p>
{preview.name} · {preview.total} 条候选 · {preview.batch_count}{" "}
个平台批次 · 来源 {preview.source.kind}
个平台批次 · 来源 {sourceLabel(preview.source.kind)}
</p>
<SourceDetails source={preview.source} onAction={onAction} />
{preview.duplicate_count > 0 && (
<Banner
type="warning"
@@ -763,10 +791,11 @@ export function BacktestPage({
/>
)}
<p className="muted">
来源 {run.source.kind}{" "}
来源 {sourceLabel(run.source.kind)}{" "}
{run.source.batch_id ? `· 批次 ${run.source.batch_id}` : ""} ·{" "}
{formatTime(run.created_at, timezone)}
</p>
<SourceDetails source={run.source} onAction={onAction} />
{run.scheduler.blocked_reason && (
<Banner
type="warning"
@@ -4,6 +4,8 @@ import type { ToolCard, UIAction } from "../ai/types";
import { api } from "../api";
import { controlLabels, labels } from "./types";
import type { Run } from "./types";
import type { Source } from "./types";
import { SourceDetails } from "../research/SourceDetails";
export function BacktestToolCard({
call,
@@ -54,6 +56,12 @@ export function BacktestToolCard({
{String(preview.batch_count)} 个批次
</p>
<p>重复提示 {String(preview.duplicate_count)} 条,确认后独立执行。</p>
{preview.source && (
<SourceDetails
source={preview.source as Source}
onAction={onAction}
/>
)}
<Button
onClick={() =>
onAction({
+1
View File
@@ -41,6 +41,7 @@ export type Source = {
template_input_id?: string | null;
research_id?: string | null;
parent_run_id?: string | null;
hypothesis?: string | null;
};
export type Draft = {
id: string;
+15
View File
@@ -32,6 +32,8 @@ import type {
} from "../types";
import { PnlChart } from "./PnlChart";
import { SelfCorrelationPanel } from "./SelfCorrelationPanel";
import { AlphaSources } from "../research/AlphaSources";
import type { UIAction } from "../ai/types";
export function AlphaDetail({
id,
@@ -41,6 +43,7 @@ export function AlphaDetail({
onClose,
onSaved,
onTask,
onAction,
suspended = false,
chatOffset = 0,
}: {
@@ -51,6 +54,7 @@ export function AlphaDetail({
onClose: () => void;
onSaved: () => void;
onTask: () => void;
onAction: (action: UIAction) => void;
suspended?: boolean;
chatOffset?: number;
}) {
@@ -316,6 +320,17 @@ export function AlphaDetail({
/>
)}
</TabPane>
<TabPane tab="研究来源" itemKey="sources">
{tab === "sources" && id && (
<AlphaSources
key={id}
id={id}
version={version}
timezone={timezone}
onAction={onAction}
/>
)}
</TabPane>
<TabPane tab="研究记录" itemKey="research">
<div className="detail-section">
<p className="muted">
+38 -1
View File
@@ -38,6 +38,8 @@ import type {
import { AlphaDetail } from "../components/AlphaDetail";
import { AlphaSyncDialog } from "../components/AlphaSyncDialog";
import type { PageContext, AlphaUIAction as UIAction } from "../ai/types";
import type { UIAction as WorkspaceAction } from "../ai/types";
import { sourceLabel } from "../research/SourceDetails";
const metricLabels = {
sharpe: "Sharpe",
@@ -57,8 +59,10 @@ const initialColumns = [
"turnover",
"correlation",
"research",
"source",
];
const columnLabels: Record<string, string> = {
source: "研究来源",
name: "Alpha",
expression: "表达式",
region: "地区 / Universe",
@@ -89,6 +93,7 @@ export function AlphaPage({
onContext,
action,
onOverlay,
onAction,
}: {
account: Account | null;
taskPanelOpen: boolean;
@@ -101,6 +106,7 @@ export function AlphaPage({
onContext: (context: PageContext) => void;
action: UIAction | null;
onOverlay: () => void;
onAction: (action: WorkspaceAction) => void;
}) {
const [data, setData] = useState<Page>({
items: [],
@@ -225,6 +231,7 @@ export function AlphaPage({
}, [action, onOverlay]);
const refresh = useCallback(() => setLocalVersion((n) => n + 1), []);
useEffect(() => {
if (!active) return;
const controller = new AbortController();
setLoading(true);
setError("");
@@ -245,7 +252,7 @@ export function AlphaPage({
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [query, version, localVersion]);
}, [active, query, version, localVersion]);
useEffect(() => {
setSelected([]);
tablePanel.current?.querySelector(".semi-table-body")?.scrollTo({ top: 0 });
@@ -348,6 +355,13 @@ export function AlphaPage({
</code>
),
},
{
key: "source",
title: "研究来源",
width: 150,
render: (_, row) =>
row!.source_kinds?.map(sourceLabel).join("、") || "暂无本地来源",
},
{
key: "region",
title: "地区 / Universe",
@@ -594,6 +608,28 @@ export function AlphaPage({
{activeFilterCount ? `· ${activeFilterCount} 项已应用` : ""}
</summary>
<div className="advanced-grid">
<label>
研究来源
<Select
aria-label="研究来源筛选"
showClear
value={draft.source || undefined}
optionList={options("source").map((option) => ({
...option,
label: sourceLabel(option.value),
}))}
placeholder="全部来源"
onChange={(v) => updateDraft("source", v)}
/>
</label>
<label>
研究编号
<Input
aria-label="研究编号筛选"
value={draft.research_id || ""}
onChange={(v) => updateDraft("research_id", v)}
/>
</label>
{[
["universe", "Universe"],
["alpha_type", "Alpha 类型"],
@@ -938,6 +974,7 @@ export function AlphaPage({
/>
)}
<AlphaDetail
onAction={onAction}
suspended={overlaySuspended}
chatOffset={chatOffset}
taskPanelOpen={taskPanelOpen}
+83 -2
View File
@@ -26,6 +26,7 @@ import {
queryString,
} from "../api";
import type { Account, Job } from "../types";
import type { PageContext, UIAction } from "../ai/types";
import "./dataset.css";
type Scope = {
@@ -66,6 +67,7 @@ type Draft = {
dataset_id: string;
scope: Scope;
field_ids: string[];
field_types: Record<string, string | null>;
collection_version: string;
selection: string;
created_at: string;
@@ -114,6 +116,8 @@ export function DatasetPage({
onTask,
onModal,
onChat,
onContext,
action,
}: {
account: Account | null;
version: string;
@@ -123,6 +127,8 @@ export function DatasetPage({
onTask: () => void;
onModal: (value: boolean) => void;
onChat: () => void;
onContext: (context: PageContext) => void;
action: UIAction | null;
}) {
const [scope, setScope] = useState<Scope>({
instrument_type: "EQUITY",
@@ -185,6 +191,61 @@ export function DatasetPage({
);
const total = selected?.complete_count ?? 0;
const chosen = Math.max(0, total - excluded.size);
const contextDraft = inputOpen ? draft : null;
useEffect(() => {
if (!active) return;
onContext({
page: "datasets",
catalog_scope: contextDraft?.scope ?? scope,
dataset_id: contextDraft?.dataset_id ?? selected?.id,
field_id: contextDraft ? undefined : detail?.field,
collection_version:
contextDraft?.collection_version ??
selected?.collection_version ??
undefined,
template_input_id: contextDraft?.id,
unsaved_field_selection: !contextDraft && excluded.size > 0,
});
}, [
active,
scopeQuery,
contextDraft,
selected?.id,
selected?.collection_version,
detail?.field,
excluded.size,
onContext,
]);
useEffect(() => {
if (action?.type !== "open_research_input") return;
let live = true;
void api<Draft>(`/catalog/inputs/${encode(action.input_id)}`)
.then((saved) => {
if (!live) return;
changeScope(saved.scope);
setDraft(saved);
setInputOpen(true);
})
.catch((e) => {
if (live) setError(e.message);
});
return () => {
live = false;
};
}, [action]);
const researchInput = (saved: Draft) => {
setDraft(saved);
setHistoryOpen(false);
setInputOpen(true);
onContext({
page: "datasets",
catalog_scope: saved.scope,
dataset_id: saved.dataset_id,
collection_version: saved.collection_version,
template_input_id: saved.id,
});
onChat();
};
const top = historyOpen
? "history"
: inputOpen
@@ -1131,6 +1192,11 @@ export function DatasetPage({
保存输入草稿
</Button>
)}
{draft && (
<Button theme="solid" onClick={() => researchInput(draft)}>
用此输入研究
</Button>
)}
</div>
<div className="catalog-details-body">
<h2>{draft ? "输入草稿已保存" : selected?.name}</h2>
@@ -1140,10 +1206,24 @@ export function DatasetPage({
: `${preparation.current?.count} 个字段`}
</p>
<p>
{scope.region} · {scope.universe} · Delay {scope.delay}
{(draft?.scope ?? scope).region} ·{" "}
{(draft?.scope ?? scope).universe} · Delay{" "}
{(draft?.scope ?? scope).delay}
</p>
<p>模板消费方尚未接入</p>
<p>保存后可交给 Chatbox 研究,构建候选并预览回测。</p>
{draft && <p>草稿 ID:{draft.id}</p>}
{draft && (
<details>
<summary>查看固定字段集合</summary>
<p>
{draft.field_ids
.map(
(id) => `${id} (${draft.field_types[id] ?? "类型未知"})`,
)
.join(", ")}
</p>
</details>
)}
<p>
集合版本:
{draft?.collection_version ?? preparation.current?.version}
@@ -1177,6 +1257,7 @@ export function DatasetPage({
{d.scope.delay}
</p>
<p>{formatTime(d.created_at, account?.timezone)}</p>
<Button onClick={() => researchInput(d)}>用此输入研究</Button>
<details>
<summary>查看固定字段集合</summary>
<p>{d.field_ids.join(", ")}</p>
+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;
}
+1
View File
@@ -57,6 +57,7 @@ export type Alpha = {
synced_at: string;
research: Research;
local_correlation: CorrelationSummary | null;
source_kinds: string[];
};
export type AlphaDetail = Alpha & {
expression: string | null;