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"; import { useMobileLayout } from '../useMobileLayout'; import { mobileBlockGroups, mobileResultSummary } from './mobile-presentation'; import { ResultActions } from './ResultActions'; 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 mobile = useMobileLayout(); 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 ( <> {mobile ?
{data.slice(currentPage * 8, currentPage * 8 + 8).map((row, index) => { const action = presentationAction(interaction, row); const vehicle = block.dataset === 'rows' && columns.some(column => column.field === 'plate'); const primary = vehicle ? columns.filter(column => ['plate', 'status', 'event', 'vehicleClass', 'type', 'city'].includes(column.field)) : columns.slice(0, 3); const extra = columns.filter(column => !primary.includes(column)); return
{vehicle ? <>
{presentationValue(row.plate, 'plate')}{primary.filter(column => ['status', 'event'].includes(column.field)).map(column => presentationValue(row[column.field], column.field)).join(' · ')}

{primary.filter(column => !['plate', 'status', 'event'].includes(column.field)).map(column => presentationValue(row[column.field], column.field)).join(' · ')}

:
{primary.map(column =>
{column.label}{presentationValue(row[column.field], column.field)}
)}
} {(extra.length > 0 || action) &&
{extra.length > 0 &&
更多字段
{extra.map(column =>
{column.label}
{presentationValue(row[column.field], column.field)}
)}
}{action && }
}
; })}
:
{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 mobile = useMobileLayout(); 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 } }; if (mobile && (block.chart === 'bar' || (block.chart === 'donut' && block.dataset === 'groups')) && series.length === 1 && allData.every(row => typeof row[series[0].field] === 'number' && Number(row[series[0].field]) >= 0)) { const field = series[0].field; const max = Math.max(1, ...allData.map(row => typeof row[field] === 'number' ? Math.abs(row[field] as number) : 0)); return
{(expanded ? allData : allData.slice(0, 5)).map((row, index) => { const action = presentationAction(interaction, row); const content = <>{datumName(row)}{presentationValue(row[field], field)}{action && }; return action ? :
{content}
; })}{allData.length > 5 && }
; } 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" && ( )} {!mobile &&
查看图表数据
} ); } function Block({ block, result, operate, disabled, }: Omit) { const mobile = useMobileLayout(); const [expandedMetrics, setExpandedMetrics] = useState(false); const metrics = presentationMetrics(result, block.metrics); 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" ? ( <>
{(mobile && !expandedMetrics ? metrics.slice(0, 4) : metrics).map((metric) => (
{metric.label} {metric.value.toLocaleString()} {metric.unit}
))}
{mobile && metrics.length > 4 && } ) : ( <> {interaction && !mobile && (

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

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

当前范围暂无匹配数据。

)} )}
); } function MobileDataBlock({ block, table, ...props }: Omit & { table?: PresentationBlock }) { const [view, setView] = useState<'chart' | 'table'>('chart'); const tableBlock: PresentationBlock = table || { ...block, kind: 'table', columns: block.columns || [{ field: block.dataset === 'daily' ? 'date' : block.dataset === 'comparison' ? 'label' : 'name', label: block.dataset === 'daily' ? '日期' : '分组' }, ...presentationSeries(block)] }; return

{block.title || '数据分析'}

; } function GenerativeResultViewContent({ result, operate, restore, active, activate, subject, disabled, correctionMessage, correctionResultId, showCorrection, }: ResultViewProps) { const mobile = useMobileLayout(); 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}

)} {!mobile && presentation.title && (

{presentation.title}

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

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

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

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

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

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

)} {mobile ? <>

{mobileResultSummary(result)}

{mobileBlockGroups(presentation.blocks).filter(({ block }) => block.kind !== 'summary' && block.kind !== 'narrative').map(({ block, table }) => block.kind === 'chart' ? : )}
分析说明

{result.summary}

{presentation.blocks.filter(block => (block.kind === 'summary' || block.kind === 'narrative') && block.text && block.text !== result.summary).map(block =>

{block.text}

)}
: 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}

{mobile ? (active ? "当前追问范围" : "继续追问") : (active ? "正在基于此结果追问" : "基于此结果追问")} }> {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, );