import { memo, useState } from "react"; import { ArrowDownToLine, ArrowUpRight, ChevronLeft, ChevronRight, Copy, } from "lucide-react"; import { ResponsiveContainer, BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip, Legend, PieChart, Pie, Cell, LineChart, Line, } from "recharts"; import type { AssistantResult, PresentationBlock, PresentationInteraction, } from "../../../shared/assets/assistant"; import { fetchAuthenticated } from "../../../auth/api-client"; import type { ResultViewProps } from "./ResultView"; import { compactAnalysisPath, copyResultText, displayTime, queryPath, resultScope, } from "./result-presentation"; import { presentationAction, presentationData, presentationFields, presentationMetrics, presentationSeries, presentationValue, type PresentationDatum, } from "./generative-presentation"; const COLORS = [ "#4e806a", "#527da5", "#b18439", "#8870a4", "#39948e", "#ae6970", ]; const datumName = (row: PresentationDatum) => String(row.name ?? row.date ?? row.label ?? row.plate ?? ""); type DataBlockProps = { block: PresentationBlock; result: AssistantResult; interaction?: PresentationInteraction; operate: ResultViewProps["operate"]; disabled: boolean; }; function DataTable({ block, result, interaction, operate, disabled, }: DataBlockProps) { const [page, setPage] = useState(0); const data = presentationData(result, block.dataset); const columns = presentationFields(block); const currentPage = Math.min( page, Math.max(0, Math.ceil(data.length / 8) - 1), ); return ( <>
{columns.map((column) => ( ))} {interaction && } {data .slice(currentPage * 8, currentPage * 8 + 8) .map((row, index) => { const action = presentationAction(interaction, row); return ( {columns.map((column) => ( ))} {interaction && ( )} ); })}
{block.title || "分析数据"}
{column.label} 继续分析
{presentationValue(row[column.field], column.field)} {action ? ( ) : ( "—" )}
{data.length > 8 && (
第 {currentPage * 8 + 1}– {Math.min(data.length, currentPage * 8 + 8)} 条,共 {data.length} 条
)} ); } function DataChart(props: DataBlockProps) { const { block, result, interaction, operate, disabled } = props; const [expanded, setExpanded] = useState(false); const allData = presentationData(result, block.dataset); const data = expanded || block.chart === "line" || block.chart === "donut" ? allData : allData.slice(0, 5); const series = presentationSeries(block); const labelField = block.dataset === "daily" ? "date" : block.dataset === "comparison" ? "label" : "name"; const explore = (datum: PresentationDatum) => { const action = presentationAction(interaction, datum); if (action && !disabled) operate(action, `${datumName(datum)} · ${interaction!.label}`); }; const clickChart = (state: { activeTooltipIndex?: string | number | null; }) => { const index = Number(state.activeTooltipIndex); if (state.activeTooltipIndex != null && data[index]) explore(data[index]); }; if (!series.length) return ; const common = { data, margin: { top: 8, right: 16, bottom: 0, left: 0 } }; return ( <>
{block.chart === "donut" ? ( explore(entry.payload as PresentationDatum)} > {data.map((_, index) => ( ))} presentationValue(value, series[0].field)} /> ) : block.chart === "line" ? ( ["rate", "share"].includes(item.field))} /> {series.map((item, index) => ( ))} ) : ( ["rate", "share"].includes(item.field))} /> {series.map((item, index) => ( ))} )}
{interaction && (
{data.map((row, index) => ( ))}
)} {allData.length > 5 && block.chart !== "line" && block.chart !== "donut" && ( )}
查看图表数据
); } function Block({ block, result, operate, disabled, }: Omit) { const interaction = result.presentation?.interactions.find( (item) => item.id === block.interactionId, ); const data = presentationData(result, block.dataset); return (
{block.title &&

{block.title}

} {block.kind === "summary" ? (

item.kind === "summary") ?.id === block.id ? `aq-answer-${result.resultId}` : undefined } > {block.text || result.summary}

) : block.kind === "narrative" ? (

{block.text}

) : block.kind === "metrics" ? (
{presentationMetrics(result, block.metrics).map((metric) => (
{metric.label} {metric.value.toLocaleString()} {metric.unit}
))}
) : ( <> {interaction && (

点击{block.dataset === "daily" ? "日期" : "数据项"} →{" "} {interaction.label}

)} {data.length ? ( block.kind === "chart" ? ( ) : ( ) ) : (

当前范围暂无匹配数据。

)} )}
); } function GenerativeResultViewContent({ result, operate, restore, active, activate, subject, disabled, correctionMessage, correctionResultId, showCorrection, }: ResultViewProps) { const [noticeByResult, setNoticeByResult] = useState<{ key: string; text: string; }>(); const [saving, setSaving] = useState(false); const [flaggedResult, setFlaggedResult] = useState(); const resultKey = result.resultId || result.fetchedAt; const notice = noticeByResult?.key === resultKey ? noticeByResult.text : ""; const flagged = flaggedResult === resultKey; const setNotice = (text: string) => setNoticeByResult({ key: resultKey, text }); const scope = resultScope(result, subject); const path = queryPath(result.plan); const breadcrumbs = compactAnalysisPath(result.breadcrumbs); const currentLabel = result.breadcrumbs?.find( (item) => item.resultId === result.resultId, )?.label; const parent = breadcrumbs .filter( (item) => item.resultId !== result.resultId && item.label !== currentLabel, ) .at(-1); const presentation = result.presentation!; const followups = presentation.interactions.filter( (item) => item.kind === "followup", ); const save = async (kind: "feedback" | "export") => { setSaving(true); setNotice(""); try { const response = await fetchAuthenticated( `/api/asset-assistant/${kind}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: result.sessionId, resultId: result.resultId, subject, }), }, ); if (!response.ok) throw new Error(); if (kind === "feedback") { setFlaggedResult(resultKey); setNotice("已记录,我们会据此完善问答能力。"); } else { const url = URL.createObjectURL(await response.blob()); const link = document.createElement("a"); link.href = url; link.download = "资产分析明细.csv"; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); setNotice("已按当前范围导出明细。"); } } catch { setNotice( kind === "feedback" ? "需求未保存,请稍后重试。" : "导出失败,请重新查询后重试。", ); } finally { setSaving(false); } }; return (
{path.map((item, index) => ( {item} ))}
{scope.company} {result.plan.kind === "flow" ? ` · ${scope.period}` : ""} {parent && ( )} {breadcrumbs.length > 1 && (
分析路径
)}
{correctionMessage && (
此回答已撤回

{correctionMessage}

{correctionResultId && ( )}
)} {result.correction && (

已纠正上一条回答:{result.correction.message}

)} {presentation.title && (

{presentation.title}

)} {result.outcome === "empty" && (

当前条件下无匹配数据,可调整范围后继续查询。

)} {result.outcome === "partial" && (

已返回可查结果,其余问题单独说明。

)} {result.outcome === "unsupported" && (

本次需求暂不能完整生成。

)} {presentation.blocks.map((block) => ( ))} {!!result.unanswered?.length && (
尚未回答 {result.unanswered.map((item) => (

{item.question}:{item.reason}

))}
)} {result.warning && (

{result.warning}

)} {result.presentationWarning && (

{result.presentationWarning}

)} {result.gap && (

{result.gap.recorded ? "未满足的需求已记录。" : "未满足的需求暂未保存。"}

)}
统计口径、权限与数据时间 {!presentation.blocks.some((block) => block.kind === "summary") && (

{result.summary}

)}

公司:{scope.company} · 权限:{scope.permission}

生效条件:{scope.filters.join(" · ") || "无额外筛选"}

分组:{scope.grouping} · 期间:{scope.period}

源数据更新时间:{displayTime(scope.sourceUpdatedAt)} · 查询时间: {displayTime(result.fetchedAt)}

{scope.definitions.map((item) => (

{item}

))} {result.contextNote &&

{result.contextNote}

}

来源:{result.source}

{notice && (

{notice}

)}
{!!followups.length && (
继续分析 {followups.map((item) => ( ))}
)}
); } export const GenerativeResultView = memo( GenerativeResultViewContent, (previous, next) => previous.result === next.result && previous.active === next.active && previous.subject === next.subject && previous.disabled === next.disabled && previous.correctionMessage === next.correctionMessage && previous.correctionResultId === next.correctionResultId, );