"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
)}
{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 */}
{/* Footer hint */}
Attach PDF · DOCX · CSV · TXT · Images · AI responses are informational, not financial advice
);
}