"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Send, Sparkles, TrendingUp, Shield, PieChart, Zap, Copy, ThumbsUp, ThumbsDown, Trash2, Mic, Paperclip, FileText, ImageIcon, X, Upload, CheckCircle2, AlertTriangle, ChevronDown, ChevronUp, FileSpreadsheet, File, } from "lucide-react"; import { aiApi, memoryApi, documentsApi } from "@/lib/api"; import { useAuthStore } from "@/lib/stores/authStore"; import { useLanguageStore } from "@/lib/stores/languageStore"; import { useThemeStore } from "@/lib/stores/themeStore"; // ─── Types ──────────────────────────────────────────────────────────────────── interface AttachedFile { file: File; previewUrl?: string; // for images status: "pending" | "uploading" | "done" | "error"; docId?: string; // backend document id after upload summary?: string; insights?: string[]; errorMsg?: string; } interface Message { id: string; role: "user" | "assistant"; content: string; streaming?: boolean; timestamp: Date; // doc context attached to this user message attachments?: Array<{ name: string; fileType: string; docId: string; summary?: string; insights?: string[]; }>; // if this was a doc-context answer docMode?: boolean; } // ─── Accepted types ─────────────────────────────────────────────────────────── const ACCEPTED = ".pdf,.docx,.txt,.csv,.png,.jpg,.jpeg,.webp"; const IMAGE_TYPES = ["image/png", "image/jpeg", "image/jpg", "image/webp"]; const DOC_TYPES = [ "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "text/plain", "text/csv", ]; // ─── Suggestions per language ───────────────────────────────────────────────── const SUGGESTIONS = { en: [ { icon: TrendingUp, label: "What's my total balance?", color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20" }, { icon: PieChart, label: "Analyze my spending this month", color: "text-blue-400", bg: "bg-blue-500/10 border-blue-500/20" }, { icon: Shield, label: "Explain my fraud alerts", color: "text-purple-400", bg: "bg-purple-500/10 border-purple-500/20" }, { icon: Zap, label: "Give me a savings nudge", color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20" }, ], hi: [ { icon: TrendingUp, label: "मेरा कुल बैलेंस क्या है?", color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20" }, { icon: PieChart, label: "इस महीने मेरा खर्च विश्लेषण करें", color: "text-blue-400", bg: "bg-blue-500/10 border-blue-500/20" }, { icon: Shield, label: "मेरे फ्रॉड अलर्ट समझाएं", color: "text-purple-400", bg: "bg-purple-500/10 border-purple-500/20" }, { icon: Zap, label: "बचत के लिए सुझाव दें", color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20" }, ], mr: [ { icon: TrendingUp, label: "माझी एकूण शिल्लक किती आहे?", color: "text-emerald-400", bg: "bg-emerald-500/10 border-emerald-500/20" }, { icon: PieChart, label: "या महिन्याचा खर्च विश्लेषण करा", color: "text-blue-400", bg: "bg-blue-500/10 border-blue-500/20" }, { icon: Shield, label: "माझे फसवणूक अलर्ट सांगा", color: "text-purple-400", bg: "bg-purple-500/10 border-purple-500/20" }, { icon: Zap, label: "बचतीसाठी सल्ला द्या", color: "text-amber-400", bg: "bg-amber-500/10 border-amber-500/20" }, ], }; // ─── File icon helper ───────────────────────────────────────────────────────── function FileIcon({ type, className }: { type: string; className?: string }) { if (IMAGE_TYPES.includes(type)) return ; if (type === "text/csv") return ; if (type.includes("pdf")) return ; return ; } function fileTypeLabel(type: string, name: string): string { const ext = name.split(".").pop()?.toUpperCase() ?? "FILE"; if (IMAGE_TYPES.includes(type)) return "IMAGE"; return ext; } function formatBytes(b: number) { if (b < 1024) return `${b} B`; if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; return `${(b / (1024 * 1024)).toFixed(1)} MB`; } // ─── AI Orb ─────────────────────────────────────────────────────────────────── function AIOrb({ isThinking }: { isThinking: boolean }) { return (
); } // ─── Attachment Card (inside input area) ───────────────────────────────────── function AttachmentChip({ file, onRemove, isLight }: { file: AttachedFile; onRemove: () => void; isLight: boolean }) { const isImg = IMAGE_TYPES.includes(file.file.type); return ( {/* Status indicator */} {file.status === "uploading" && ( )} {file.status === "done" && } {file.status === "error" && } {file.status === "pending" && ( isImg ? : )} {/* Preview thumb for images */} {isImg && file.previewUrl && ( // eslint-disable-next-line @next/next/no-img-element preview )} {file.file.name} {formatBytes(file.file.size)} {file.status !== "uploading" && ( )} ); } // ─── Document Badge (in message bubble) ────────────────────────────────────── function DocBadge({ name, fileType, summary, insights, isLight, }: { name: string; fileType: string; summary?: string; insights?: string[]; isLight: boolean; }) { const [expanded, setExpanded] = useState(false); const isImg = ["png", "jpg", "jpeg", "webp"].includes(fileType.toLowerCase()); return (
{isImg ? : } {name} {fileType.toUpperCase()} {(summary || (insights && insights.length > 0)) && ( )}
{expanded && (summary || (insights && insights.length > 0)) && ( {summary && (

{summary}

)} {insights && insights.length > 0 && (
    {insights.map((ins, i) => (
  • {ins}
  • ))}
)}
)}
); } // ─── Message Bubble ─────────────────────────────────────────────────────────── function MessageBubble({ message, onCopy, userInitial, isLight, }: { message: Message; onCopy: (t: string) => void; userInitial: string; isLight: boolean; }) { const isUser = message.role === "user"; const renderContent = (text: string) => text.split("\n").map((line, i) => { if (line.startsWith("**") && line.endsWith("**")) return

{line.slice(2, -2)}

; if (line.startsWith("- ")) return

• {line.slice(2)}

; if (line.startsWith("# ")) return

{line.slice(2)}

; return (

{line}

); }); return ( {/* Avatar */}
{isUser ? userInitial : }
{/* Content */}
{/* Attachment badges (user side) */} {isUser && message.attachments && message.attachments.length > 0 && (
{message.attachments.map((att) => ( ))}
)} {/* Doc-mode label on AI answer */} {!isUser && message.docMode && (
Document Analysis
)} {/* Bubble */} {message.content && (
{renderContent(message.content)}
{message.streaming && }
)} {/* Action row for AI messages */} {!isUser && !message.streaming && message.content && (
{[ { icon: Copy, label: "Copy", action: () => onCopy(message.content) }, { icon: ThumbsUp, label: "Good", action: () => {} }, { icon: ThumbsDown, label: "Bad", action: () => {} }, ].map(({ icon: Icon, label, action }) => ( ))}
)}
); } // ─── Main Chat Page ─────────────────────────────────────────────────────────── export default function ChatPage() { const { user } = useAuthStore(); const { language, t } = useLanguageStore(); const { theme } = useThemeStore(); const isLight = theme === "light"; const userInitial = user?.name?.charAt(0).toUpperCase() ?? "U"; const suggestions = SUGGESTIONS[language as keyof typeof SUGGESTIONS] ?? SUGGESTIONS.en; const [sessionId, setSessionId] = useState(() => typeof window !== "undefined" ? localStorage.getItem("bb_chat_session") : null ); const [messages, setMessages] = useState([]); const [historyLoaded, setHistoryLoaded] = useState(false); const [input, setInput] = useState(""); const [isThinking, setIsThinking] = useState(false); const [attachedFiles, setAttachedFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const inputRef = useRef(null); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const abortRef = useRef(null); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Load history on mount useEffect(() => { if (historyLoaded) return; const load = async () => { try { const res = await memoryApi.history(sessionId ?? undefined); if (res.messages.length > 0) { setMessages(res.messages.map((m) => ({ id: m.id, role: m.role as "user" | "assistant", content: m.content, timestamp: new Date(m.created_at), }))); if (res.messages[0].session_id) { setSessionId(res.messages[0].session_id); localStorage.setItem("bb_chat_session", res.messages[0].session_id); } } else { setMessages([{ id: "welcome", role: "assistant", content: language === "hi" ? "नमस्ते! मैं आपका AI वित्तीय सहायक हूँ। आप बैंक स्टेटमेंट, इनवॉइस, CSV या कोई भी दस्तावेज़ अटैच करके उसके बारे में पूछ सकते हैं।\n\nआज आप क्या जानना चाहते हैं?" : language === "mr" ? "नमस्कार! मी तुमचा AI आर्थिक सहाय्यक आहे. बँक स्टेटमेंट, इनव्हॉइस किंवा कोणतेही कागदपत्र अटॅच करा आणि प्रश्न विचारा.\n\nआज तुम्हाला काय जाणून घ्यायचे आहे?" : "Hello! I'm your AI financial assistant.\n\nYou can **attach files** (PDF, DOCX, CSV, images) directly here and ask me anything about them — bank statements, invoices, contracts, and more.\n\nWhat would you like to explore today?", timestamp: new Date(), }]); } } catch { setMessages([{ id: "welcome", role: "assistant", content: "Hello! I'm your AI financial assistant. Attach any document and ask me about it, or ask about your finances.", timestamp: new Date(), }]); } setHistoryLoaded(true); }; load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps // ── Upload a single file → returns docId + analysis ────────────────────── const uploadFile = useCallback(async (af: AttachedFile): Promise => { try { setAttachedFiles((prev) => prev.map((f) => f.file === af.file ? { ...f, status: "uploading" } : f) ); const result = await documentsApi.upload(af.file, language); const updated: AttachedFile = { ...af, status: "done", docId: result.id, summary: result.summary, insights: result.insights, }; setAttachedFiles((prev) => prev.map((f) => f.file === af.file ? updated : f) ); return updated; } catch (err) { const updated: AttachedFile = { ...af, status: "error", errorMsg: (err as Error).message || "Upload failed", }; setAttachedFiles((prev) => prev.map((f) => f.file === af.file ? updated : f) ); return updated; } }, [language]); // ── Add files (from input or drop) ─────────────────────────────────────── const addFiles = useCallback((files: FileList | File[]) => { const arr = Array.from(files); const valid = arr.filter((f) => { const ok = [...IMAGE_TYPES, ...DOC_TYPES].includes(f.type) || ["pdf","docx","txt","csv","png","jpg","jpeg","webp"].some(ext => f.name.toLowerCase().endsWith(`.${ext}`) ); return ok && f.size <= 10 * 1024 * 1024; }); if (valid.length === 0) return; const newFiles: AttachedFile[] = valid.map((f) => ({ file: f, previewUrl: IMAGE_TYPES.includes(f.type) ? URL.createObjectURL(f) : undefined, status: "pending", })); setAttachedFiles((prev) => { const updated = [...prev, ...newFiles]; // Kick off uploads immediately newFiles.forEach((af) => { // slight delay so state settles setTimeout(() => uploadFile(af), 100); }); return updated; }); }, [uploadFile]); // ── Drag-and-drop ───────────────────────────────────────────────────────── const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files); }; // ── Streaming word-by-word ─────────────────────────────────────────────── const streamWords = useCallback((msgId: string, fullText: string) => { const words = fullText.split(" "); let i = 0; setMessages((prev) => prev.map((m) => m.id === msgId ? { ...m, streaming: true } : m)); const interval = setInterval(() => { if (i >= words.length) { clearInterval(interval); setMessages((prev) => prev.map((m) => m.id === msgId ? { ...m, streaming: false } : m)); return; } setMessages((prev) => prev.map((m) => m.id === msgId ? { ...m, content: words.slice(0, i + 1).join(" ") } : m) ); i++; }, 28); }, []); // ── Send message ───────────────────────────────────────────────────────── const sendMessage = useCallback(async (text?: string) => { const content = (text ?? input).trim(); // Need either text or a completed doc const doneDocs = attachedFiles.filter((f) => f.status === "done" && f.docId); if (!content && doneDocs.length === 0) return; if (isThinking) return; abortRef.current?.abort(); abortRef.current = new AbortController(); setInput(""); setIsThinking(true); // Build user message const userMsg: Message = { id: `user-${Date.now()}`, role: "user", content, timestamp: new Date(), attachments: doneDocs.map((f) => ({ name: f.file.name, fileType: f.file.name.split(".").pop() ?? "file", docId: f.docId!, summary: f.summary, insights: f.insights, })), }; setMessages((prev) => [...prev, userMsg]); // Clear attached files after sending setAttachedFiles([]); const aiId = `ai-${Date.now()}`; setMessages((prev) => [ ...prev, { id: aiId, role: "assistant", content: "", streaming: true, timestamp: new Date(), docMode: doneDocs.length > 0 }, ]); try { let responseText = ""; if (doneDocs.length > 0) { // ── Document Q&A mode ──────────────────────────────────────────── const doc = doneDocs[0]; // use first doc for Q&A const question = content || (language === "hi" ? "इस दस्तावेज़ का सारांश और मुख्य जानकारी बताएं।" : language === "mr" ? "या कागदपत्राचा सारांश आणि मुख्य माहिती सांगा." : "Please summarize this document and give me the key financial insights."); const res = await documentsApi.chat(doc.docId!, question, language); responseText = res.answer; // If multiple docs, answer about remaining ones too for (const extraDoc of doneDocs.slice(1)) { const extra = await documentsApi.chat( extraDoc.docId!, question, language ); responseText += `\n\n---\n**${extraDoc.file.name}:**\n${extra.answer}`; } } else { // ── Regular financial chat ──────────────────────────────────────── const res = await aiApi.chat(content, user?.user_id, sessionId ?? undefined, language); responseText = res.response; } setIsThinking(false); streamWords(aiId, responseText); // Persist to memory const msgContent = content || (doneDocs.map(d => `[Document: ${d.file.name}]`).join(", ")); const sid = sessionId; const savedUser = await memoryApi.save({ session_id: sid ?? undefined, role: "user", content: msgContent, session_title: msgContent.slice(0, 40), }); const newSid = savedUser.session_id; if (newSid && newSid !== sessionId) { setSessionId(newSid); localStorage.setItem("bb_chat_session", newSid); } await memoryApi.save({ session_id: newSid ?? undefined, role: "assistant", content: responseText }); } catch (err) { setIsThinking(false); const errMsg = (err as Error).message === "Session expired. Please log in again." ? "Session expired. Please refresh and log in again." : "⚠️ Could not reach the AI backend. Please try again."; setMessages((prev) => prev.map((m) => m.id === aiId ? { ...m, content: errMsg, streaming: false } : m) ); } }, [input, isThinking, attachedFiles, user?.user_id, sessionId, streamWords, language]); const clearHistory = async () => { try { await memoryApi.clear(sessionId ?? undefined); setSessionId(null); setAttachedFiles([]); localStorage.removeItem("bb_chat_session"); setMessages([{ id: "welcome", role: "assistant", content: "Chat history cleared. How can I help you?", timestamp: new Date(), }]); } catch { /* ignore */ } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const isStreaming = messages.some((m) => m.streaming); const hasUploading = attachedFiles.some((f) => f.status === "uploading"); const hasDone = attachedFiles.some((f) => f.status === "done"); const canSend = (input.trim() || hasDone) && !isThinking && !isStreaming && !hasUploading; const headerBg = isLight ? "bg-white/90 border-black/8" : "bg-black/20 border-white/10"; const inputBg = isLight ? "bg-white/90 border-black/8" : "bg-black/20 border-white/10"; return (
{/* ── Drag overlay ───────────────────────────────────────────────────── */} {isDragging && (

Drop files here

PDF, DOCX, CSV, TXT, Images — max 10 MB

)}
{/* ── Header ─────────────────────────────────────────────────────────── */}
Live AI · {language.toUpperCase()}

BankBot AI Assistant

Context-aware · Document analysis · Memory enabled

{/* Upload shortcut */} fileInputRef.current?.click()} title="Attach file" className={`flex items-center gap-1.5 rounded-xl border px-3 py-1.5 text-xs font-medium transition-colors ${ isLight ? "border-purple-200 bg-purple-50 text-purple-600 hover:bg-purple-100" : "border-purple-500/30 bg-purple-500/10 text-purple-400 hover:bg-purple-500/20" }`} > Attach
{/* ── Messages ───────────────────────────────────────────────────────── */}
{/* Welcome screen */} {messages.length === 1 && messages[0].id === "welcome" && (

Your AI Financial Twin

{t("chat_placeholder")}

{/* File type shortcuts */}
{[ { icon: FileText, label: "Bank Statement", ext: ".pdf", color: "text-blue-400", bg: isLight ? "bg-blue-50 border-blue-200" : "bg-blue-500/10 border-blue-500/20" }, { icon: FileSpreadsheet, label: "CSV / Excel", ext: ".csv", color: "text-emerald-400", bg: isLight ? "bg-emerald-50 border-emerald-200" : "bg-emerald-500/10 border-emerald-500/20" }, { icon: FileText, label: "Invoice / Doc", ext: ".docx", color: "text-purple-400", bg: isLight ? "bg-purple-50 border-purple-200" : "bg-purple-500/10 border-purple-500/20" }, { icon: ImageIcon, label: "Statement Image",ext: ".png", color: "text-amber-400", bg: isLight ? "bg-amber-50 border-amber-200" : "bg-amber-500/10 border-amber-500/20" }, ].map((item) => ( fileInputRef.current?.click()} className={`flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-medium transition-all ${item.bg} ${item.color}`} > {item.label} ))}
{/* Standard suggestions */}
{suggestions.map((s) => { const Icon = s.icon; return ( sendMessage(s.label)} className={`flex items-center gap-2 rounded-xl border px-3 py-2.5 text-sm font-medium transition-all ${s.bg} ${s.color} hover:brightness-110`} > {s.label} ); })}
)} {/* Message list */} {messages.map((msg) => ( navigator.clipboard.writeText(t).catch(() => {})} userInitial={userInitial} isLight={isLight} /> ))} {/* Thinking indicator */} {isThinking && (
{[0, 1, 2].map((i) => ( ))}
)}
{/* ── Quick suggestions (post-first-message) ─────────────────────────── */} {messages.length > 1 && (
{suggestions.map((s) => { const Icon = s.icon; return ( ); })}
)} {/* ── Input area ─────────────────────────────────────────────────────── */}
{/* Attached files preview */} {attachedFiles.length > 0 && ( {attachedFiles.map((af, i) => ( { if (af.previewUrl) URL.revokeObjectURL(af.previewUrl); setAttachedFiles((prev) => prev.filter((_, idx) => idx !== i)); }} /> ))} )} {/* Input row */}
{/* Attach button */} {/* Hidden file input */} { if (e.target.files) { addFiles(e.target.files); e.target.value = ""; } }} /> {/* Text area */}