diff --git a/bridge/src/handlers/aiHandlers.ts b/bridge/src/handlers/aiHandlers.ts index 05f4172..cf09daa 100644 --- a/bridge/src/handlers/aiHandlers.ts +++ b/bridge/src/handlers/aiHandlers.ts @@ -178,11 +178,12 @@ export class AIHandlers { // ── History CRUD handlers ───────────────────────────────────────────── - async handleGetHistory(params: { feature?: string; provider?: string; limit?: number; offset?: number }, id: number | string) { + async handleGetHistory(params: { feature?: string; provider?: string; datasource_id?: string; limit?: number; offset?: number }, id: number | string) { try { const result = aiHistoryStore.list({ feature: params?.feature, provider: params?.provider, + datasource_id: params?.datasource_id, limit: params?.limit, offset: params?.offset, }); diff --git a/bridge/src/services/aiHistoryStore.ts b/bridge/src/services/aiHistoryStore.ts index 726f8c1..7106711 100644 --- a/bridge/src/services/aiHistoryStore.ts +++ b/bridge/src/services/aiHistoryStore.ts @@ -54,6 +54,7 @@ export interface AIHistoryInsert { export interface AIHistoryListParams { feature?: string; provider?: string; + datasource_id?: string; limit?: number; offset?: number; } @@ -196,6 +197,10 @@ export class AIHistoryStore { conditions.push("provider = ?"); values.push(params.provider); } + if (params.datasource_id) { + conditions.push("datasource_id = ?"); + values.push(params.datasource_id); + } const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` diff --git a/src/components/layout/VerticalIconBar.tsx b/src/components/layout/VerticalIconBar.tsx index 47d7a61..d959777 100644 --- a/src/components/layout/VerticalIconBar.tsx +++ b/src/components/layout/VerticalIconBar.tsx @@ -1,4 +1,4 @@ -import { Activity, Home, Database, Search, GitBranch, GitCommitHorizontal, Settings, Layers, Terminal, History } from 'lucide-react'; +import { Activity, Home, Database, Search, GitBranch, GitCommitHorizontal, Settings, Layers, Terminal, History, Sparkles } from 'lucide-react'; import { Link, useLocation } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { @@ -7,7 +7,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; -export type PanelType = 'data' | 'sql-workspace' | 'query-builder' | 'schema-explorer' | 'er-diagram' | 'monitoring' | 'git-status' | 'migrations'; +export type PanelType = 'data' | 'sql-workspace' | 'query-builder' | 'schema-explorer' | 'er-diagram' | 'monitoring' | 'git-status' | 'migrations' | 'ai-workspace'; interface VerticalIconBarProps { dbId?: string; @@ -39,6 +39,7 @@ export default function VerticalIconBar({ dbId, databaseType, activePanel, onPan const databasePanelItems: Array<{ icon: any; label: string; panel: PanelType }> = dbId ? [ { icon: Layers, label: 'Data View', panel: 'data' }, { icon: Terminal, label: 'SQL Workspace', panel: 'sql-workspace' }, + { icon: Sparkles, label: 'AI Workspace', panel: 'ai-workspace' }, { icon: Search, label: 'Query Builder', panel: 'query-builder' }, { icon: GitBranch, label: 'Schema Explorer', panel: 'schema-explorer' }, { icon: Database, label: 'ER Diagram', panel: 'er-diagram' }, diff --git a/src/features/ai/components/AIHistoryDetailDialog.tsx b/src/features/ai/components/AIHistoryDetailDialog.tsx index f59841d..060a898 100644 --- a/src/features/ai/components/AIHistoryDetailDialog.tsx +++ b/src/features/ai/components/AIHistoryDetailDialog.tsx @@ -205,7 +205,7 @@ export function AIHistoryDetailDialog({
- +
@@ -253,6 +253,76 @@ export function AIHistoryDetailDialog({ ); } +function FormattedResponseRenderer({ feature, content }: { feature: string; content: string }) { + try { + const data = JSON.parse(content); + + if (feature === "nl_to_sql") { + return ( +
+
+ Generated SQL +
{data.sql}
+
+ {data.explanation && ( +
+ Explanation +

{data.explanation}

+
+ )} + {data.assumptions && data.assumptions.length > 0 && ( +
+ Assumptions + +
+ )} +
+ ); + } + + if (feature === "chart-recommendation") { + return ( +
+
+
+ Chart Type + {data.chartType} +
+
+ X-Axis + {data.xAxis} +
+
+ Y-Axis + {data.yAxis} +
+
+ {data.reasoning && ( +
+ Reasoning +

{data.reasoning}

+
+ )} +
+ ); + } + + // Fallback for other JSON types + return ( +
+        {JSON.stringify(data, null, 2)}
+      
+ ); + } catch (e) { + // Not valid JSON, fallback to markdown renderer (used by schema-analysis, query-explanation) + return ; + } +} + function MetaItem({ icon, label, diff --git a/src/features/ai/components/AIHistoryPanel.tsx b/src/features/ai/components/AIHistoryPanel.tsx index 92d3d35..8f087ff 100644 --- a/src/features/ai/components/AIHistoryPanel.tsx +++ b/src/features/ai/components/AIHistoryPanel.tsx @@ -92,7 +92,7 @@ function timeAgo(isoDate: string): string { return `${months}mo ago`; } -export default function AIHistoryPanel() { +export default function AIHistoryPanel({ dbId }: { dbId?: string }) { const { data: databases } = useDatabases(); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); @@ -114,6 +114,7 @@ export default function AIHistoryPanel() { const result = await aiService.getHistory({ feature: featureFilter !== "all" ? featureFilter : undefined, provider: providerFilter !== "all" ? providerFilter : undefined, + datasource_id: dbId, limit: PAGE_SIZE, offset: page * PAGE_SIZE, }); @@ -268,7 +269,7 @@ export default function AIHistoryPanel() { Feature - Database + {!dbId && Database} Provider Tokens Created @@ -292,15 +293,17 @@ export default function AIHistoryPanel() { {FEATURE_LABELS[item.feature] ?? item.feature} - - {item.datasource_id ? ( - - {databases?.find(d => d.id === item.datasource_id)?.name || `${item.datasource_id.slice(0, 8)}...`} - - ) : ( - - )} - + {!dbId && ( + + {item.datasource_id ? ( + + {databases?.find(d => d.id === item.datasource_id)?.name || `${item.datasource_id.slice(0, 8)}...`} + + ) : ( + Global + )} + + )} {PROVIDER_LABELS[item.provider] ?? item.provider} diff --git a/src/features/ai/components/AIResultDialog.tsx b/src/features/ai/components/AIResultDialog.tsx index f17f247..9fa56d9 100644 --- a/src/features/ai/components/AIResultDialog.tsx +++ b/src/features/ai/components/AIResultDialog.tsx @@ -85,26 +85,31 @@ export function AIResultDialog({ )} {/* Description row with optional timestamp + re-analyze */} -
- {description && ( - {description} - )} - {markdown && !loading && cached && createdAt && ( - - Generated {timeAgo(createdAt)} - - )} - {markdown && !loading && cached && onReanalyze && ( - - )} +
+
+ {description && ( + {description} + )} + {markdown && !loading && cached && createdAt && ( + + Generated {timeAgo(createdAt)} + + )} + {markdown && !loading && cached && onReanalyze && ( + + )} +
+

+ Note: AI can make mistakes. Please verify responses and generated queries. +

@@ -214,6 +219,53 @@ export function MarkdownRenderer({ content }: { content: string }) { continue; } + // Table + if (line.trim().startsWith("|") && line.includes("|", 1)) { + const tableLines: string[] = []; + while (i < lines.length && lines[i].trim().startsWith("|")) { + tableLines.push(lines[i].trim()); + i++; + } + + const parseRow = (rowStr: string) => { + const parts = rowStr.split("|"); + if (parts.length > 0 && parts[0].trim() === "") parts.shift(); + if (parts.length > 0 && parts[parts.length - 1].trim() === "") parts.pop(); + return parts; + }; + + const headerCells = parseRow(tableLines[0]); + const bodyLines = tableLines.length > 2 ? tableLines.slice(2) : []; + + elements.push( +
+ + + + {headerCells.map((cell, idx) => ( + + ))} + + + + {bodyLines.map((rowLine, rowIdx) => ( + + {parseRow(rowLine).map((cell, idx) => ( + + ))} + + ))} + +
+ {renderInline(cell.trim())} +
+ {renderInline(cell.trim())} +
+
+ ); + continue; + } + // Blank line if (!line.trim()) { i++; diff --git a/src/features/chart/components/ChartVisualization.tsx b/src/features/chart/components/ChartVisualization.tsx index f490434..0d91e06 100644 --- a/src/features/chart/components/ChartVisualization.tsx +++ b/src/features/chart/components/ChartVisualization.tsx @@ -64,7 +64,7 @@ export const ChartVisualization = ({ type: c.type, isPrimaryKey: c.isPrimaryKey, })), - }); + }, { datasourceName: dbId }); setChartType(rec.chartType); setXAxis(rec.xAxis); setYAxis(rec.yAxis); diff --git a/src/features/database/components/AIWorkspacePanel.tsx b/src/features/database/components/AIWorkspacePanel.tsx new file mode 100644 index 0000000..8ea9f94 --- /dev/null +++ b/src/features/database/components/AIWorkspacePanel.tsx @@ -0,0 +1,87 @@ +import { Sparkles, Terminal, FileJson, Search, Database } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import AIHistoryPanel from "@/features/ai/components/AIHistoryPanel"; +import { PanelType } from "@/components/layout/VerticalIconBar"; + +interface AIWorkspacePanelProps { + dbId: string; + onNavigate: (panel: PanelType) => void; +} + +export default function AIWorkspacePanel({ dbId, onNavigate }: AIWorkspacePanelProps) { + return ( +
+
+

+ + AI Workspace +

+

+ Central hub for AI-powered features for this database. Use the tools below to analyze data, build queries, and explore your schema using natural language. +

+

+ Note: AI can make mistakes. Please verify responses and generated queries. +

+
+ +
+ + + + + Natural Language to SQL + + + Write database queries using plain English sentences. Let AI generate the SQL for you. + + + + + + + + + + + + Schema Analysis + + + Generate AI documentation and summaries for your entire database schema. + + + + + + + + + + + + Query Explanation + + + Highlight any complex SQL query in the workspace and let AI explain what it does step-by-step. + + + + + + +
+ +
+ {/* Pass dbId so it strictly filters for this DB and hides the Global DB column */} + +
+
+ ); +} diff --git a/src/features/settings/components/SettingsDialog.tsx b/src/features/settings/components/SettingsDialog.tsx index 5caf027..ef14a31 100644 --- a/src/features/settings/components/SettingsDialog.tsx +++ b/src/features/settings/components/SettingsDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { AISettings, AIHistoryPanel, CheckForUpdates, ColorVariant, DeveloperMode, ThemeMode, Version, AnalyticsSettings } from "@/features/settings/components"; +import { AISettings, CheckForUpdates, ColorVariant, DeveloperMode, ThemeMode, Version, AnalyticsSettings } from "@/features/settings/components"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; @@ -69,7 +69,6 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { {activeTab === "ai" && (
-
)} diff --git a/src/features/settings/components/index.tsx b/src/features/settings/components/index.tsx index 7df7d47..e3bbb26 100644 --- a/src/features/settings/components/index.tsx +++ b/src/features/settings/components/index.tsx @@ -6,5 +6,5 @@ export { default as AnalyticsSettings } from './AnalyticsSettings' export { default as CheckForUpdates } from './CheckForUpdates' export { default as Version } from './Version' export { default as AISettings } from './AISettings' -export { default as AIHistoryPanel } from '../../ai/components/AIHistoryPanel' + export { SettingsDialog } from './SettingsDialog' \ No newline at end of file diff --git a/src/features/workspace/components/ExplainQueryButton.tsx b/src/features/workspace/components/ExplainQueryButton.tsx index dab13a2..bf500cd 100644 --- a/src/features/workspace/components/ExplainQueryButton.tsx +++ b/src/features/workspace/components/ExplainQueryButton.tsx @@ -8,10 +8,10 @@ import { aiService } from "@/services/bridge/ai"; interface ExplainQueryButtonProps { sql: string; disabled?: boolean; - databaseName?: string; + dbId?: string; } -export function ExplainQueryButton({ sql, disabled, databaseName }: ExplainQueryButtonProps) { +export function ExplainQueryButton({ sql, disabled, dbId }: ExplainQueryButtonProps) { const { settings } = useAISettings(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); @@ -29,7 +29,7 @@ export function ExplainQueryButton({ sql, disabled, databaseName }: ExplainQuery try { const result = await aiService.explainQuery(settings, { sql: sql.trim(), - }, { skipCache, datasourceName: databaseName }); + }, { skipCache, datasourceName: dbId }); setMarkdown(result.markdown); setCached(result.cached); setCreatedAt(result.createdAt); diff --git a/src/features/workspace/components/NLQueryDialog.tsx b/src/features/workspace/components/NLQueryDialog.tsx index 00f41a1..0dfe8bf 100644 --- a/src/features/workspace/components/NLQueryDialog.tsx +++ b/src/features/workspace/components/NLQueryDialog.tsx @@ -69,6 +69,9 @@ export const NLQueryDialog: React.FC = ({ Natural Language to SQL +

+ AI can make mistakes. Please verify the generated SQL before applying it to your workspace. +

diff --git a/src/features/workspace/components/SQLWorkspacePanel.tsx b/src/features/workspace/components/SQLWorkspacePanel.tsx index 5b909ee..c681b0f 100644 --- a/src/features/workspace/components/SQLWorkspacePanel.tsx +++ b/src/features/workspace/components/SQLWorkspacePanel.tsx @@ -180,6 +180,7 @@ const SQLWorkspacePanel = ({ dbId }: SQLWorkspacePanelProps) => { return (
{/* NL to SQL Query */} diff --git a/src/pages/DatabaseDetails.tsx b/src/pages/DatabaseDetails.tsx index 2c9f26c..e10b6ed 100644 --- a/src/pages/DatabaseDetails.tsx +++ b/src/pages/DatabaseDetails.tsx @@ -27,6 +27,7 @@ import SQLWorkspacePanel from "@/features/workspace/components/SQLWorkspacePanel import GitStatusPanel from "@/features/git/components/GitStatusPanel"; import GitStatusBar from "@/features/git/components/GitStatusBar"; import { MonitoringPanel } from "@/features/monitoring/components/MonitoringPanel"; +import AIWorkspacePanel from "@/features/database/components/AIWorkspacePanel"; import { ShortcutsHelp } from "@/components/shared/ShortcutsHelp"; import { ShortcutsTrigger } from "@/components/shared/ShortcutsTrigger"; import { MigrationSyncDialog } from "@/features/project/components/MigrationSyncDialog"; @@ -113,6 +114,7 @@ const DatabaseDetail = () => { case "er-diagram": return ; case "monitoring": return ; case "git-status": return ; + case "ai-workspace": return ; case "migrations": return
; default: return ( diff --git a/src/services/bridge/ai.ts b/src/services/bridge/ai.ts index b1fc4e1..7cec32c 100644 --- a/src/services/bridge/ai.ts +++ b/src/services/bridge/ai.ts @@ -275,6 +275,7 @@ class AIService { async getHistory(params?: { feature?: string; provider?: string; + datasource_id?: string; limit?: number; offset?: number; }): Promise {