Files
tusk/src/components/results/ResultsPanel.tsx
A.Shakhmatov 3ad0ee5cc3 feat: add AI Explain Query and Fix Error via Ollama
Extract shared call_ollama_chat helper from generate_sql to reuse
settings loading and Ollama API call logic. Add two new AI commands:
- explain_sql: explains what a SQL query does in plain language
- fix_sql_error: suggests corrected SQL based on the error and schema

UI additions: "AI Explain" toolbar button, "Explain" and "Fix with AI"
action buttons on query errors, inline explanation display in results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:48:39 +03:00

143 lines
3.9 KiB
TypeScript

import { ResultsTable } from "./ResultsTable";
import { ResultsJsonView } from "./ResultsJsonView";
import type { QueryResult } from "@/types";
import { Loader2, AlertCircle, Sparkles, Wand2 } from "lucide-react";
import { Button } from "@/components/ui/button";
interface Props {
result?: QueryResult | null;
error?: string | null;
isLoading?: boolean;
viewMode?: "table" | "json";
onCellDoubleClick?: (
rowIndex: number,
colIndex: number,
value: unknown
) => void;
highlightedCells?: Set<string>;
aiExplanation?: string | null;
isAiLoading?: boolean;
onExplainError?: () => void;
onFixError?: () => void;
}
export function ResultsPanel({
result,
error,
isLoading,
viewMode = "table",
onCellDoubleClick,
highlightedCells,
aiExplanation,
isAiLoading,
onExplainError,
onFixError,
}: Props) {
if (isLoading) {
return (
<div className="flex h-full items-center justify-center text-muted-foreground">
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Executing query...
</div>
);
}
if (aiExplanation) {
return (
<div className="h-full overflow-auto p-4">
<div className="rounded-md border bg-muted/30 p-4">
<div className="mb-2 flex items-center gap-2 text-xs font-medium text-muted-foreground">
<Sparkles className="h-3.5 w-3.5" />
AI Explanation
</div>
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-foreground">
{aiExplanation}
</pre>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 p-4">
<div className="flex items-start gap-2 rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<pre className="whitespace-pre-wrap font-mono text-xs">{error}</pre>
</div>
{(onExplainError || onFixError) && (
<div className="flex items-center gap-2">
{onExplainError && (
<Button
size="sm"
variant="outline"
className="h-7 gap-1.5 text-xs"
onClick={onExplainError}
disabled={isAiLoading}
>
{isAiLoading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Sparkles className="h-3 w-3" />
)}
Explain
</Button>
)}
{onFixError && (
<Button
size="sm"
variant="outline"
className="h-7 gap-1.5 text-xs"
onClick={onFixError}
disabled={isAiLoading}
>
{isAiLoading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Wand2 className="h-3 w-3" />
)}
Fix with AI
</Button>
)}
</div>
)}
</div>
);
}
if (!result) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Press Ctrl+Enter to execute query
</div>
);
}
if (result.columns.length === 0) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Query executed successfully. {result.row_count} rows affected.
</div>
);
}
if (viewMode === "json") {
return (
<ResultsJsonView
columns={result.columns}
rows={result.rows}
/>
);
}
return (
<ResultsTable
columns={result.columns}
types={result.types}
rows={result.rows}
onCellDoubleClick={onCellDoubleClick}
highlightedCells={highlightedCells}
/>
);
}