refactor: unify AI capabilities and workspace integration
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Toast } from "@douyinfe/semi-ui-19";
|
||||
import { api, formatNumber, formatTime, stateLabels } from "../api";
|
||||
import { PnlChart } from "../components/PnlChart";
|
||||
import type { Alpha, Pnl, Research } from "../types";
|
||||
import type { ToolCardProps } from "./types";
|
||||
|
||||
export function AlphaToolCard({ call, onAction, timezone }: ToolCardProps) {
|
||||
const result = call.result ?? {};
|
||||
const items = Array.isArray(result.items) ? result.items : [];
|
||||
const isSearch = call.name === "search_alphas";
|
||||
const alphas = (
|
||||
isSearch ? items : call.name === "get_alpha" && result.id ? [result] : []
|
||||
) as Alpha[];
|
||||
const openAlpha = (id: string) =>
|
||||
onAction({ type: "open_alpha", alpha_id: id, nonce: Date.now() });
|
||||
return (
|
||||
<>
|
||||
{call.preview.targets?.map((target) => (
|
||||
<details
|
||||
key={target.alpha_id}
|
||||
open={call.preview.targets!.length === 1}
|
||||
>
|
||||
<summary>{target.alpha_id} 的修改</summary>
|
||||
{(["note", "tags", "favorite", "state"] as const)
|
||||
.filter(
|
||||
(key) =>
|
||||
JSON.stringify(target.before[key]) !==
|
||||
JSON.stringify(target.after[key]),
|
||||
)
|
||||
.map((key) => (
|
||||
<div className="ai-diff" key={key}>
|
||||
<strong>
|
||||
{
|
||||
{
|
||||
note: "备注",
|
||||
tags: "标签",
|
||||
favorite: "收藏",
|
||||
state: "研究状态",
|
||||
}[key]
|
||||
}
|
||||
</strong>
|
||||
<del>
|
||||
<span>修改前</span>
|
||||
{researchValue(key, target.before[key])}
|
||||
</del>
|
||||
<ins>
|
||||
<span>修改后</span>
|
||||
{researchValue(key, target.after[key])}
|
||||
</ins>
|
||||
</div>
|
||||
))}
|
||||
</details>
|
||||
))}
|
||||
{alphas.length > 0 && (
|
||||
<div className="ai-alpha-results">
|
||||
{alphas.map((alpha) => (
|
||||
<div key={alpha.id}>
|
||||
<button className="text-link" onClick={() => openAlpha(alpha.id)}>
|
||||
{alpha.name || alpha.id}
|
||||
</button>
|
||||
<small>
|
||||
{alpha.id} · {alpha.region ?? "地区未提供"}
|
||||
</small>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sharpe</th>
|
||||
<th>Fitness</th>
|
||||
<th>Turnover</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{formatNumber(alpha.sharpe)}</td>
|
||||
<td>{formatNumber(alpha.fitness)}</td>
|
||||
<td>
|
||||
{alpha.turnover == null
|
||||
? "未提供"
|
||||
: `${formatNumber(alpha.turnover * 100)}%`}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<small>本地快照 · {formatTime(alpha.synced_at, timezone)}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isSearch && (
|
||||
<p>
|
||||
共 {String(result.total ?? 0)} 条,当前返回 {items.length} 条。
|
||||
{!!result.filters && (
|
||||
<button
|
||||
className="text-link"
|
||||
onClick={() =>
|
||||
onAction({
|
||||
type: "apply_filters",
|
||||
filters: result.filters as Record<string, unknown>,
|
||||
nonce: Date.now(),
|
||||
})
|
||||
}
|
||||
>
|
||||
应用到列表
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{call.name === "get_alpha_facets" && call.status === "completed" && (
|
||||
<p>
|
||||
已同步 {String(result.total ?? 0)} 条 Alpha · 收藏{" "}
|
||||
{String(result.favorites ?? 0)} 条<br />
|
||||
地区:
|
||||
{Array.isArray(result.region) ? result.region.join("、") : "未提供"}
|
||||
</p>
|
||||
)}
|
||||
{call.name === "get_alpha_pnl" && call.status === "completed" && (
|
||||
<ChatPnl result={result} timezone={timezone} />
|
||||
)}
|
||||
{result.ok === true && (
|
||||
<p>
|
||||
操作已保存。
|
||||
{typeof result.alpha_id === "string" && (
|
||||
<button
|
||||
className="text-link"
|
||||
onClick={() => openAlpha(result.alpha_id as string)}
|
||||
>
|
||||
打开 Alpha
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{typeof result.updated === "number" && (
|
||||
<p>已更新 {result.updated} 条研究记录。</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
function researchValue(key: keyof Research, value: unknown) {
|
||||
if (key === "favorite") return value ? "已收藏" : "未收藏";
|
||||
if (key === "state")
|
||||
return (
|
||||
stateLabels[String(value) as keyof typeof stateLabels] ?? String(value)
|
||||
);
|
||||
if (Array.isArray(value)) return value.join("、") || "无";
|
||||
return String(value || "空");
|
||||
}
|
||||
|
||||
function ChatPnl({
|
||||
result,
|
||||
timezone,
|
||||
}: {
|
||||
result: Record<string, unknown>;
|
||||
timezone?: string;
|
||||
}) {
|
||||
const [pnl, setPnl] = useState<Pnl | null>(null);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (result.cached && typeof result.alpha_id === "string")
|
||||
api<Pnl>(`/alphas/${encodeURIComponent(result.alpha_id)}/pnl`)
|
||||
.then((value) => {
|
||||
if (active) setPnl(value);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (active) Toast.error(error.message);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [result.alpha_id, result.fetched_at, result.cached]);
|
||||
return result.cached ? (
|
||||
<div>
|
||||
{pnl && <PnlChart points={pnl.points} />}
|
||||
<small>
|
||||
{String(result.count)} 条记录 · 缓存于{" "}
|
||||
{formatTime(result.fetched_at as string, timezone)}
|
||||
</small>
|
||||
</div>
|
||||
) : (
|
||||
<p>尚无 PnL 缓存,可以请求创建 PnL 刷新任务。</p>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user