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 { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { Eraser, Sparkles, Layers } from "lucide-react"; import { useAppStore } from "@/stores/app-store"; import { useAiSettings } from "@/hooks/use-ai"; import type { ContextUsage } from "@/types"; interface Props { tabId: string; connectionId: string; } export function ChatPanel({ tabId, connectionId }: Props) { const { messages, pending, usage, send, clear, compact } = useChat(tabId, connectionId); const dbFlavors = useAppStore((s) => s.dbFlavors); const flavor = dbFlavors[connectionId]; const { data: aiSettings } = useAiSettings(); const aiReady = !!aiSettings?.model; const scrollerRef = useRef(null); useEffect(() => { scrollerRef.current?.scrollTo({ top: scrollerRef.current.scrollHeight, behavior: "smooth", }); }, [messages.length, pending]); return (
AI Assistant {flavor && ( · {flavor} )} {aiSettings?.model && ( · {aiSettings.model} )}
{messages.length === 0 && !pending ? ( ) : (
{messages.map((m) => ( ))} {pending && }
)}
); } function UsageBadge({ usage }: { usage: ContextUsage | undefined }) { if (!usage || usage.budget_chars === 0) return null; const ratio = Math.min(usage.used_chars / usage.budget_chars, 1.5); const usedTok = Math.round(usage.used_chars / 3 / 100) / 10; // ~k-tokens with 1 decimal const budgetTok = Math.round(usage.budget_chars / 3 / 100) / 10; const percent = Math.round(ratio * 100); let toneClass = "text-muted-foreground/70"; if (ratio >= 0.85) toneClass = "text-destructive"; else if (ratio >= 0.6) toneClass = "text-warning"; else if (ratio >= 0.3) toneClass = "text-success/80"; const trackClass = "h-1.5 w-12 overflow-hidden rounded-full bg-muted"; let fillClass = "bg-success/70"; if (ratio >= 0.85) fillClass = "bg-destructive"; else if (ratio >= 0.6) fillClass = "bg-warning"; return (
{usedTok}k / {budgetTok}k tok · {percent}%
Approximate context usage. {usage.used_chars.toLocaleString()} chars sent to the model last turn out of {usage.budget_chars.toLocaleString()} budget. {ratio >= 0.6 && " Type /compact (or click Compact) to summarise older history."} ); } function PendingIndicator() { return (
Thinking...
); } function EmptyState({ aiReady, flavor }: { aiReady: boolean; flavor: string | undefined }) { return (

Ask anything about your data

{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."}

{aiReady && (

Slash commands: /compact · /clear

)}
); }