feat(charts): manage investigation charts in a scrolling workspace
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { useId, useState } from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { ChevronDown, Eye, EyeOff, LocateFixed, MapPinned, Pause, X } from "lucide-react";
|
||||
import { AgentChartView, type AnswerUiBlock } from "@/features/agent";
|
||||
import { formatChartDisplayText } from "@/shared/utils/date-time-text";
|
||||
import { ChevronDown, Eye, EyeOff, LocateFixed, MapPinned, Pause } from "lucide-react";
|
||||
import type { AnswerUiBlock } from "@/features/agent";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/shared/ui/card";
|
||||
@@ -19,7 +18,9 @@ import type { OperationalTask } from "../api/operational-task-client";
|
||||
import type { InvestigationOverlay } from "../api/investigation-presentation";
|
||||
import type { InvestigationChart } from "../hooks/use-investigation-presentation";
|
||||
|
||||
import { FloatingWindow } from "@/shared/ui/floating-window";
|
||||
import { InvestigationChartManager } from "./investigation-chart-manager";
|
||||
import { InvestigationChartGroup } from "./investigation-chart-group";
|
||||
import { groupInvestigationCharts } from "../utils/investigation-chart-groups";
|
||||
import { PresentationHistorySwitcher } from "./presentation-history-switcher";
|
||||
import { PresentationBusinessResults } from "./presentation-business-results";
|
||||
import type { PresentationHistoryControls } from "../utils/presentation-view-history";
|
||||
@@ -36,6 +37,8 @@ type Props = {
|
||||
task: OperationalTask | null;
|
||||
localPreview?: boolean;
|
||||
charts: InvestigationChart[];
|
||||
chartWindowAutoOpen?: boolean;
|
||||
chartWindowScope?: string;
|
||||
overlay: InvestigationOverlay | null;
|
||||
cameraPaused: boolean;
|
||||
status: "connecting" | "live" | "history" | "disconnected" | "error";
|
||||
@@ -68,6 +71,8 @@ export function InvestigationPresentationPanel({
|
||||
onLocateTargets,
|
||||
localPreview = false,
|
||||
charts,
|
||||
chartWindowAutoOpen = false,
|
||||
chartWindowScope,
|
||||
overlay,
|
||||
cameraPaused,
|
||||
status,
|
||||
@@ -80,13 +85,13 @@ export function InvestigationPresentationPanel({
|
||||
onFitOverlay,
|
||||
onToggleLayer
|
||||
}: Props) {
|
||||
const chartGroups = groupInvestigationCharts(charts);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [keyboardAction, setKeyboardAction] = useState(false);
|
||||
const reducedMotion = useReducedMotion();
|
||||
const immediate = Boolean(reducedMotion) || keyboardAction;
|
||||
const contentId = useId();
|
||||
const reviewing = history?.selectedId != null;
|
||||
const chartWindowKey = scope === "chat" ? undefined : JSON.stringify([scope, task?.task_id, reviewing ? "history" : "live"]);
|
||||
const selected = history?.entries.find((entry) => entry.id === history.selectedId);
|
||||
const pending = selected ? history!.entries.filter((entry) => entry.id > selected.id).length : 0;
|
||||
const title = scope === "chat" ? "对话展示" : localPreview ? "本地预览展示" : "调查实时展示";
|
||||
@@ -121,7 +126,7 @@ export function InvestigationPresentationPanel({
|
||||
<motion.header
|
||||
layout={immediate || localPreview ? false : "position"}
|
||||
transition={presentationTransition(immediate)}
|
||||
className={`shrink-0 border-b border-border px-3 ${localPreview ? `${collapsed ? "h-[110px]" : "min-h-[110px]"} py-2` : "py-2.5"}`}
|
||||
className={`@container shrink-0 border-b border-border px-3 ${localPreview ? `${collapsed ? "h-[110px]" : "min-h-[110px]"} py-2` : "py-2.5"}`}
|
||||
>
|
||||
<div className={localPreview ? "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-2 gap-y-1" : "flex items-start justify-between gap-2"}>
|
||||
<div className={localPreview ? "contents" : "min-w-0 space-y-1"}>
|
||||
@@ -137,7 +142,7 @@ export function InvestigationPresentationPanel({
|
||||
: statusLabels[status]}
|
||||
</Badge>
|
||||
<span className="text-[11px] text-muted-foreground tabular-nums">
|
||||
图表 {charts.length} · 范围 {overlay ? 1 : 0} · 数据 {records.length}
|
||||
图表 {chartGroups.length} · 范围 {overlay ? 1 : 0} · 数据 {records.length}
|
||||
</span>
|
||||
{reviewing && pending > 0 ? (
|
||||
<span role="status" className="text-[11px] text-muted-foreground">
|
||||
@@ -146,6 +151,10 @@ export function InvestigationPresentationPanel({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex shrink-0 items-center gap-1 ${localPreview ? "col-start-2 row-start-1" : ""}`}>
|
||||
<InvestigationChartManager key={chartWindowScope ?? JSON.stringify([scope, task?.task_id, task?.execution_generation])}
|
||||
groups={chartGroups} blocks={blocks} autoOpen={chartWindowAutoOpen} taskTitle={task?.title ?? title}
|
||||
reviewing={reviewing} onCloseChart={onCloseChart} references={references} />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -161,6 +170,7 @@ export function InvestigationPresentationPanel({
|
||||
<ChevronDown size={16} aria-hidden />
|
||||
</motion.span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className={
|
||||
@@ -242,7 +252,7 @@ export function InvestigationPresentationPanel({
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<AnimatePresence key={chartWindowScope} initial={false} mode="popLayout">
|
||||
{overlay ? (
|
||||
<PresentationItem key={`overlay-${overlay.overlay_id}`} immediate={immediate}>
|
||||
<Card role="region" aria-label="调查覆盖层" className="border-border shadow-none">
|
||||
@@ -291,51 +301,10 @@ export function InvestigationPresentationPanel({
|
||||
</Card>
|
||||
</PresentationItem>
|
||||
) : null}
|
||||
{charts.map((item) => (
|
||||
<PresentationItem key={item.chart_id} immediate={immediate}>
|
||||
<Card
|
||||
data-investigation-chart={item.chart_id}
|
||||
role="group"
|
||||
aria-label={formatChartDisplayText(item.chart.title)}
|
||||
className="border-border shadow-none"
|
||||
>
|
||||
<CardHeader className="flex-row items-start justify-between gap-2 space-y-0 p-3 pb-2">
|
||||
<div className="min-w-0">
|
||||
<h3 className="break-words text-xs font-semibold leading-5 [overflow-wrap:anywhere]">
|
||||
{formatChartDisplayText(item.chart.title)}
|
||||
</h3>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||
{item.chart.series.some((series) => series.type === "scatter") ? "折线与历史散点" : item.chart.chart_type === "bar" ? "柱状图" : "折线图"} ·{" "}
|
||||
{item.chart.x_data.length} 个
|
||||
{item.chart.chart_type === "bar" ? "类别" : "时间点"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<FloatingWindow title={formatChartDisplayText(item.chart.title)} fillContent
|
||||
openRequestKey={chartWindowKey === undefined ? undefined : JSON.stringify([chartWindowKey, item])}>
|
||||
{() => <div className="flex h-full min-h-0 min-w-0 flex-col gap-2">
|
||||
<AgentChartView chart={item.chart} embedded expanded />
|
||||
{item.evidence_refs.length ? <div className="max-h-[35%] shrink-0 overflow-auto">{references(item.evidence_refs)}</div> : null}
|
||||
</div>}
|
||||
</FloatingWindow>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="shrink-0"
|
||||
disabled={reviewing}
|
||||
aria-label={`关闭图表 ${formatChartDisplayText(item.chart.title)}`}
|
||||
onClick={() => onCloseChart(item.chart_id)}
|
||||
>
|
||||
<X size={14} aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 pb-2 pt-0">
|
||||
<AgentChartView chart={item.chart} embedded />
|
||||
{references(item.evidence_refs)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{chartGroups.map((group) => (
|
||||
<PresentationItem key={`${chartWindowScope}:${group.id}`} immediate={immediate}>
|
||||
<InvestigationChartGroup group={group} reviewing={reviewing ?? false}
|
||||
onCloseChart={onCloseChart} references={references} />
|
||||
</PresentationItem>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
@@ -343,7 +312,6 @@ export function InvestigationPresentationPanel({
|
||||
blocks={blocks}
|
||||
records={records}
|
||||
reviewing={reviewing}
|
||||
chartWindowKey={chartWindowKey}
|
||||
onLocateTargets={onLocateTargets}
|
||||
/>
|
||||
{!charts.length &&
|
||||
|
||||
Reference in New Issue
Block a user