Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bridge/src/handlers/aiHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
5 changes: 5 additions & 0 deletions bridge/src/services/aiHistoryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface AIHistoryInsert {
export interface AIHistoryListParams {
feature?: string;
provider?: string;
datasource_id?: string;
limit?: number;
offset?: number;
}
Expand Down Expand Up @@ -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 ")}`
Expand Down
5 changes: 3 additions & 2 deletions src/components/layout/VerticalIconBar.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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' },
Expand Down
72 changes: 71 additions & 1 deletion src/features/ai/components/AIHistoryDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export function AIHistoryDetailDialog({
</Button>
</div>
<div className="rounded-md border border-border/20 p-3 bg-background/50">
<MarkdownRenderer content={entry.response} />
<FormattedResponseRenderer feature={entry.feature} content={entry.response} />
</div>
</div>

Expand Down Expand Up @@ -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 (
<div className="space-y-4">
<div className="p-3 bg-muted/30 border border-border/30 rounded-md">
<span className="text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-wider mb-2 block">Generated SQL</span>
<pre className="text-[11px] font-mono text-primary/90 whitespace-pre-wrap">{data.sql}</pre>
</div>
{data.explanation && (
<div>
<span className="text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-wider mb-1.5 block">Explanation</span>
<p className="text-xs text-foreground/80 leading-relaxed">{data.explanation}</p>
</div>
)}
{data.assumptions && data.assumptions.length > 0 && (
<div>
<span className="text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-wider mb-1.5 block">Assumptions</span>
<ul className="list-disc pl-4 space-y-1">
{data.assumptions.map((a: string, i: number) => (
<li key={i} className="text-xs text-foreground/70">{a}</li>
))}
</ul>
</div>
)}
</div>
);
}

if (feature === "chart-recommendation") {
return (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-3">
<div className="p-2.5 bg-muted/20 border border-border/20 rounded-md">
<span className="text-[9px] font-semibold text-muted-foreground/60 uppercase tracking-wider block mb-1">Chart Type</span>
<span className="text-xs text-foreground/80 font-medium capitalize">{data.chartType}</span>
</div>
<div className="p-2.5 bg-muted/20 border border-border/20 rounded-md">
<span className="text-[9px] font-semibold text-muted-foreground/60 uppercase tracking-wider block mb-1">X-Axis</span>
<span className="text-xs text-foreground/80 font-mono">{data.xAxis}</span>
</div>
<div className="p-2.5 bg-muted/20 border border-border/20 rounded-md">
<span className="text-[9px] font-semibold text-muted-foreground/60 uppercase tracking-wider block mb-1">Y-Axis</span>
<span className="text-xs text-foreground/80 font-mono">{data.yAxis}</span>
</div>
</div>
{data.reasoning && (
<div>
<span className="text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-wider mb-1.5 block">Reasoning</span>
<p className="text-xs text-foreground/80 leading-relaxed">{data.reasoning}</p>
</div>
)}
</div>
);
}

// Fallback for other JSON types
return (
<pre className="text-[11px] font-mono text-foreground/80 whitespace-pre-wrap">
{JSON.stringify(data, null, 2)}
</pre>
);
} catch (e) {
// Not valid JSON, fallback to markdown renderer (used by schema-analysis, query-explanation)
return <MarkdownRenderer content={content} />;
}
}

function MetaItem({
icon,
label,
Expand Down
25 changes: 14 additions & 11 deletions src/features/ai/components/AIHistoryPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AIHistoryListItem[]>([]);
const [total, setTotal] = useState(0);
Expand All @@ -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,
});
Expand Down Expand Up @@ -268,7 +269,7 @@ export default function AIHistoryPanel() {
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="text-[10px] uppercase tracking-wider h-8">Feature</TableHead>
<TableHead className="text-[10px] uppercase tracking-wider h-8">Database</TableHead>
{!dbId && <TableHead className="text-[10px] uppercase tracking-wider h-8">Database</TableHead>}
<TableHead className="text-[10px] uppercase tracking-wider h-8">Provider</TableHead>
<TableHead className="text-[10px] uppercase tracking-wider h-8 text-right">Tokens</TableHead>
<TableHead className="text-[10px] uppercase tracking-wider h-8 text-right">Created</TableHead>
Expand All @@ -292,15 +293,17 @@ export default function AIHistoryPanel() {
{FEATURE_LABELS[item.feature] ?? item.feature}
</Badge>
</TableCell>
<TableCell className="py-2 text-xs text-foreground/70 max-w-[140px] truncate">
{item.datasource_id ? (
<span title={item.datasource_id}>
{databases?.find(d => d.id === item.datasource_id)?.name || `${item.datasource_id.slice(0, 8)}...`}
</span>
) : (
<span className="text-muted-foreground/40 italic">β€”</span>
)}
</TableCell>
{!dbId && (
<TableCell className="py-2 text-xs text-foreground/70 max-w-[140px] truncate">
{item.datasource_id ? (
<span title={item.datasource_id}>
{databases?.find(d => d.id === item.datasource_id)?.name || `${item.datasource_id.slice(0, 8)}...`}
</span>
) : (
<span className="text-muted-foreground/40 italic">Global</span>
)}
</TableCell>
)}
<TableCell className="py-2 text-xs text-foreground/70">
{PROVIDER_LABELS[item.provider] ?? item.provider}
</TableCell>
Expand Down
92 changes: 72 additions & 20 deletions src/features/ai/components/AIResultDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,31 @@ export function AIResultDialog({
)}
</DialogTitle>
{/* Description row with optional timestamp + re-analyze */}
<div className="flex items-center gap-2">
{description && (
<DialogDescription className="text-xs flex-1">{description}</DialogDescription>
)}
{markdown && !loading && cached && createdAt && (
<span className="text-[10px] text-muted-foreground/60 whitespace-nowrap">
Generated {timeAgo(createdAt)}
</span>
)}
{markdown && !loading && cached && onReanalyze && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[10px] gap-1 text-muted-foreground hover:text-foreground"
onClick={onReanalyze}
>
<RefreshCw className="h-3 w-3" />
Re-analyze
</Button>
)}
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
{description && (
<DialogDescription className="text-xs flex-1">{description}</DialogDescription>
)}
{markdown && !loading && cached && createdAt && (
<span className="text-[10px] text-muted-foreground/60 whitespace-nowrap">
Generated {timeAgo(createdAt)}
</span>
)}
{markdown && !loading && cached && onReanalyze && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[10px] gap-1 text-muted-foreground hover:text-foreground"
onClick={onReanalyze}
>
<RefreshCw className="h-3 w-3" />
Re-analyze
</Button>
)}
</div>
<p className="text-[10px] text-muted-foreground/60">
Note: AI can make mistakes. Please verify responses and generated queries.
</p>
</div>
</DialogHeader>

Expand Down Expand Up @@ -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(
<div key={i} className="overflow-x-auto my-3 border border-border/30 rounded-md">
<table className="w-full text-xs text-left border-collapse">
<thead>
<tr className="border-b border-border/50 bg-muted/30">
{headerCells.map((cell, idx) => (
<th key={idx} className="px-3 py-2 font-semibold text-foreground/80 whitespace-nowrap">
{renderInline(cell.trim())}
</th>
))}
</tr>
</thead>
<tbody>
{bodyLines.map((rowLine, rowIdx) => (
<tr key={rowIdx} className="border-b border-border/20 last:border-0 hover:bg-muted/10">
{parseRow(rowLine).map((cell, idx) => (
<td key={idx} className="px-3 py-2 text-foreground/70">
{renderInline(cell.trim())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
continue;
}

// Blank line
if (!line.trim()) {
i++;
Expand Down
2 changes: 1 addition & 1 deletion src/features/chart/components/ChartVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const ChartVisualization = ({
type: c.type,
isPrimaryKey: c.isPrimaryKey,
})),
});
}, { datasourceName: dbId });
setChartType(rec.chartType);
setXAxis(rec.xAxis);
setYAxis(rec.yAxis);
Expand Down
87 changes: 87 additions & 0 deletions src/features/database/components/AIWorkspacePanel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col h-full overflow-y-auto bg-background p-6 gap-6">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2 mb-2">
<Sparkles className="h-6 w-6 text-primary" />
AI Workspace
</h1>
<p className="text-muted-foreground text-sm">
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.
</p>
<p className="text-xs text-muted-foreground/60 mt-1">
Note: AI can make mistakes. Please verify responses and generated queries.
</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card className="bg-card hover:bg-accent/5 transition-colors border-border/40">
<CardHeader className="pb-3">
<CardTitle className="text-base font-semibold flex items-center gap-2">
<Terminal className="h-4 w-4 text-emerald-500" />
Natural Language to SQL
</CardTitle>
<CardDescription className="text-xs">
Write database queries using plain English sentences. Let AI generate the SQL for you.
</CardDescription>
</CardHeader>
<CardContent>
<Button size="sm" variant="secondary" className="w-full text-xs" onClick={() => onNavigate("sql-workspace")}>
Launch NL Query
</Button>
</CardContent>
</Card>

<Card className="bg-card hover:bg-accent/5 transition-colors border-border/40">
<CardHeader className="pb-3">
<CardTitle className="text-base font-semibold flex items-center gap-2">
<Database className="h-4 w-4 text-violet-500" />
Schema Analysis
</CardTitle>
<CardDescription className="text-xs">
Generate AI documentation and summaries for your entire database schema.
</CardDescription>
</CardHeader>
<CardContent>
<Button size="sm" variant="secondary" className="w-full text-xs" onClick={() => onNavigate("schema-explorer")}>
Go to Schema Explorer
</Button>
</CardContent>
</Card>

<Card className="bg-card hover:bg-accent/5 transition-colors border-border/40">
<CardHeader className="pb-3">
<CardTitle className="text-base font-semibold flex items-center gap-2">
<Search className="h-4 w-4 text-blue-500" />
Query Explanation
</CardTitle>
<CardDescription className="text-xs">
Highlight any complex SQL query in the workspace and let AI explain what it does step-by-step.
</CardDescription>
</CardHeader>
<CardContent>
<Button size="sm" variant="secondary" className="w-full text-xs" onClick={() => onNavigate("sql-workspace")}>
Open SQL Workspace
</Button>
</CardContent>
</Card>
</div>

<div className="flex-1 mt-4">
{/* Pass dbId so it strictly filters for this DB and hides the Global DB column */}
<AIHistoryPanel dbId={dbId} />
</div>
</div>
);
}
Loading
Loading