import React, { useState, useEffect, useRef } from 'react'; import { useAgent } from '../contexts/AgentContext'; import ReportRenderer from './ReportRenderer'; import { Box, Typography, IconButton, Avatar, Paper, TextField, InputAdornment, List, ListItem, ListItemAvatar, Chip, } from '@mui/material'; import SendIcon from '@mui/icons-material/Send'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import { jsPDF } from 'jspdf'; import { Document, Packer, Paragraph, ImageRun, Header as DocxHeader, AlignmentType, HeadingLevel, Table as DocxTable, TableRow as DocxTableRow, TableCell as DocxTableCell, WidthType, TextRun, BorderStyle, } from 'docx'; interface ChatInterfaceProps { onClose?: () => void; } const ChatInterface: React.FC = ({ onClose }) => { const { selectedAgent, setSelectedAgent } = useAgent(); type QuickReplyButton = { title: string; payload: string }; type CustomMultiSelect = { type: 'multi_select_chips'; options: string[]; columns?: number; hint?: string; selections?: string[]; // maintained client-side }; // Branding palette + tokens inspired by your screenshot const BRAND = { gradientFrom: '#FF6B00', gradientTo: '#FF3D00', headerBg: 'linear-gradient(135deg, #FF6B00 0%, #FF3D00 100%)', headerShadow: '0 10px 24px rgba(255, 80, 0, 0.25)', windowBg: '#F6F7FB', surface: '#FFFFFF', border: '#FFE0D1', botBubble: '#FFFFFF', userBubble: '#FFE9DF', userText: '#7A3D2B', inputBg: '#FFF4EE', inputBorder: '#FFC9B3', placeholder: '#9C6B5E', sendBtn: '#FA8C6A', sendBtnHover: '#FF7A50', chipBg: '#FFE4D3', chipText: '#7A3D2B', }; // Helper to push a bot notice message after an operation completes const pushBotNotice = (text: string) => setMessages(prev => [...prev, { sender: 'bot', text }]); type ChatMessage = { sender: 'user' | 'bot'; text?: string; buttons?: QuickReplyButton[]; custom?: CustomMultiSelect; json_message?: any; // pass-through for backend control messages }; const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); const lastReportRef = useRef(null); const reportRefs = useRef>(new Map()); // Unique sender per agent session to avoid reusing old Rasa tracker state const senderRef = useRef(''); // Track previous and last REST responses so we can download the prior one on demand const lastResponseRef = useRef(null); const prevResponseRef = useRef(null); // Static public paths for logos (put files in /public) const YUGEN_LOGO_SRC = '/yugensys.png'; const BIZ_LOGO_SRC = '/BizInsaights.png'; // NOTE: fixed spelling // Initialize a stable sender id once useEffect(() => { if (!senderRef.current) { senderRef.current = (crypto as any)?.randomUUID?.() ? `web-${(crypto as any).randomUUID()}` : `web-${Math.random().toString(36).slice(2, 10)}`; } }, []); // Show a "thinking..." message when loading useEffect(() => { if (isLoading) { const loadingText = selectedAgent?.id === 'lexa-legal' ? 'Lexa is thinking...' : 'BizInsights is thinking...'; setMessages(prev => { const alreadyShown = prev.some(m => m.text === loadingText); if (alreadyShown) return prev; return [...prev, { sender: 'bot', text: loadingText }]; }); } else { setMessages(prev => prev.filter(m => m.text !== 'BizInsights is thinking...' && m.text !== 'Lexa is thinking...' )); } }, [isLoading, selectedAgent]); // Util: download an object as a .json file const downloadJson = (obj: any, filename?: string) => { try { const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename || `response-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.json`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); sendMessage('/research_complete'); pushBotNotice('✅ JSON file downloaded'); } catch (e) { console.error('[JSON]Failed to download JSON: ', e); } }; // ---- PDF: helpers (fetch + downscale) ---- const fetchAsDataURL = async (src: string): Promise => { const res = await fetch(src, { credentials: 'include', cache: 'force-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${src}`); const blob = await res.blob(); return await new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); }; const downscaleDataURL = async ( dataUrl: string, maxEdgePx: number, mime: 'image/jpeg' | 'image/png', quality: number ): Promise => { const img = await new Promise((resolve, reject) => { const i = new Image(); i.onload = () => resolve(i); i.onerror = () => reject(new Error('Image decode failed')); i.src = dataUrl; }); const naturalW = (img as any).naturalWidth || img.width; const naturalH = (img as any).naturalHeight || img.height; const longest = Math.max(naturalW, naturalH); const scale = longest > maxEdgePx ? maxEdgePx / longest : 1; const w = Math.max(1, Math.round(naturalW * scale)); const h = Math.max(1, Math.round(naturalH * scale)); const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Canvas 2D context unavailable'); if (mime === 'image/jpeg') { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, w, h); } ctx.drawImage(img, 0, 0, w, h); return canvas.toDataURL(mime, quality); }; // --------- Rasa helpers (NEW) ---------- // Map Rasa REST payload to our ChatMessage array const mapRasaToMessages = (data: any[]): ChatMessage[] => { if (!Array.isArray(data)) return []; return data .map((m: any): ChatMessage | null => { let text = typeof m?.text === 'string' ? m.text : undefined; const buttons = Array.isArray(m?.buttons) ? m.buttons .filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string') .map((b: any) => ({ title: b.title as string, payload: b.payload as string })) : undefined; // Pass-through known custom blocks let custom: ChatMessage['custom'] = undefined; if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') { const opts = Array.isArray(m.custom.options) ? m.custom.options.filter((o: any) => typeof o === 'string') : []; custom = { type: 'multi_select_chips', options: opts, columns: typeof m.custom.columns === 'number' ? m.custom.columns : undefined, hint: typeof m.custom.hint === 'string' ? m.custom.hint : undefined, selections: [], }; } // Control messages let json_message = m?.json_message; if (!json_message && m?.custom && typeof m.custom === 'object' && typeof m.custom.action === 'string') { json_message = m.custom; } const action = json_message?.action; if (action === 'json:download' || action === 'pdf:download' || action === 'docx:download') { text = undefined; // suppress bubble for control actions } if (!text && (!buttons || buttons.length === 0) && !custom && !json_message) return null; return { sender: 'bot', text, buttons, custom, json_message }; }) .filter(Boolean) as ChatMessage[]; }; // ---- PDF: renderer with logos + selectable text ---- const generateReportPdf = async ({ content, title, yugenLogoSrc = YUGEN_LOGO_SRC, bizLogoSrc = BIZ_LOGO_SRC, logoMime = 'image/jpeg' as 'image/jpeg' | 'image/png', maxLogoEdgePx = 600, jpegQuality = 0.82, }: { content: string; title?: string; yugenLogoSrc?: string; bizLogoSrc?: string; logoMime?: 'image/jpeg' | 'image/png'; maxLogoEdgePx?: number; jpegQuality?: number; }): Promise => { console.info('[PDF] Generating...', { yugenLogoSrc, bizLogoSrc, logoMime }); const doc = new jsPDF({ unit: 'mm', format: 'a4', compress: true }); const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); const margin = 15; const logoHeight = 15; // mm const logoWidth = 40; // mm const contentWidth = pageWidth - 2 * margin; // Title: derive from first markdown header if not provided const derivedTitle = (() => { const firstHeader = (content || '') .split('\n') .map(s => s.trim()) .find(s => s.startsWith('# ')); return title || (firstHeader ? firstHeader.replace(/^#\s+/, '') : 'Report'); })(); // Load + downscale logos let yugenDataUrl: string | null = null; let bizDataUrl: string | null = null; try { const yugenWithCacheBust = `${yugenLogoSrc}?t=${Date.now()}`; const yugenRaw = await fetchAsDataURL(yugenWithCacheBust); yugenDataUrl = await downscaleDataURL(yugenRaw, maxLogoEdgePx, 'image/png', 1.0); } catch (e) { console.warn('[PDF] Yugen logo load failed, will skip:', yugenLogoSrc, e); } try { const bizWithCacheBust = `${bizLogoSrc}?t=${Date.now()}`; const bizRaw = await fetchAsDataURL(bizWithCacheBust); bizDataUrl = await downscaleDataURL(bizRaw, maxLogoEdgePx, 'image/png', 1.0); } catch (e) { console.warn('[PDF] Biz logo load failed, will skip:', bizLogoSrc, e); } const YUGEN_ALIAS = 'YUGEN_LOGO'; const BIZ_ALIAS = 'BIZ_LOGO'; let embeddedOnce = false; let cursorY = margin + logoHeight + 24; const baseLine = 6; // mm baseline leading const addHeader = () => { const fmt = logoMime === 'image/png' ? 'PNG' : 'JPEG'; if (yugenDataUrl) { if (!embeddedOnce) { doc.addImage(yugenDataUrl, fmt, margin, margin, logoWidth, logoHeight, YUGEN_ALIAS); } else { doc.addImage(YUGEN_ALIAS, fmt, margin, margin, logoWidth, logoHeight); } } if (bizDataUrl) { const x = pageWidth - margin - logoWidth; if (!embeddedOnce) { doc.addImage(bizDataUrl, fmt, x, margin, logoWidth, logoHeight, BIZ_ALIAS); } else { doc.addImage(BIZ_ALIAS, fmt, x, margin, logoWidth, logoHeight); } } embeddedOnce = true; doc.setDrawColor(200); doc.setLineWidth(0.3); doc.line(margin, margin + logoHeight + 4, pageWidth - margin, margin + logoHeight + 4); }; const ensurePage = (nextBlockHeight = 0) => { if (cursorY + nextBlockHeight > pageHeight - margin) { doc.addPage(); addHeader(); cursorY = margin + logoHeight + 10; } }; // First page header + title addHeader(); doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.text(derivedTitle, pageWidth / 2, margin + logoHeight + 14, { align: 'center' }); doc.setFont('helvetica', 'normal'); doc.setFontSize(12); // Light markdown handling const lines = (content || '').split('\n'); let inCode = false; const codeLines: string[] = []; const flushCode = () => { if (!inCode || codeLines.length === 0) return; doc.setFont('courier', 'normal'); const lh = 5; for (const cl of codeLines) { const wrapped = doc.splitTextToSize(cl, contentWidth); for (const w of wrapped) { ensurePage(lh); doc.text(w, margin, cursorY); cursorY += lh; } } doc.setFont('helvetica', 'normal'); inCode = false; codeLines.length = 0; cursorY += 1.5; }; for (let raw of lines) { const line = raw ?? ''; const t = line.trim(); if (t.startsWith('```')) { if (inCode) flushCode(); else { inCode = true; codeLines.length = 0; } continue; } if (inCode) { codeLines.push(line); continue; } if (!t) { cursorY += baseLine; continue; } if (t.startsWith('# ')) { const txt = t.replace(/^#\s+/, ''); doc.setFont('helvetica', 'bold'); doc.setFontSize(16); const wrapped = doc.splitTextToSize(txt, contentWidth); ensurePage(baseLine * wrapped.length + 4); for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; } cursorY += 2; doc.setFont('helvetica', 'normal'); doc.setFontSize(12); continue; } if (t.startsWith('## ')) { const txt = t.replace(/^##\s+/, ''); doc.setFont('helvetica', 'bold'); doc.setFontSize(14); const wrapped = doc.splitTextToSize(txt, contentWidth); ensurePage(baseLine * wrapped.length + 2); for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; } cursorY += 1; doc.setFont('helvetica', 'normal'); doc.setFontSize(12); continue; } if (t.startsWith('### ')) { const txt = t.replace(/^###\s+/, ''); doc.setFont('helvetica', 'bold'); doc.setFontSize(12); const wrapped = doc.splitTextToSize(txt, contentWidth); ensurePage(baseLine * wrapped.length + 1); for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; } doc.setFont('helvetica', 'normal'); continue; } if (/^(\* |- |• )/.test(t)) { const bulletText = t.replace(/^(\* |- |• )/, ''); const bulletIndent = 6; const textX = margin + bulletIndent; const wrapWidth = contentWidth - bulletIndent; const wrapped = doc.splitTextToSize(bulletText, wrapWidth); ensurePage(baseLine * wrapped.length); doc.text('•', margin + 1, cursorY); doc.text(wrapped[0], textX, cursorY); cursorY += baseLine; for (let k = 1; k < wrapped.length; k++) { ensurePage(baseLine); doc.text(wrapped[k], textX, cursorY); cursorY += baseLine; } continue; } const wrapped = doc.splitTextToSize(line, contentWidth); ensurePage(baseLine * wrapped.length); for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; } } if (inCode) flushCode(); const filename = `report-${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`; // 🟢 Trigger file download doc.save(filename); // Reuse your pipeline so the UI updates and isLoading is handled await sendMessage('/research_complete'); return filename; }; // ---- DOCX: Generator with logos + selectable text ---- const generateReportDocx = async ({ content, title, yugenLogoSrc = YUGEN_LOGO_SRC, bizLogoSrc = BIZ_LOGO_SRC, }: { content: string; title?: string; yugenLogoSrc?: string; bizLogoSrc?: string; }): Promise => { console.info('[DOCX] Generating text-based document...'); // Helper to fetch image as ArrayBuffer for the docx library const fetchImageAsArrayBuffer = async (src: string): Promise => { try { const res = await fetch(src, { credentials: 'include', cache: 'force-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status} for ${src}`); return await res.arrayBuffer(); } catch (e) { console.warn(`[DOCX] Image load failed for ${src}:`, e); return null; } }; const [yugenLogoBuffer, bizLogoBuffer] = await Promise.all([ fetchImageAsArrayBuffer(yugenLogoSrc), fetchImageAsArrayBuffer(bizLogoSrc), ]); const headerChildren: (Paragraph | DocxTable)[] = []; if (yugenLogoBuffer && bizLogoBuffer) { headerChildren.push( new DocxTable({ width: { size: '100%', type: WidthType.PERCENTAGE }, columnWidths: [2500, 5000, 2500], borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } }, rows: [ new DocxTableRow({ children: [ new DocxTableCell({ children: [new Paragraph({ children: [new ImageRun({ data: yugenLogoBuffer, transformation: { width: 160, height: 60 }, type: 'png', // or 'jpeg' based on your image type })] })], borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } }, }), new DocxTableCell({ children: [new Paragraph('')], borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } } }), new DocxTableCell({ children: [new Paragraph({ alignment: AlignmentType.RIGHT, children: [new ImageRun({ data: bizLogoBuffer, transformation: { width: 160, height: 60 }, type: 'png', // or 'jpeg' based on your image type })] })], borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } }, }), ], }), ], }) ); } headerChildren.push(new Paragraph({ style: 'header', border: { top: { style: BorderStyle.SINGLE }, bottom: { style: BorderStyle.SINGLE }, left: { style: BorderStyle.SINGLE }, right: { style: BorderStyle.SINGLE } } })); const derivedTitle = title || (content || '').split('\n').map(s => s.trim()).find(s => s.startsWith('# '))?.replace(/^#\s+/, '') || 'Report'; const bodyParagraphs: Paragraph[] = [ new Paragraph({ text: derivedTitle, heading: HeadingLevel.TITLE, alignment: AlignmentType.CENTER }), new Paragraph({ text: '' }), // Spacer ]; (content || '').split('\n').forEach(line => { const t = line.trim(); if (t.startsWith('### ')) { bodyParagraphs.push(new Paragraph({ text: t.replace(/^###\s+/, ''), heading: HeadingLevel.HEADING_3 })); } else if (t.startsWith('## ')) { bodyParagraphs.push(new Paragraph({ text: t.replace(/^##\s+/, ''), heading: HeadingLevel.HEADING_2 })); } else if (t.startsWith('# ') && t.replace(/^#\s+/, '') !== derivedTitle) { bodyParagraphs.push(new Paragraph({ text: t.replace(/^#\s+/, ''), heading: HeadingLevel.HEADING_1 })); } else if (/^(\* |- |• )/.test(t)) { bodyParagraphs.push(new Paragraph({ text: t.replace(/^(\* |- |• )/, ''), bullet: { level: 0 } })); } else { const runs = (line.split(/(\*\*.*?\*\*|\*.*?\*)/g).filter(Boolean)).map(part => { if (part.startsWith('**') && part.endsWith('**')) return new TextRun({ text: part.slice(2, -2), bold: true }); if (part.startsWith('*') && part.endsWith('*')) return new TextRun({ text: part.slice(1, -1), italics: true }); return new TextRun(part); }); bodyParagraphs.push(new Paragraph({ children: runs })); } }); const doc = new Document({ sections: [{ headers: { default: new DocxHeader({ children: headerChildren }) }, children: bodyParagraphs }], }); const filename = `report-${new Date().toISOString().replace(/[:.]/g, '-')}.docx`; const blob = await Packer.toBlob(doc); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); // Notify RASA that the research step is complete await sendMessage('/research_complete'); return filename; }; const sendMessage = async (outgoing: string) => { if (!outgoing.trim()) return; setIsLoading(true); try { const requestBody = { sender: senderRef.current || 'web-user', message: outgoing, metadata: { agent: selectedAgent?.id === 'lexa-legal' ? 'lexa' : 'biz' } }; const res = await fetch(`https://devyugensys-rasa-bizinsight.hf.space/webhooks/rest/webhook`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (!res.ok) throw new Error(`Backend error: ${res.status} ${res.statusText}`); const data = await res.json(); prevResponseRef.current = lastResponseRef.current; lastResponseRef.current = data; const botMessages: ChatMessage[] = Array.isArray(data) ? data .map((m: any): ChatMessage | null => { let text = typeof m?.text === 'string' ? m.text : undefined; const buttons = Array.isArray(m?.buttons) ? m.buttons .filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string') .map((b: any) => ({ title: b.title as string, payload: b.payload as string })) : undefined; let custom: ChatMessage['custom'] = undefined; if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') { const opts = Array.isArray(m.custom.options) ? m.custom.options.filter((o: any) => typeof o === 'string') : []; custom = { type: 'multi_select_chips', options: opts, columns: typeof m.custom.columns === 'number' ? m.custom.columns : undefined, hint: typeof m.custom.hint === 'string' ? m.custom.hint : undefined, selections: [], }; } let json_message = m?.json_message; if (!json_message && m?.custom && typeof m.custom === 'object' && typeof m.custom.action === 'string') { json_message = m.custom; } const action = json_message?.action; if (action === 'json:download' || action === 'pdf:download' || action === 'docx:download') { text = undefined; } if (!text && (!buttons || buttons.length === 0) && !custom && !json_message) return null; return { sender: 'bot', text, buttons, custom, json_message } as ChatMessage; }) .filter(Boolean) as ChatMessage[] : []; if (botMessages.length > 0) { const visibleMessages = botMessages.filter( (m) => !!(m.text || (m.buttons && m.buttons.length > 0) || m.custom) ); const combined = [...messages, ...visibleMessages]; setMessages(prev => [...prev, ...visibleMessages]); const pdfAction = botMessages.find(m => (m as any).json_message?.action === 'pdf:download'); const docxAction = botMessages.find(m => (m as any).json_message?.action === 'docx:download'); const jsonAction = botMessages.find(m => (m as any).json_message?.action === 'json:download'); if (pdfAction || docxAction || jsonAction) { // New, more robust logic to find the last substantive report message, // ignoring short confirmation or prompt messages. const findLastReportText = (messageList: ChatMessage[]): string => { // Search backwards from the end of the message list. for (let i = messageList.length - 1; i >= 0; i--) { const msg = messageList[i]; if (msg.sender === 'bot' && msg.text) { // Heuristic: A real report contains markdown headers ('#') or is reasonably long. // This reliably filters out short status updates like "✅ PDF downloaded...". const isLikelyReport = msg.text.includes('Report') || msg.text.length > 200; if (isLikelyReport) { return msg.text; // Found the report content } } } return ''; // No report found }; const targetText = findLastReportText(combined); if (!targetText.trim()) { pushBotNotice('❌ Could not find report content to download.'); } else { if (pdfAction) { try { const filename = await generateReportPdf({ content: targetText }); pushBotNotice(`✅ PDF downloaded: ${filename}`); } catch (e) { console.error('[PDF] Failed to generate:', e); pushBotNotice('❌ Failed to generate PDF file.'); } } if (docxAction) { try { const filename = await generateReportDocx({ content: targetText }); pushBotNotice(`✅ DOCX downloaded: ${filename}`); } catch (e) { console.error('[DOCX] Failed to generate:', e); pushBotNotice('❌ Failed to generate DOCX file.'); } } if (jsonAction) { try { // Create a proper JSON object with the report content const jsonContent = { report: targetText, generatedAt: new Date().toISOString() }; // Call downloadJson with the JSON object and let it handle the rest downloadJson(jsonContent, `report-${new Date().toISOString().slice(0, 10)}.json`); } catch (e) { console.error('[JSON] Failed to generate:', e); pushBotNotice('❌ Failed to generate JSON file.'); } } } } // if (pdfAction || docxAction) { // const botTextIdxs = combined // .map((m, idx) => ({ m, idx })) // .filter(x => x.m.sender === 'bot' && !!x.m.text) // .map(x => x.idx); // const targetIdx = // botTextIdxs.length >= 2 // ? botTextIdxs[botTextIdxs.length - 2] // : botTextIdxs[botTextIdxs.length - 1]; // const targetMsg = targetIdx !== undefined ? combined[targetIdx] : undefined; // const targetText = targetMsg?.text || ''; // if (pdfAction) { // if (!targetText.trim()) { // await startDownload(); // } else { // try { // const filename = await generateReportPdf({ content: targetText }); // pushBotNotice(`✅ PDF downloaded: ${filename}`); // } catch (e) { // console.error('[PDF] Failed to generate:', e); // } // } // } // if (docxAction) { // if (!targetText.trim()) { // pushBotNotice('❌ Could not find report content to download.'); // } else { // try { // // Use the new text-based generator // const filename = await generateReportDocx({ content: targetText }); // pushBotNotice(`✅ DOCX downloaded: ${filename}`); // } catch (e) { // console.error('[DOCX] Failed to generate:', e); // pushBotNotice('❌ Failed to generate DOCX file.'); // } // } // } // } } } catch (error) { console.error('[DEBUG] sendMessage error:', error); setMessages(prev => [...prev, { text: '❌ Something went wrong.', sender: 'bot' }]); } finally { setIsLoading(false); } }; const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!inputValue.trim()) return; const userMessage = inputValue.trim(); setMessages(prev => [...prev, { text: userMessage, sender: 'user' }]); setInputValue(''); await sendMessage(userMessage); }; const handleQuickReply = async (btn: QuickReplyButton) => { const lastMultiIndex = [...messages] .map((m, idx) => ({ m, idx })) .reverse() .find(item => item.m.custom?.type === 'multi_select_chips')?.idx; const lastMulti = lastMultiIndex !== undefined ? messages[lastMultiIndex] : undefined; const selections = lastMulti?.custom?.selections ?? []; if (btn.payload.startsWith('/')) { if (selections.length > 0 && btn.payload.includes('continue')) { const payloadWithSelections = `${btn.payload}${JSON.stringify({ selections })}`; setMessages(prev => [ ...prev, { text: `${btn.title}\n${selections.join(', ')}`, sender: 'user' }, ]); await sendMessage(payloadWithSelections); return; } } setMessages(prev => [...prev, { text: btn.title, sender: 'user' }]); await sendMessage(btn.payload); }; const toggleMultiSelect = (messageIndex: number, option: string) => { setMessages(prev => { const next = [...prev]; const msg = next[messageIndex]; if (!msg?.custom || msg.custom.type !== 'multi_select_chips') return prev; const current = new Set(msg.custom.selections || []); current.has(option) ? current.delete(option) : current.add(option); msg.custom = { ...msg.custom, selections: Array.from(current) }; next[messageIndex] = { ...msg }; return next; }); }; const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); useEffect(() => { scrollToBottom(); }, [messages, isLoading]); // Preload Bizinsight welcome content on first entry useEffect(() => { if (!selectedAgent) return; const isBizInsight = selectedAgent.id === 'rival-lens' || /bizinsight/i.test(selectedAgent.name || ''); if (!isBizInsight) return; if (messages.length > 0) return; const welcomeText = "👋 Welcome to Yugensys BizResearch AI Agent!\n\nYour AI-powered business intelligence researcher. Get comprehensive insights on any company in minutes.\n\nWhat company would you like to research today?"; const buttons = [ { title: 'Hubspot', payload: '/inform_company{"company_name": "Hubspot"}' }, { title: 'Zoho', payload: '/inform_company{"company_name": "Zoho"}' }, { title: 'Salesforce', payload: '/inform_company{"company_name": "Salesforce"}' }, { title: 'Tesla', payload: '/inform_company{"company_name": "Tesla"}' }, ]; const followUpText = 'Or you can Enter company name... 🔍'; setMessages([ { sender: 'bot', text: welcomeText, buttons }, { sender: 'bot', text: followUpText }, ]); }, [selectedAgent]); if (!selectedAgent) return null; // Determine last bot message with text (rendered in ReportRenderer) const lastBotIndex = (() => { for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]; if (m.sender === 'bot' && m.text) return i; } return -1; })(); return ( {/* Chat window */} {/* Header with gradient */} { if (onClose) { onClose(); } else { setSelectedAgent(null); } }} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.2)', color: '#fff', '&:hover': { bgcolor: 'rgba(255,255,255,0.3)', transform: 'translateX(-2px)' }, transition: 'all 0.2s ease', width: 32, height: 32, mr: 1 }} > {selectedAgent?.id === 'lexa-legal' ? 'Lexa' : 'BizInsights'} Assistant {selectedAgent?.id === 'lexa-legal' ? 'Legal Research Assistant' : 'Competitive Intelligence Assistant'} {/* Conversation area */} {messages.map((msg, i) => ( {msg.sender === 'bot' && ( )} {msg.sender === 'bot' && msg.text ? ( <> *:not(:last-child)': { mb: 1.5 }, '& h1, & h2, & h3, & h4, & h5, & h6': { color: 'primary.main', mt: 2, mb: 1.5 }, '& pre': { backgroundColor: 'rgba(0,0,0,0.05)', p: 2, borderRadius: 1, overflowX: 'auto' }, '& table': { borderCollapse: 'collapse', width: '100%', mb: 2, '& th, & td': { border: '1px solid', borderColor: 'divider', p: 1, textAlign: 'left' }, '& th': { backgroundColor: 'action.hover', fontWeight: 'bold' }, }, }}> {msg.buttons && msg.buttons.length > 0 && ( {msg.buttons.map((btn, idx) => ( handleQuickReply(btn)} sx={{ bgcolor: BRAND.chipBg, color: BRAND.chipText, '&:hover': { bgcolor: '#FFD4BB' }, cursor: 'pointer', }} /> ))} )} ) : ( <> {msg.text && ( ') }} /> )} {msg.buttons && msg.buttons.length > 0 && ( {msg.buttons.map((btn, idx) => ( handleQuickReply(btn)} sx={{ bgcolor: BRAND.chipBg, color: BRAND.chipText, '&:hover': { bgcolor: '#FFD4BB' }, cursor: 'pointer', }} /> ))} )} )} {/* Multi-select chips (custom payload) */} {msg.custom?.type === 'multi_select_chips' && ( {msg.custom.hint && ( {msg.custom.hint} )} {(msg.custom.options || []).map((opt) => { const selected = !!msg.custom?.selections?.includes(opt); return ( toggleMultiSelect(i, opt)} variant={selected ? 'filled' : 'outlined'} color={selected ? 'primary' : undefined} sx={{ borderRadius: 999, width: '100%', justifyContent: 'flex-start', bgcolor: selected ? BRAND.chipBg : 'transparent', borderColor: BRAND.inputBorder, color: BRAND.chipText, '& .MuiChip-label': { whiteSpace: 'normal' }, '&:hover': { bgcolor: selected ? '#FFD4BB' : 'transparent' }, }} /> ); })} )} ))}
{/* Composer */} setInputValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(e); } }} multiline maxRows={4} disabled={isLoading} InputProps={{ endAdornment: ( ), sx: { borderRadius: 3, bgcolor: BRAND.inputBg, '& .MuiOutlinedInput-input::placeholder': { color: BRAND.placeholder, opacity: 1, }, '& .MuiOutlinedInput-notchedOutline': { borderColor: BRAND.inputBorder, }, '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: BRAND.inputBorder, }, '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: BRAND.sendBtn, borderWidth: 1.5, }, }, }} /> ); }; export default ChatInterface;