feat: add AI research chatbot with confirmed business tools
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "../api";
|
||||
import type { Account, Alpha, AlphaPage as Page, Facets } from "../types";
|
||||
import { AlphaDetail } from "../components/AlphaDetail";
|
||||
import type { PageContext, UIAction } from "../ai/types";
|
||||
|
||||
const metricLabels = {
|
||||
sharpe: "Sharpe",
|
||||
@@ -77,12 +78,24 @@ export function AlphaPage({
|
||||
version,
|
||||
onTask,
|
||||
onAccount,
|
||||
active,
|
||||
overlaySuspended,
|
||||
chatOffset,
|
||||
onContext,
|
||||
action,
|
||||
onOverlay,
|
||||
}: {
|
||||
account: Account | null;
|
||||
taskPanelOpen: boolean;
|
||||
version: string;
|
||||
onTask: () => void;
|
||||
onAccount: () => void;
|
||||
active: boolean;
|
||||
overlaySuspended: boolean;
|
||||
chatOffset: number;
|
||||
onContext: (context: PageContext) => void;
|
||||
action: UIAction | null;
|
||||
onOverlay: () => void;
|
||||
}) {
|
||||
const [data, setData] = useState<Page>({
|
||||
items: [],
|
||||
@@ -124,6 +137,8 @@ export function AlphaPage({
|
||||
const [idText, setIdText] = useState("");
|
||||
const [busy, setBusy] = useState("");
|
||||
const [bulkOpen, setBulkOpen] = useState(false);
|
||||
const [bulkVersions, setBulkVersions] = useState<Record<string, number>>({});
|
||||
const actionNonce = useRef(0);
|
||||
const [bulk, setBulk] = useState({
|
||||
add_tags: "",
|
||||
remove_tags: "",
|
||||
@@ -147,6 +162,46 @@ export function AlphaPage({
|
||||
[filters, sort, direction, pageSize, page],
|
||||
);
|
||||
const query = queryString(params);
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
onContext({
|
||||
page: "alphas",
|
||||
alpha_id: detailId,
|
||||
selected_ids: selected,
|
||||
filters: Object.fromEntries(
|
||||
Object.entries(params).filter(
|
||||
([, value]) => value !== "" && value !== undefined && value !== null,
|
||||
),
|
||||
),
|
||||
});
|
||||
}, [active, detailId, selected, params, onContext]);
|
||||
useEffect(() => {
|
||||
if (!action || actionNonce.current === action.nonce) return;
|
||||
actionNonce.current = action.nonce;
|
||||
if (action.type === "open_alpha") {
|
||||
setDetailId(action.alpha_id);
|
||||
onOverlay();
|
||||
} else {
|
||||
const {
|
||||
sort: nextSort,
|
||||
direction: nextDirection,
|
||||
limit: _limit,
|
||||
offset: _offset,
|
||||
...values
|
||||
} = action.filters;
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(values)
|
||||
.filter(([, v]) => v !== null && v !== undefined)
|
||||
.map(([k, v]) => [k, String(v)]),
|
||||
);
|
||||
setDraft(next);
|
||||
setFilters(next);
|
||||
setPage(1);
|
||||
setSelected([]);
|
||||
if (nextSort) setSort(String(nextSort));
|
||||
if (nextDirection) setDirection(String(nextDirection));
|
||||
}
|
||||
}, [action, onOverlay]);
|
||||
const refresh = useCallback(() => setLocalVersion((n) => n + 1), []);
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -210,7 +265,8 @@ export function AlphaPage({
|
||||
.filter(Boolean);
|
||||
try {
|
||||
await patch("/alphas/research/bulk", {
|
||||
alpha_ids: selected,
|
||||
alpha_ids: Object.keys(bulkVersions),
|
||||
versions: bulkVersions,
|
||||
add_tags: split(bulk.add_tags),
|
||||
remove_tags: split(bulk.remove_tags),
|
||||
state: bulk.state || null,
|
||||
@@ -233,7 +289,13 @@ export function AlphaPage({
|
||||
fixed: "left",
|
||||
width: 190,
|
||||
render: (_, row) => (
|
||||
<button className="alpha-link" onClick={() => setDetailId(row!.id)}>
|
||||
<button
|
||||
className="alpha-link"
|
||||
onClick={() => {
|
||||
setDetailId(row!.id);
|
||||
onOverlay();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{row!.research.favorite && <IconStar className="favorite-icon" />}
|
||||
{row!.name || row!.id}
|
||||
@@ -569,7 +631,20 @@ export function AlphaPage({
|
||||
<div className="inline-actions">
|
||||
{selected.length > 0 && (
|
||||
<>
|
||||
<Button size="small" onClick={() => setBulkOpen(true)}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setBulkVersions(
|
||||
Object.fromEntries(
|
||||
data.items
|
||||
.filter((row) => selected.includes(row.id))
|
||||
.map((row) => [row.id, row.research.version]),
|
||||
),
|
||||
);
|
||||
setBulkOpen(true);
|
||||
onOverlay();
|
||||
}}
|
||||
>
|
||||
批量编辑
|
||||
</Button>
|
||||
<Button
|
||||
@@ -705,7 +780,7 @@ export function AlphaPage({
|
||||
</p>
|
||||
<Modal
|
||||
title="导入 Alpha ID"
|
||||
visible={importing}
|
||||
visible={importing && !overlaySuspended}
|
||||
onCancel={() => setImporting(false)}
|
||||
confirmLoading={busy === "alpha_refresh"}
|
||||
okText="开始导入"
|
||||
@@ -730,8 +805,8 @@ export function AlphaPage({
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={`批量编辑 ${selected.length} 条研究记录`}
|
||||
visible={bulkOpen}
|
||||
title={`批量编辑 ${Object.keys(bulkVersions).length} 条研究记录`}
|
||||
visible={bulkOpen && !overlaySuspended}
|
||||
onCancel={() => setBulkOpen(false)}
|
||||
confirmLoading={busy === "bulk"}
|
||||
onOk={() => void bulkSave()}
|
||||
@@ -768,6 +843,8 @@ export function AlphaPage({
|
||||
</label>
|
||||
</Modal>
|
||||
<AlphaDetail
|
||||
suspended={overlaySuspended}
|
||||
chatOffset={chatOffset}
|
||||
taskPanelOpen={taskPanelOpen}
|
||||
id={detailId}
|
||||
version={`${version}:${localVersion}`}
|
||||
|
||||
Reference in New Issue
Block a user