feat: rescope to AI-first DB harness with multi-DB chat agent
Removes enterprise/DBA features and replaces the marginal AI bar with a central chat agent that has progressive-discovery tools, cross-session memory, saved-query reuse, and inline result actions. Adds ClickHouse support alongside PostgreSQL/Greenplum. Cleanup - Drop ~10k LOC of advanced features: Docker, Snapshots, Validation, Index Advisor, Role/User Management, Data Generator, ERD, Lookup. - Trim deps: drop @xyflow/react, dagre, @types/dagre; cut tokio features to rt-multi-thread/sync/time/net/macros. - Remove unused TuskError variants and dead helpers (topological_sort, invalidate_schema_cache). Multi-DB (PostgreSQL + ClickHouse) - New src-tauri/src/db/ module: ChClient (HTTP-based, reuses reqwest), sql_guard (cross-flavor read-only whitelist with 8 tests). - ConnectionConfig gains db_flavor and secure fields with serde defaults for backwards-compatible connections.json. - All connection/query/schema/data commands dispatch by flavor; CH covers connect, execute_query, list_databases/schemas/tables/views/ columns/completion_schema, paginated table fetch. - Frontend: dbCapabilities matrix, ConnectionDialog engine selector with port auto-swap and HTTPS toggle, SqlEditor switches to StandardSQL dialect for CH, TableDataView surfaces CH connections as read-only. AI-first chat agent - New src/components/chat/ panel with composer, message rendering, collapsible tool-call/result blocks, top-level ErrorBoundary. - Backend agent loop in commands/chat.rs with strict-JSON tool protocol. Nine tools: list_databases, list_tables, get_columns, switch_database, run_query, remember, save_query, find_queries, final. Forgiving parser accepts both flat and nested-input shapes. - Compressed history: only the last 4 run_query results carry sample rows (≤10, cells truncated to 200 chars) into LLM context; older results marked omitted. - System prompt uses lite OVERVIEW (DB list + active-DB tables only) instead of full DDL — schema details are loaded on demand via get_columns. CH OVERVIEW shows cross-DB tables since CH allows db.table queries. Cross-session memory (F1) - Per-connection markdown file at app_data_dir/memory/<connection_id>.md, 16KB cap with oldest-block eviction. Agent appends via remember() tool; the file is injected into LEARNED NOTES section of every system prompt. - New Memory sidebar tab with editable textarea, badge for note count, empty-state with template. Edits picked up on the next agent turn. Saved-query reuse (F2) - Tools save_query and find_queries scoped to current connection. save_query attaches a UUID + timestamp; find_queries returns top 10 matches with SQL preview ≤500 chars. - Storage shared with the sidebar Saved panel. Inline result actions (F3) - run_query result block in chat gets Open-full (90vw × 80vh modal with full ResultsTable, no row cap) and Export (reuses ExportDialog for CSV/JSON via existing exportCsv/exportJson commands). Verification - cargo check clean, zero warnings. - cargo test --lib: 50 pass (20 chat parser + 4 memory + 8 sql_guard + 6 clean_sql + 12 escape_ident). - npx tsc --noEmit clean. - npx vitest run: 20 pass.
This commit is contained in:
108
src/components/chat/ChatPanel.tsx
Normal file
108
src/components/chat/ChatPanel.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useChat } from "@/hooks/use-chat";
|
||||
import { ChatComposer } from "./ChatComposer";
|
||||
import { ChatMessageView } from "./ChatMessageView";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Eraser, Sparkles } from "lucide-react";
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { useAiSettings } from "@/hooks/use-ai";
|
||||
|
||||
interface Props {
|
||||
tabId: string;
|
||||
connectionId: string;
|
||||
}
|
||||
|
||||
export function ChatPanel({ tabId, connectionId }: Props) {
|
||||
const { messages, pending, send, clear } = useChat(tabId, connectionId);
|
||||
const dbFlavors = useAppStore((s) => s.dbFlavors);
|
||||
const flavor = dbFlavors[connectionId];
|
||||
const { data: aiSettings } = useAiSettings();
|
||||
const aiReady = !!aiSettings?.model;
|
||||
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
scrollerRef.current?.scrollTo({
|
||||
top: scrollerRef.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, [messages.length, pending]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-9 items-center justify-between border-b border-border/40 px-3">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5 text-primary/70" />
|
||||
<span className="font-medium">AI Assistant</span>
|
||||
{flavor && <span className="text-[10px] uppercase tracking-wider text-muted-foreground/60">· {flavor}</span>}
|
||||
{aiSettings?.model && (
|
||||
<span className="text-[10px] text-muted-foreground/60">· {aiSettings.model}</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={clear}
|
||||
disabled={messages.length === 0 || pending}
|
||||
className="h-6 gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Eraser className="h-3 w-3" />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div ref={scrollerRef} className="min-h-0 flex-1 overflow-y-auto">
|
||||
{messages.length === 0 && !pending ? (
|
||||
<EmptyState aiReady={aiReady} flavor={flavor} />
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 px-4 py-3">
|
||||
{messages.map((m) => (
|
||||
<ChatMessageView key={m.id} message={m} />
|
||||
))}
|
||||
{pending && <PendingIndicator />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/40 bg-background/40 p-2">
|
||||
<ChatComposer
|
||||
onSend={send}
|
||||
disabled={pending || !aiReady}
|
||||
placeholder={
|
||||
aiReady
|
||||
? "Ask in plain language. The agent will browse schema and run read-only queries."
|
||||
: "Configure an AI model in Settings to enable chat."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PendingIndicator() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground/70">
|
||||
<span className="inline-flex gap-0.5">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-primary/70" />
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-primary/70 [animation-delay:120ms]" />
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-primary/70 [animation-delay:240ms]" />
|
||||
</span>
|
||||
Thinking...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ aiReady, flavor }: { aiReady: boolean; flavor: string | undefined }) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<div className="max-w-md space-y-3 text-center">
|
||||
<Sparkles className="mx-auto h-8 w-8 text-primary/50" />
|
||||
<h3 className="text-sm font-medium">Ask anything about your data</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{aiReady
|
||||
? `Connected to ${flavor ?? "database"}. Try: "How many rows in each table?", "Top 10 customers by total spend", "Show me last week's orders".`
|
||||
: "Open Settings → AI to choose an Ollama model. Tusk will then assist with natural-language queries."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user