"use client"; import { useEffect, useState, useRef } from "react"; import Image from "next/image"; import { Send, History, Sparkles, Trash2, MessageSquare, Clock, ChevronLeft, Menu, X, Activity, Database } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import type { UIPlan } from "@/components/dynamic/types"; type Health = { status: string; duckdb_ok: boolean; tables: number; llm_keys: number; mode: string; }; type HistoryEntry = { q: string; plan: UIPlan; ts: number }; type Props = { history: HistoryEntry[]; onAsk: (q: string) => void; onSelect: (e: HistoryEntry) => void; onClear?: () => void; loading: boolean; children: React.ReactNode; liveMode?: boolean; onToggleLive?: () => void; onOpenKB?: () => void; }; export function ChatShell({ history, onAsk, onSelect, onClear, loading, children, liveMode, onToggleLive, onOpenKB }: Props) { const [q, setQ] = useState(""); const [sidebarOpen, setSidebarOpen] = useState(false); const inputRef = useRef(null); const submit = () => { if (!q.trim() || loading) return; onAsk(q); setQ(""); }; return (
{/* Mobile sidebar overlay */} {sidebarOpen && ( setSidebarOpen(false)} /> )} { onSelect(e); setSidebarOpen(false); }} onClear={onClear} mobileOpen={sidebarOpen} onCloseMobile={() => setSidebarOpen(false)} liveMode={liveMode} onToggleLive={onToggleLive ? () => { onToggleLive(); setSidebarOpen(false); } : undefined} onOpenKB={onOpenKB ? () => { onOpenKB(); setSidebarOpen(false); } : undefined} />
setSidebarOpen(true)} />
{children}
{/* ── Enhanced Input Bar ──────────────────────────────── */}
{ e.preventDefault(); submit(); }} className="mx-auto max-w-4xl" >
setQ(e.target.value)} placeholder="Ask about budgets, programs, regional trends…" className="h-10 flex-1 bg-transparent text-[15px] text-mb-ink outline-none placeholder:text-mb-slate/40" suppressHydrationWarning autoComplete="off" />
{q.trim() && ( ↵ Enter )}
Results include provenance citations from your database · No hallucinated data
); } function Sidebar({ history, onSelect, onClear, mobileOpen, onCloseMobile, liveMode, onToggleLive, onOpenKB, }: Pick & { mobileOpen: boolean; onCloseMobile: () => void; }) { return ( ); } function fmtTime(ts: number): string { const d = new Date(ts); const now = new Date(); const sameDay = d.toDateString() === now.toDateString(); return sameDay ? d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + " · " + d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); } function Topbar({ onMenuClick }: { onMenuClick: () => void }) { const [h, setH] = useState(null); useEffect(() => { let alive = true; const load = () => fetch("/api/backend/health") .then((r) => (r.ok ? r.json() : null)) .then((j) => alive && setH(j)) .catch(() => alive && setH(null)); load(); const id = setInterval(load, 15000); return () => { alive = false; clearInterval(id); }; }, []); const tables = h?.tables ?? 0; const keys = h?.llm_keys ?? 0; const mode = h?.mode ?? "…"; const dbOk = h?.duckdb_ok; const loadingHealth = h === null; const dotColor = loadingHealth ? "bg-yellow-400 animate-pulse" : dbOk && keys > 0 ? "bg-emerald-400" : "bg-rose-400"; return (
finai · {loadingHealth ? "connecting…" : dbOk === false ? "database unavailable" : `${tables} tables · Llama 3.3`}
{!loadingHealth && keys > 0 && ( {keys} keys active )} FY 2025–26
); }