beta
This commit is contained in:
@@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
import { useVisualizationStore } from "@/store/visualizationStore";
|
||||
import { type ChartSpec, useVisualizationStore } from "@/store/visualizationStore";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -267,7 +267,12 @@ export function ChatInterface() {
|
||||
} else {
|
||||
// Fallback to existing NL2SQL or other skills (e.g. for "表格问答" or "深度问数")
|
||||
const source = selectedDataSource.split('-')[0]; // postgres-main -> postgres
|
||||
const response = await api.post<{sql?: string, result?: unknown, error?: string}>('/api/v1/agent/nl2sql', {
|
||||
const response = await api.post<{
|
||||
sql?: string,
|
||||
result?: unknown,
|
||||
error?: string,
|
||||
chart?: { chart_spec: ChartSpec, reasoning: string, can_visualize: boolean }
|
||||
}>('/api/v1/agent/nl2sql', {
|
||||
query: messagePayload,
|
||||
source: source,
|
||||
session_id: activeSessionKey,
|
||||
@@ -287,9 +292,9 @@ export function ChatInterface() {
|
||||
setMessages(prev => [...prev, {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: `I've generated a SQL query and fetched ${rows.length} rows for you. Check the visualization panel.`
|
||||
content: `I've generated a SQL query and fetched ${rows.length} rows for you. Check the visualization panel.${response.chart?.reasoning ? `\n\nVisualization reasoning: ${response.chart.reasoning}` : ''}`
|
||||
}]);
|
||||
setVisualization(rows, sql);
|
||||
setVisualization(rows, sql, response.chart?.chart_spec);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { VegaEmbed } from 'react-vega';
|
||||
import type { ChartSpec } from '@/store/visualizationStore';
|
||||
|
||||
interface VegaChartProps {
|
||||
data: any[];
|
||||
spec: ChartSpec;
|
||||
}
|
||||
|
||||
export const VegaChart: React.FC<VegaChartProps> = ({ data, spec }) => {
|
||||
const vegaSpec: any = {
|
||||
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
|
||||
description: spec.description,
|
||||
title: spec.title,
|
||||
width: "container",
|
||||
height: "container",
|
||||
mark: { type: spec.chart_type, tooltip: true },
|
||||
encoding: {
|
||||
x: { field: spec.x_axis, type: 'nominal', axis: { labelAngle: -45 } },
|
||||
y: { field: spec.y_axis, type: 'quantitative' },
|
||||
},
|
||||
data: { values: data }
|
||||
};
|
||||
|
||||
if (spec.color) {
|
||||
vegaSpec.encoding.color = { field: spec.color, type: 'nominal' };
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<VegaEmbed
|
||||
spec={vegaSpec}
|
||||
options={{ actions: false }}
|
||||
style={{width: '100%', height: '100%'}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,25 +3,24 @@ 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line } from 'recharts';
|
||||
import { Code, Table as TableIcon, BarChart as ChartIcon, LineChart as LineChartIcon, Download, LayoutDashboard, Loader2 } from "lucide-react";
|
||||
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 { useVisualizationStore } from "@/store/visualizationStore";
|
||||
import { VegaChart } from "./VegaChart";
|
||||
|
||||
export function VisualizationPanel() {
|
||||
const [view, setView] = useState<'table' | 'chart'>('chart');
|
||||
const [chartType, setChartType] = useState<'bar' | 'line'>('bar');
|
||||
const { addChart } = useDashboardStore();
|
||||
const { currentData, currentSQL, isLoading, error } = useVisualizationStore();
|
||||
const { currentData, currentSQL, currentChartSpec, isLoading, error } = useVisualizationStore();
|
||||
|
||||
const handleAddToDashboard = () => {
|
||||
if (!currentData || !currentSQL) return;
|
||||
|
||||
addChart({
|
||||
id: Date.now().toString(),
|
||||
title: 'Generated Analysis', // Could be dynamic based on query
|
||||
type: chartType,
|
||||
title: currentChartSpec?.title || 'Generated Analysis',
|
||||
type: currentChartSpec?.chart_type as any || 'bar',
|
||||
data: currentData,
|
||||
sql: currentSQL,
|
||||
});
|
||||
@@ -67,9 +66,6 @@ export function VisualizationPanel() {
|
||||
}
|
||||
|
||||
const columns = Object.keys(objectRows[0] as Record<string, unknown>);
|
||||
const firstRow = objectRows[0] as Record<string, unknown>;
|
||||
const stringColumn = columns.find(col => typeof firstRow[col] === 'string') || columns[0];
|
||||
const numberColumns = columns.filter(col => typeof firstRow[col] === 'number');
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-muted/10 overflow-hidden">
|
||||
@@ -98,17 +94,6 @@ export function VisualizationPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{view === 'chart' && (
|
||||
<div className="flex gap-1 mr-2 border-r pr-2">
|
||||
<Button variant={chartType === 'bar' ? 'secondary' : 'ghost'} size="icon" className="h-7 w-7" onClick={() => setChartType('bar')}>
|
||||
<ChartIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant={chartType === 'line' ? 'secondary' : 'ghost'} size="icon" className="h-7 w-7" onClick={() => setChartType('line')}>
|
||||
<LineChartIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleAddToDashboard}>
|
||||
<LayoutDashboard className="h-3.5 w-3.5 mr-1.5" />
|
||||
Add to Dashboard
|
||||
@@ -148,42 +133,21 @@ export function VisualizationPanel() {
|
||||
<div className="flex-1 p-4 overflow-hidden min-h-0">
|
||||
<Card className="h-full flex flex-col shadow-sm border-muted">
|
||||
<CardHeader className="pb-2 shrink-0">
|
||||
<CardTitle>Analysis Result</CardTitle>
|
||||
<CardDescription>Generated from your query</CardDescription>
|
||||
<CardTitle>{currentChartSpec?.title || 'Analysis Result'}</CardTitle>
|
||||
<CardDescription>{currentChartSpec?.description || 'Generated from your query'}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 min-h-0 p-4">
|
||||
{view === 'chart' ? (
|
||||
<div className="h-full w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{chartType === 'bar' ? (
|
||||
<BarChart data={objectRows} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
||||
<XAxis dataKey={stringColumn} tickLine={false} axisLine={false} tick={{ fontSize: 12, fill: '#6b7280' }} />
|
||||
<YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12, fill: '#6b7280' }} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(0,0,0,0.05)' }}
|
||||
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ paddingTop: '20px' }} />
|
||||
{numberColumns.map((col, idx) => (
|
||||
<Bar key={col} dataKey={col} fill={`hsl(${idx * 60 + 200}, 70%, 50%)`} radius={[4, 4, 0, 0]} name={col} />
|
||||
))}
|
||||
</BarChart>
|
||||
) : (
|
||||
<LineChart data={objectRows} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
||||
<XAxis dataKey={stringColumn} tickLine={false} axisLine={false} tick={{ fontSize: 12, fill: '#6b7280' }} />
|
||||
<YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12, fill: '#6b7280' }} />
|
||||
<Tooltip
|
||||
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ paddingTop: '20px' }} />
|
||||
{numberColumns.map((col, idx) => (
|
||||
<Line key={col} type="monotone" dataKey={col} stroke={`hsl(${idx * 60 + 200}, 70%, 50%)`} strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} name={col} />
|
||||
))}
|
||||
</LineChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
{currentChartSpec ? (
|
||||
<VegaChart data={objectRows} spec={currentChartSpec} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<ChartIcon className="h-12 w-12 mb-4 opacity-20" />
|
||||
<p>No chart configuration available for this data.</p>
|
||||
<Button variant="link" onClick={() => setView('table')}>View Table</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full border rounded-md">
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface ChartSpec {
|
||||
chart_type: string;
|
||||
title: string;
|
||||
x_axis: string;
|
||||
y_axis: string;
|
||||
color?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface VisualizationState {
|
||||
currentData: any[] | null;
|
||||
currentSQL: string | null;
|
||||
currentChartType: 'bar' | 'line';
|
||||
currentChartSpec: ChartSpec | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
setVisualization: (data: any[], sql: string) => void;
|
||||
setChartType: (type: 'bar' | 'line') => void;
|
||||
setVisualization: (data: any[], sql: string, chartSpec?: ChartSpec | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
clearVisualization: () => void;
|
||||
@@ -16,12 +24,11 @@ export interface VisualizationState {
|
||||
export const useVisualizationStore = create<VisualizationState>((set) => ({
|
||||
currentData: null,
|
||||
currentSQL: null,
|
||||
currentChartType: 'bar',
|
||||
currentChartSpec: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
setVisualization: (data, sql) => set({ currentData: data, currentSQL: sql, error: null }),
|
||||
setChartType: (type) => set({ currentChartType: type }),
|
||||
setVisualization: (data, sql, chartSpec = null) => set({ currentData: data, currentSQL: sql, currentChartSpec: chartSpec, error: null }),
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
setError: (error) => set({ error, isLoading: false }),
|
||||
clearVisualization: () => set({ currentData: null, currentSQL: null, error: null }),
|
||||
clearVisualization: () => set({ currentData: null, currentSQL: null, currentChartSpec: null, error: null }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user