diff --git a/frontend/src/components/InlineVisualizationCard.tsx b/frontend/src/components/InlineVisualizationCard.tsx index 5031178..01f7c83 100644 --- a/frontend/src/components/InlineVisualizationCard.tsx +++ b/frontend/src/components/InlineVisualizationCard.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Code, Table as TableIcon, BarChart as ChartIcon, LayoutDashboard } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useDashboardStore } from "@/store/dashboardStore"; +import { useDashboardStore, type ChartConfig } from "@/store/dashboardStore"; import type { ChartSpec } from "@/store/visualizationStore"; import { VegaChart } from "./VegaChart"; @@ -22,22 +22,37 @@ interface InlineVisualizationCardProps { export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) { const [view, setView] = useState<'table' | 'chart'>('chart'); + const [confirmOpen, setConfirmOpen] = useState(false); + const [pendingChart, setPendingChart] = useState | null>(null); const { addChart } = useDashboardStore(); const objectRows = viz.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row)) as Record[]; const columns = objectRows.length > 0 ? Object.keys(objectRows[0]) : []; - const handleAddToDashboard = () => { + const buildPendingChart = (): Omit => { const mark = viz.chartSpec?.mark; const markType = typeof mark === "string" ? mark : mark?.type; const dashboardType = markType === "line" ? "line" : "bar"; - addChart({ + return { id: Date.now().toString(), title: viz.chartSpec?.title || "Generated Analysis", type: dashboardType, data: objectRows, sql: viz.sql, chartSpec: viz.chartSpec, - }); + }; + }; + + const handleAddToDashboard = () => { + const chart = buildPendingChart(); + setPendingChart(chart); + setConfirmOpen(true); + }; + + const handleConfirmAdd = () => { + if (!pendingChart) return; + addChart(pendingChart); + setConfirmOpen(false); + setPendingChart(null); }; if (viz.error) { @@ -128,6 +143,30 @@ export function InlineVisualizationCard({ viz }: InlineVisualizationCardProps) {
当前结果没有可渲染的结构化数据。
)} + + + + 确认加入 Dashboard + + 将当前图表添加到 Dashboard,是否继续? + + + + + + + + ); } diff --git a/frontend/src/components/VisualizationPanel.tsx b/frontend/src/components/VisualizationPanel.tsx index 0344200..3e746a3 100644 --- a/frontend/src/components/VisualizationPanel.tsx +++ b/frontend/src/components/VisualizationPanel.tsx @@ -1,33 +1,48 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Code, Table as TableIcon, BarChart as ChartIcon, Download, LayoutDashboard, Loader2 } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useDashboardStore } from "@/store/dashboardStore"; +import { useDashboardStore, type ChartConfig } from "@/store/dashboardStore"; import { useVisualizationStore } from "@/store/visualizationStore"; import { VegaChart } from "./VegaChart"; export function VisualizationPanel() { const [view, setView] = useState<'table' | 'chart'>('chart'); + const [confirmOpen, setConfirmOpen] = useState(false); + const [pendingChart, setPendingChart] = useState | null>(null); const { addChart } = useDashboardStore(); const { currentData, currentSQL, currentChartSpec, currentChartInfo, isLoading, error } = useVisualizationStore(); - const handleAddToDashboard = () => { - if (!currentData || !currentSQL) return; + const buildPendingChart = (): Omit | null => { + if (!currentData || !currentSQL) return null; const mark = currentChartSpec?.mark; const markType = typeof mark === "string" ? mark : mark?.type; const dashboardType = markType === "line" ? "line" : "bar"; - addChart({ + return { id: Date.now().toString(), title: currentChartSpec?.title || 'Generated Analysis', type: dashboardType, data: currentData, sql: currentSQL, chartSpec: currentChartSpec, - }); - alert("Added to Dashboard!"); + }; + }; + + const handleAddToDashboard = () => { + const chart = buildPendingChart(); + if (!chart) return; + setPendingChart(chart); + setConfirmOpen(true); + }; + + const handleConfirmAdd = () => { + if (!pendingChart) return; + addChart(pendingChart); + setConfirmOpen(false); + setPendingChart(null); }; if (isLoading) { @@ -175,6 +190,28 @@ export function VisualizationPanel() { + + + + 确认加入 Dashboard + + 将当前图表添加到 Dashboard,是否继续? + + + + + + + + ); } diff --git a/frontend/src/store/dashboardStore.ts b/frontend/src/store/dashboardStore.ts index 06901fe..84ccf17 100644 --- a/frontend/src/store/dashboardStore.ts +++ b/frontend/src/store/dashboardStore.ts @@ -21,25 +21,67 @@ interface DashboardState { updateLayout: (layouts: GridLayout[]) => void; } +const DASHBOARD_STORAGE_KEY = 'dashboard_charts_v1'; + +function loadChartsFromStorage(): ChartConfig[] { + if (typeof window === 'undefined') return []; + try { + const raw = window.localStorage.getItem(DASHBOARD_STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return []; + return parsed + .filter((item): item is ChartConfig => Boolean(item?.id && item?.layout)) + .map((item) => ({ + ...item, + layout: { + i: item.layout.i, + x: Number.isFinite(item.layout.x) ? item.layout.x : 0, + y: Number.isFinite(item.layout.y) ? item.layout.y : 0, + w: Number.isFinite(item.layout.w) ? item.layout.w : 4, + h: Number.isFinite(item.layout.h) ? item.layout.h : 4, + }, + })); + } catch { + return []; + } +} + +function saveChartsToStorage(charts: ChartConfig[]) { + if (typeof window === 'undefined') return; + window.localStorage.setItem(DASHBOARD_STORAGE_KEY, JSON.stringify(charts)); +} + export const useDashboardStore = create((set) => ({ - charts: [], + charts: loadChartsFromStorage(), addChart: (chart) => set((state) => { + const colSize = 4; + const cols = 12 / colSize; + const index = state.charts.length; const newLayout: GridLayout = { i: chart.id, - x: (state.charts.length * 4) % 12, - y: Infinity, - w: 4, + x: (index % cols) * colSize, + y: Math.floor(index / cols) * 4, + w: colSize, h: 4, }; - return { charts: [...state.charts, { ...chart, layout: newLayout }] }; + const nextCharts = [...state.charts, { ...chart, layout: newLayout }]; + saveChartsToStorage(nextCharts); + return { charts: nextCharts }; }), removeChart: (id) => set((state) => ({ - charts: state.charts.filter((c) => c.id !== id), + charts: (() => { + const nextCharts = state.charts.filter((c) => c.id !== id); + saveChartsToStorage(nextCharts); + return nextCharts; + })(), })), - updateLayout: (layouts) => set((state) => ({ - charts: state.charts.map((chart) => { + updateLayout: (layouts) => set((state) => { + const nextCharts = state.charts.map((chart) => { const layout = layouts.find((l) => l.i === chart.id); return layout ? { ...chart, layout } : chart; - }), - })), + }); + saveChartsToStorage(nextCharts); + return { charts: nextCharts }; + }), }));