import { ResearchDataInput, researchSelection, } from "../preparations/ResearchDataInput"; import { ResearchList, ResearchDetail } from "./ResearchList"; import { useEffect, useRef, useState } from "react"; import { Banner, Button, Input, Pagination, Table, TextArea, Toast, } from "@douyinfe/semi-ui-19"; import { ResearchInput } from "../ai/ResearchInput"; import { IconAIEditLevel3 } from "@douyinfe/semi-icons"; import { api, post } from "../api"; import type { PageContext, UIAction } from "../ai/types"; import { TemplateEditor } from "./TemplateEditor"; import { DeleteResearchButton } from "./DeleteResearchButton"; import { ResearchSelect } from "./ResearchSelect"; import { blankTemplate } from "./workspaceTypes"; import type { Asset, InputSnapshot, Template } from "./workspaceTypes"; import "./workspace.css"; type Feature = { name: string; hypothesis: string; input_ids: string[]; steps: { name: string; rationale: string; expression: string }[]; template: Template | null; }; type FeatureAsset = Omit & { content: Feature }; const blank = (): Feature => ({ name: "新特征方案", hypothesis: "", input_ids: [], steps: [], template: blankTemplate(), }); export function FeaturesPage({ active, onAction, onContext, action, }: { active: boolean; action?: UIAction | null; onAction: (action: UIAction) => void; onContext: (context: PageContext) => void; }) { const [detailOpen, setDetailOpen] = useState(false); const [draft, setDraft] = useState(blank); const [asset, setAsset] = useState(null); const [items, setItems] = useState([]); const [inputs, setInputs] = useState([]); const [versions, setVersions] = useState<{ version: number }[]>([]); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [q, setQ] = useState(""); const [busy, setBusy] = useState(""); const [error, setError] = useState(""); const [revision, setRevision] = useState(0); useEffect(() => { if (!active) return; const controller = new AbortController(); Promise.all([ api<{ items: FeatureAsset[]; total: number }>( `/research/assets?kind=feature&q=${encodeURIComponent(q)}&offset=${(page - 1) * 25}`, { signal: controller.signal }, ), Promise.resolve({ items: inputs }), ]) .then(([list, fixed]) => { setItems(list.items); setTotal(list.total); setPage((current) => Math.min(current, Math.max(1, Math.ceil(list.total / 25))), ); setInputs(fixed.items); }) .catch((e) => { if (!controller.signal.aborted) setError(e.message); }); return () => controller.abort(); }, [active, page, q, revision]); useEffect(() => { if (active) onContext({ page: "features", research_asset_id: asset?.id }); }, [active, asset?.id, onContext]); const handled = useRef(0); useEffect(() => { if ( !active || action?.type !== "open_feature" || handled.current === action.nonce ) return; handled.current = action.nonce; void run("load", async () => load( await api( `/research/assets/${action.asset_id}${action.version ? `?version=${action.version}` : ""}`, ), ), ); }, [active, action]); async function load(item: FeatureAsset) { setDetailOpen(true); setAsset(item); setDraft(item.content); setVersions(await api(`/research/assets/${item.id}/versions`)); } async function run(name: string, work: () => Promise) { setBusy(name); setError(""); try { await work(); } catch (e) { setError((e as Error).message); } finally { setBusy(""); } } async function save() { const saved = await api( `/research/assets${asset ? `/${asset.id}` : ""}`, { method: asset ? "PUT" : "POST", body: JSON.stringify({ kind: "feature", version: asset?.version, content: { ...draft, ...researchSelection(draft.input_ids, inputs) }, }), }, ); await load(saved); setRevision((n) => n + 1); return saved; } async function remove(item: FeatureAsset) { await api(`/research/assets/${item.id}?version=${item.version}`, { method: "DELETE", }); setItems((old) => old.filter((entry) => entry.id !== item.id)); setTotal((old) => Math.max(0, old - 1)); if (asset?.id === item.id) { setDetailOpen(false); setAsset(null); setDraft(blank()); setVersions([]); } setRevision((n) => n + 1); Toast.success("特征方案已删除"); } return (
{error && !detailOpen && } { setQ(v); setPage(1); }} /> } actions={ } pagination={ <> 共 {total} 个方案 {total > 0 && ( )} } > className="research-table" dataSource={items} rowKey="id" pagination={false} size="small" sticky style={{ minWidth: 800 }} empty="暂无特征方案,请新建方案或调整筛选条件。" columns={[ { title: "方案名称", key: "name", width: 200, render: (_, item) => ( ), }, { title: "版本", key: "version", width: 80, render: (_, item) => <>v{item.version}, }, { title: "经济假设", key: "hypothesis", render: (_, item) => item.content.hypothesis || "—", }, { title: "固定输入", key: "inputs", width: 100, render: (_, item) => item.content.input_ids.length, }, { title: "处理步骤", key: "steps", width: 100, render: (_, item) => item.content.steps.length, }, { title: "操作", key: "actions", width: 80, render: (_, item) => ( run("delete", () => remove(item))} /> ), }, ]} /> setDetailOpen(false)} > {error && } {asset?.archived && ( )}
{asset && ( <> 版本 ({ value: v.version, label: `v${v.version}`, }))} onChange={(v) => void run("load", async () => { await load( await api(`/research/assets/${asset.id}?version=${v}`), ); }) } /> )}

setDraft({ ...draft, hypothesis })} placeholder="描述特征的经济含义、处理方式和预期信号…" references={inputs .filter((i) => draft.input_ids.includes(i.id)) .map((i) => ({ id: i.id, name: `${i.name} · ${i.scope.region}/${i.scope.universe}/D${i.scope.delay} · ${i.field_ids.length} 字段 · ${i.id}`, }))} disabled={!!busy} busy={busy === "generate"} canSubmit={!!draft.input_ids.length} submitLabel="AI 生成新方案" sendHotKey="shift+enter" onSubmit={() => void run("generate", async () => { await load( await post("/research/generate", { name: draft.name, hypothesis: draft.hypothesis, ...researchSelection(draft.input_ids, inputs), method: "feature", }), ); setRevision((n) => n + 1); }) } prompts={["研究字段的时序变化,构建具有经济解释的特征"]} hint={ draft.input_ids.length ? "生成处理步骤与输出模板,保留假设供继续迭代。" : "请先在下方关联固定输入。" } />
{inputs .filter((i) => draft.input_ids.includes(i.id)) .map((i) => (
{i.name} 的固定字段

{i.field_ids.join("、")}

))}

处理与迭代步骤

{draft.steps.map((step, index) => (