pranav8tripathi@gmail.com commited on
Commit
773a8a3
·
1 Parent(s): 8186377

fix custom fields

Browse files
Files changed (1) hide show
  1. src/components/ChatInterface.tsx +501 -381
src/components/ChatInterface.tsx CHANGED
@@ -1,6 +1,4 @@
1
  import React, { useState, useEffect, useRef } from 'react';
2
- import { Resizable } from 'react-resizable';
3
- import 'react-resizable/css/styles.css';
4
  import { useAgent } from '../contexts/AgentContext';
5
  import ReportRenderer from './ReportRenderer';
6
  import {
@@ -14,6 +12,8 @@ import {
14
  List,
15
  ListItem,
16
  ListItemAvatar,
 
 
17
  Chip,
18
  } from '@mui/material';
19
  import SendIcon from '@mui/icons-material/Send';
@@ -27,20 +27,20 @@ import {
27
  Packer,
28
  Paragraph,
29
  ImageRun,
 
 
30
  HeadingLevel,
 
 
 
 
31
  TextRun,
32
  } from 'docx';
33
-
34
  interface ChatInterfaceProps {
35
  onClose?: () => void;
36
  }
37
 
38
  const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
39
- const [dimensions, setDimensions] = useState({ width: 420, height: 600 });
40
- const onResize = (event: any, { size }: { size: { width: number; height: number } }) => {
41
- setDimensions({ width: size.width, height: size.height });
42
- };
43
-
44
  const { selectedAgent, setSelectedAgent } = useAgent();
45
  type QuickReplyButton = { title: string; payload: string };
46
  type CustomMultiSelect = {
@@ -48,10 +48,10 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
48
  options: string[];
49
  columns?: number;
50
  hint?: string;
51
- selections?: string[];
52
  };
53
 
54
- // Brand tokens (unchanged)
55
  const BRAND = {
56
  gradientFrom: '#FF6B00',
57
  gradientTo: '#FF3D00',
@@ -72,14 +72,16 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
72
  chipText: '#7A3D2B',
73
  };
74
 
75
- const pushBotNotice = (text: string) => setMessages(prev => [...prev, { sender: 'bot', text }]);
 
 
76
 
77
  type ChatMessage = {
78
  sender: 'user' | 'bot';
79
  text?: string;
80
  buttons?: QuickReplyButton[];
81
  custom?: CustomMultiSelect;
82
- json_message?: any;
83
  };
84
 
85
  const [messages, setMessages] = useState<ChatMessage[]>([]);
@@ -89,34 +91,17 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
89
  const chatContainerRef = useRef<HTMLDivElement>(null);
90
  const lastReportRef = useRef<HTMLDivElement | null>(null);
91
  const reportRefs = useRef<Map<number, HTMLDivElement>>(new Map());
 
92
  const senderRef = useRef<string>('');
 
93
  const lastResponseRef = useRef<any>(null);
94
  const prevResponseRef = useRef<any>(null);
95
 
 
96
  const YUGEN_LOGO_SRC = '/yugensys.png';
97
- const BIZ_LOGO_SRC = '/BizInsaights.png';
98
-
99
- // Initialize a stable sender id once
100
- useEffect(() => {
101
- if (!senderRef.current) {
102
- senderRef.current =
103
- (crypto as any)?.randomUUID?.() ? `web-${(crypto as any).randomUUID()}` : `web-${Math.random().toString(36).slice(2, 10)}`;
104
- }
105
- }, []);
106
- // Show a "thinking" message when loading
107
- useEffect(() => {
108
- if (isLoading) {
109
- setMessages(prev => {
110
- const alreadyShown = prev.some(m => m.text === 'BizInsights is thinking...');
111
- if (alreadyShown) return prev;
112
- return [...prev, { sender: 'bot', text: 'BizInsights is thinking...' }];
113
- });
114
- } else {
115
- setMessages(prev => prev.filter(m => m.text !== 'BizInsights is thinking...'));
116
- }
117
- }, [isLoading]);
118
 
119
- // Utils
120
  const downloadJson = (obj: any, filename?: string) => {
121
  try {
122
  const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
@@ -133,6 +118,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
133
  }
134
  };
135
 
 
136
  const fetchAsDataURL = async (src: string): Promise<string> => {
137
  const res = await fetch(src, { credentials: 'include', cache: 'force-cache' });
138
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${src}`);
@@ -144,13 +130,19 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
144
  });
145
  };
146
 
147
- const downscaleDataURL = async (dataUrl: string, maxEdgePx: number, mime: 'image/jpeg' | 'image/png', quality: number): Promise<string> => {
 
 
 
 
 
148
  const img = await new Promise<HTMLImageElement>((resolve, reject) => {
149
  const i = new Image();
150
  i.onload = () => resolve(i);
151
  i.onerror = () => reject(new Error('Image decode failed'));
152
  i.src = dataUrl;
153
  });
 
154
  const naturalW = (img as any).naturalWidth || img.width;
155
  const naturalH = (img as any).naturalHeight || img.height;
156
  const longest = Math.max(naturalW, naturalH);
@@ -163,6 +155,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
163
  canvas.height = h;
164
  const ctx = canvas.getContext('2d');
165
  if (!ctx) throw new Error('Canvas 2D context unavailable');
 
166
  if (mime === 'image/jpeg') {
167
  ctx.fillStyle = '#ffffff';
168
  ctx.fillRect(0, 0, w, h);
@@ -171,78 +164,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
171
  return canvas.toDataURL(mime, quality);
172
  };
173
 
174
- // --------- Rasa helpers (NEW) ----------
175
- // Map Rasa REST payload to our ChatMessage array
176
- const mapRasaToMessages = (data: any[]): ChatMessage[] => {
177
- if (!Array.isArray(data)) return [];
178
- return data
179
- .map((m: any): ChatMessage | null => {
180
- let text = typeof m?.text === 'string' ? m.text : undefined;
181
- const buttons = Array.isArray(m?.buttons)
182
- ? m.buttons
183
- .filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string')
184
- .map((b: any) => ({ title: b.title as string, payload: b.payload as string }))
185
- : undefined;
186
-
187
- // Pass-through known custom blocks
188
- let custom: ChatMessage['custom'] = undefined;
189
- if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') {
190
- const opts = Array.isArray(m.custom.options)
191
- ? m.custom.options.filter((o: any) => typeof o === 'string')
192
- : [];
193
- custom = {
194
- type: 'multi_select_chips',
195
- options: opts,
196
- columns: typeof m.custom.columns === 'number' ? m.custom.columns : undefined,
197
- hint: typeof m.custom.hint === 'string' ? m.custom.hint : undefined,
198
- selections: [],
199
- };
200
- }
201
-
202
- // Control messages
203
- let json_message = m?.json_message;
204
- if (!json_message && m?.custom && typeof m.custom === 'object' && typeof m.custom.action === 'string') {
205
- json_message = m.custom;
206
- }
207
- const action = json_message?.action;
208
- if (action === 'json:download' || action === 'pdf:download' || action === 'docx:download') {
209
- text = undefined; // suppress bubble for control actions
210
- }
211
-
212
- if (!text && (!buttons || buttons.length === 0) && !custom && !json_message) return null;
213
- return { sender: 'bot', text, buttons, custom, json_message };
214
- })
215
- .filter(Boolean) as ChatMessage[];
216
- };
217
-
218
- // // Call Rasa and append messages to UI (used after PDF completes)
219
- // const notifyRasaResearchComplete = async (message = '/research_complete') => {
220
- // try {
221
- // const res = await fetch('https://devyugensys-rasa-bizinsight.hf.space/webhooks/rest/webhook', {
222
- // method: 'POST',
223
- // headers: { 'Content-Type': 'application/json' },
224
- // body: JSON.stringify({ sender: senderRef.current || 'web-user', message }),
225
- // });
226
- // if (!res.ok) {
227
- // console.warn('[RASA] notify failed:', res.status, res.statusText);
228
- // return;
229
- // }
230
- // const data = await res.json();
231
- // const botMessages = mapRasaToMessages(data);
232
-
233
- // if (botMessages.length > 0) {
234
- // // Only append visible content to UI
235
- // const visible = botMessages.filter(
236
- // (m) => !!(m.text || (m.buttons && m.buttons.length > 0) || m.custom)
237
- // );
238
- // setMessages(prev => [...prev, ...visible]);
239
- // }
240
- // } catch (err) {
241
- // console.error('[RASA] notify error:', err);
242
- // }
243
- // };
244
- // // ---------------------------------------
245
-
246
  // ---- PDF: renderer with logos + selectable text ----
247
  const generateReportPdf = async ({
248
  content,
@@ -261,14 +182,17 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
261
  maxLogoEdgePx?: number;
262
  jpegQuality?: number;
263
  }): Promise<string> => {
 
 
264
  const doc = new jsPDF({ unit: 'mm', format: 'a4', compress: true });
265
  const pageWidth = doc.internal.pageSize.getWidth();
266
  const pageHeight = doc.internal.pageSize.getHeight();
267
  const margin = 15;
268
- const logoHeight = 15;
269
- const logoWidth = 40;
270
  const contentWidth = pageWidth - 2 * margin;
271
 
 
272
  const derivedTitle = (() => {
273
  const firstHeader = (content || '')
274
  .split('\n')
@@ -277,6 +201,24 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
277
  return title || (firstHeader ? firstHeader.replace(/^#\s+/, '') : 'Report');
278
  })();
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  // Load + downscale logos
281
  let yugenDataUrl: string | null = null;
282
  let bizDataUrl: string | null = null;
@@ -284,29 +226,39 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
284
  const yugenWithCacheBust = `${yugenLogoSrc}?t=${Date.now()}`;
285
  const yugenRaw = await fetchAsDataURL(yugenWithCacheBust);
286
  yugenDataUrl = await downscaleDataURL(yugenRaw, maxLogoEdgePx, 'image/png', 1.0);
287
- } catch { }
 
 
288
  try {
289
  const bizWithCacheBust = `${bizLogoSrc}?t=${Date.now()}`;
290
  const bizRaw = await fetchAsDataURL(bizWithCacheBust);
291
  bizDataUrl = await downscaleDataURL(bizRaw, maxLogoEdgePx, 'image/png', 1.0);
292
- } catch { }
 
 
293
 
294
  const YUGEN_ALIAS = 'YUGEN_LOGO';
295
  const BIZ_ALIAS = 'BIZ_LOGO';
296
  let embeddedOnce = false;
297
  let cursorY = margin + logoHeight + 24;
298
- const baseLine = 6;
299
 
300
  const addHeader = () => {
301
  const fmt = logoMime === 'image/png' ? 'PNG' : 'JPEG';
302
  if (yugenDataUrl) {
303
- if (!embeddedOnce) doc.addImage(yugenDataUrl, fmt, margin, margin, logoWidth, logoHeight, YUGEN_ALIAS);
304
- else doc.addImage(YUGEN_ALIAS, fmt, margin, margin, logoWidth, logoHeight);
 
 
 
305
  }
306
  if (bizDataUrl) {
307
  const x = pageWidth - margin - logoWidth;
308
- if (!embeddedOnce) doc.addImage(bizDataUrl, fmt, x, margin, logoWidth, logoHeight, BIZ_ALIAS);
309
- else doc.addImage(BIZ_ALIAS, fmt, x, margin, logoWidth, logoHeight);
 
 
 
310
  }
311
  embeddedOnce = true;
312
  doc.setDrawColor(200);
@@ -322,7 +274,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
322
  }
323
  };
324
 
325
- // Header + title
326
  addHeader();
327
  doc.setFont('helvetica', 'bold');
328
  doc.setFontSize(18);
@@ -367,27 +319,32 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
367
 
368
  if (t.startsWith('# ')) {
369
  const txt = t.replace(/^#\s+/, '');
370
- doc.setFont('helvetica', 'bold'); doc.setFontSize(16);
 
371
  const wrapped = doc.splitTextToSize(txt, contentWidth);
372
  ensurePage(baseLine * wrapped.length + 4);
373
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
374
  cursorY += 2;
375
- doc.setFont('helvetica', 'normal'); doc.setFontSize(12);
 
376
  continue;
377
  }
378
  if (t.startsWith('## ')) {
379
  const txt = t.replace(/^##\s+/, '');
380
- doc.setFont('helvetica', 'bold'); doc.setFontSize(14);
 
381
  const wrapped = doc.splitTextToSize(txt, contentWidth);
382
  ensurePage(baseLine * wrapped.length + 2);
383
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
384
  cursorY += 1;
385
- doc.setFont('helvetica', 'normal'); doc.setFontSize(12);
 
386
  continue;
387
  }
388
  if (t.startsWith('### ')) {
389
  const txt = t.replace(/^###\s+/, '');
390
- doc.setFont('helvetica', 'bold'); doc.setFontSize(12);
 
391
  const wrapped = doc.splitTextToSize(txt, contentWidth);
392
  ensurePage(baseLine * wrapped.length + 1);
393
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
@@ -420,15 +377,22 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
420
  if (inCode) flushCode();
421
 
422
  const filename = `report-${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`;
 
 
423
  doc.save(filename);
424
 
425
- // After download, notify Rasa and show its response in UI
426
- // After saving the PDF
427
- await sendMessage('/research_complete'); // or whichever intent you use
 
 
 
 
428
  return filename;
429
  };
430
 
431
- // DOCX (raster fallback, unchanged)
 
432
  const captureNodeToDocx = async (node: HTMLElement, onDone?: (filename: string) => void) => {
433
  if (!node) return;
434
  try {
@@ -445,7 +409,13 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
445
  node.style.width = '1024px';
446
  node.style.boxShadow = 'none';
447
 
448
- const canvas = await html2canvas(node, { scale: 3, useCORS: true, backgroundColor: '#ffffff', windowWidth: node.scrollWidth });
 
 
 
 
 
 
449
  const pageWidthTwips = Math.round(8.27 * 1440);
450
  const pageHeightTwips = Math.round(11.69 * 1440);
451
  const marginTwips = 720;
@@ -470,7 +440,17 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
470
  const tctx = tempCanvas.getContext('2d');
471
  if (!tctx) break;
472
 
473
- tctx.drawImage(canvas, 0, offsetY, canvas.width, sliceHeightPx, 0, 0, tempCanvas.width, tempCanvas.height);
 
 
 
 
 
 
 
 
 
 
474
 
475
  const dataUrl = tempCanvas.toDataURL('image/png');
476
  const res = await fetch(dataUrl);
@@ -489,7 +469,22 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
489
  }
490
 
491
  const doc = new Document({
492
- sections: [{ properties: { page: { size: { width: pageWidthTwips, height: pageHeightTwips }, margin: { top: marginTwips, bottom: marginTwips, left: marginTwips, right: marginTwips } } }, children: sectionChildren }],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  });
494
 
495
  const blob = await Packer.toBlob(doc);
@@ -522,7 +517,10 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
522
  const text = idx >= 0 ? (messages[idx].text || '') : '';
523
  if (!text.trim()) return;
524
  try {
525
- const filename = await generateReportPdf({ content: text, bizLogoSrc: BIZ_LOGO_SRC });
 
 
 
526
  pushBotNotice(`✅ PDF downloaded: ${filename}`);
527
  } catch (e) {
528
  console.error('[PDF] Failed to generate:', e);
@@ -544,7 +542,39 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
544
  prevResponseRef.current = lastResponseRef.current;
545
  lastResponseRef.current = data;
546
 
547
- const botMessages: ChatMessage[] = mapRasaToMessages(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
 
549
  if (botMessages.length > 0) {
550
  const visibleMessages = botMessages.filter(
@@ -590,6 +620,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
590
  }
591
 
592
  if (docxAction) {
 
593
  setTimeout(() => {
594
  const node = reportRefs.current.get(targetIdx as number);
595
  if (node) {
@@ -660,7 +691,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
660
  const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
661
  useEffect(() => { scrollToBottom(); }, [messages, isLoading]);
662
 
663
- // Preload welcome content
664
  useEffect(() => {
665
  if (!selectedAgent) return;
666
  const isBizInsight = selectedAgent.id === 'rival-lens' || /bizinsight/i.test(selectedAgent.name || '');
@@ -684,6 +715,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
684
 
685
  if (!selectedAgent) return null;
686
 
 
687
  const lastBotIndex = (() => {
688
  for (let i = messages.length - 1; i >= 0; i--) {
689
  const m = messages[i];
@@ -693,286 +725,374 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
693
  })();
694
 
695
  return (
696
- <Resizable
697
- width={dimensions.width}
698
- height={dimensions.height}
699
- onResize={onResize}
700
- minConstraints={[320, 400]}
701
- maxConstraints={[800, 1000]}
702
- resizeHandles={['se']}
 
 
 
 
703
  >
704
- <Box
 
 
705
  sx={{
706
- position: 'fixed',
707
- right: { xs: 12, sm: 24 },
708
- bottom: { xs: 12, sm: 24 },
709
- width: `${dimensions.width}px`,
710
- height: `${dimensions.height}px`,
711
- maxWidth: 'calc(100% - 48px)',
712
- maxHeight: 'calc(100vh - 48px)',
713
- zIndex: 1300,
714
- pointerEvents: 'auto',
715
- bgcolor: 'transparent',
716
  display: 'flex',
717
  flexDirection: 'column',
 
 
 
 
718
  }}
719
  >
720
- <Paper
721
- elevation={0}
722
  sx={{
723
- width: '100%',
724
- height: '100%',
 
 
725
  display: 'flex',
726
- flexDirection: 'column',
727
- borderRadius: 10,
728
- overflow: 'hidden',
729
- boxShadow: BRAND.headerShadow,
730
- bgcolor: BRAND.windowBg,
731
  }}
732
  >
733
- <Box
 
 
 
 
 
 
 
 
734
  sx={{
735
- background: BRAND.headerBg,
736
  color: '#fff',
737
- px: 2,
738
- py: 1.5,
739
- display: 'flex',
740
- alignItems: 'center',
741
- gap: 1.25,
 
 
 
742
  }}
743
  >
744
- <IconButton
745
- onClick={() => { onClose ? onClose() : setSelectedAgent(null); }}
746
- size="small"
747
- sx={{
748
- bgcolor: 'rgba(255,255,255,0.2)',
749
- color: '#fff',
750
- '&:hover': { bgcolor: 'rgba(255,255,255,0.3)', transform: 'translateX(-2px)' },
751
- transition: 'all 0.2s ease',
752
- width: 32,
753
- height: 32,
754
- mr: 1
755
- }}
756
- >
757
- <ArrowBackIcon fontSize="small" />
758
- </IconButton>
759
- <Avatar
760
  sx={{
761
- width: 28,
762
- height: 28,
763
- border: '2px solid rgba(255,255,255,0.35)',
764
- bgcolor: 'primary.main',
765
- color: 'white'
 
 
 
 
766
  }}
767
- >
768
- <AutoAwesomeIcon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  sx={{
770
- color: '#FF3D00',
771
- bgcolor: 'white',
772
- borderRadius: '50%',
773
- p: 0.8,
774
- fontSize: 31,
775
- transition: 'all 0.3s ease',
776
- '&:hover': { transform: 'scale(1.1)' },
777
  }}
778
- />
779
- </Avatar>
780
- <Box sx={{ flex: 1, overflow: 'hidden' }}>
781
- <Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.1 }}>
782
- BizInsights Assistant
783
- </Typography>
784
- <Typography variant="caption" sx={{ opacity: 0.9 }}>
785
- Competitive Intelligence Assistant
786
- </Typography>
787
- </Box>
788
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
 
790
- <Box
791
- ref={chatContainerRef}
792
- sx={{
793
- flex: 1,
794
- minHeight: 0,
795
- overflowY: 'auto',
796
- p: 1.5,
797
- overscrollBehavior: 'contain',
798
- bgcolor: BRAND.windowBg,
799
- '&::-webkit-scrollbar': { width: 8 },
800
- '&::-webkit-scrollbar-thumb': { backgroundColor: '#E5E7EB', borderRadius: 8 },
801
- }}
802
- >
803
- <List sx={{ width: '100%', py: 0 }}>
804
- {messages.map((msg, i) => (
805
- <ListItem
806
- key={i}
807
  sx={{
808
- justifyContent: msg.sender === 'user' ? 'flex-end' : 'flex-start',
809
- alignItems: 'flex-start',
810
- px: 0.5,
 
 
 
 
 
 
811
  }}
812
  >
813
- {msg.sender === 'bot' && (
814
- <ListItemAvatar sx={{ minWidth: 38, mt: 0.5 }}>
815
- <Avatar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
  sx={{
817
- width: 28,
818
- height: 28,
819
- border: `2px solid ${BRAND.border}`,
820
- bgcolor: 'primary.main',
821
- color: 'white'
822
  }}
823
  >
824
- <AutoAwesomeIcon
825
- sx={{
826
- color: '#FF3D00',
827
- bgcolor: 'white',
828
- borderRadius: '50%',
829
- p: 0.8,
830
- fontSize: 28,
831
- transition: 'all 0.3s ease',
832
- '&:hover': { transform: 'scale(1.1)' },
833
- }}
834
- />
835
- </Avatar>
836
- </ListItemAvatar>
 
 
 
 
 
 
 
 
 
 
 
837
  )}
 
838
 
839
- <Paper
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
  sx={{
841
- p: 1.25,
842
- px: 1.5,
843
- bgcolor: msg.sender === 'user' ? BRAND.userBubble : BRAND.botBubble,
844
- borderRadius: 3,
845
- boxShadow: '0 4px 14px rgba(0,0,0,0.06)',
846
- maxWidth: '85%',
847
- position: 'relative',
848
- ...(msg.sender === 'user' && { borderTopRightRadius: 4, color: BRAND.userText }),
849
- ...(msg.sender === 'bot' && { borderTopLeftRadius: 4 }),
850
  }}
 
851
  >
852
- {msg.sender === 'bot' && msg.text ? (
853
- <>
854
- <Box sx={{
855
- '& > *:not(:last-child)': { mb: 1.5 },
856
- '& h1, & h2, & h3, & h4, & h5, & h6': { color: 'primary.main', mt: 2, mb: 1.5 },
857
- '& pre': { backgroundColor: 'rgba(0,0,0,0.05)', p: 2, borderRadius: 1, overflowX: 'auto' },
858
- '& table': {
859
- borderCollapse: 'collapse', width: '100%', mb: 2,
860
- '& th, & td': { border: '1px solid', borderColor: 'divider', p: 1, textAlign: 'left' },
861
- '& th': { backgroundColor: 'action.hover', fontWeight: 'bold' },
862
- },
863
- }}>
864
- <ReportRenderer content={msg.text} />
865
- </Box>
866
- {msg.buttons && msg.buttons.length > 0 && (
867
- <Box sx={{ mt: 1.5, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
868
- {msg.buttons.map((btn, idx) => (
869
- <Chip
870
- key={idx}
871
- label={btn.title}
872
- onClick={() => handleQuickReply(btn)}
873
- sx={{
874
- bgcolor: BRAND.chipBg,
875
- color: BRAND.chipText,
876
- '&:hover': { bgcolor: '#FFD4BB' },
877
- cursor: 'pointer',
878
- }}
879
- />
880
- ))}
881
- </Box>
882
- )}
883
- </>
884
- ) : (
885
- <>
886
- {msg.text && (
887
- <Typography
888
- variant="body2"
889
- sx={{ whiteSpace: 'pre-line', lineHeight: 1.5 }}
890
- dangerouslySetInnerHTML={{ __html: msg.text.replace(/\n/g, '<br />') }}
891
- />
892
- )}
893
- {msg.buttons && msg.buttons.length > 0 && (
894
- <Box sx={{ mt: 1.5, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
895
- {msg.buttons.map((btn, idx) => (
896
- <Chip
897
- key={idx}
898
- label={btn.title}
899
- onClick={() => handleQuickReply(btn)}
900
- sx={{
901
- bgcolor: BRAND.chipBg,
902
- color: BRAND.chipText,
903
- '&:hover': { bgcolor: '#FFD4BB' },
904
- cursor: 'pointer',
905
- }}
906
- />
907
- ))}
908
- </Box>
909
- )}
910
- </>
911
- )}
912
- </Paper>
913
- </ListItem>
914
- ))}
915
-
916
- <div ref={messagesEndRef} />
917
- </List>
918
- </Box>
919
-
920
- {/* Composer */}
921
- <Box
922
- component="form"
923
- onSubmit={handleSendMessage}
924
- sx={{ p: 1.25, bgcolor: BRAND.windowBg, borderTop: '1px solid #EDF0F5', flexShrink: 0 }}
925
- >
926
- <TextField
927
- fullWidth
928
- variant="outlined"
929
- placeholder="Type here"
930
- value={inputValue}
931
- onChange={(e) => setInputValue(e.target.value)}
932
- onKeyDown={(e) => {
933
- if (e.key === 'Enter' && !e.shiftKey) {
934
- e.preventDefault();
935
- handleSendMessage(e);
936
- }
937
- }}
938
- multiline
939
- maxRows={4}
940
- disabled={isLoading}
941
- InputProps={{
942
- endAdornment: (
943
- <InputAdornment position="end" sx={{ mr: 0.25 }}>
944
- <IconButton
945
- type="submit"
946
- disabled={!inputValue.trim() || isLoading}
947
- sx={{
948
- bgcolor: BRAND.sendBtn,
949
- color: '#fff',
950
- width: 40,
951
- height: 40,
952
- borderRadius: 2,
953
- boxShadow: '0 6px 18px rgba(250, 140, 106, 0.35)',
954
- '&:hover': { bgcolor: BRAND.sendBtnHover },
955
- }}
956
- edge="end"
957
- >
958
- <SendIcon fontSize="small" />
959
- </IconButton>
960
- </InputAdornment>
961
- ),
962
- sx: {
963
- borderRadius: 3,
964
- bgcolor: BRAND.inputBg,
965
- '& .MuiOutlinedInput-input::placeholder': { color: BRAND.placeholder, opacity: 1 },
966
- '& .MuiOutlinedInput-notchedOutline': { borderColor: BRAND.inputBorder },
967
- '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: BRAND.inputBorder },
968
- '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: BRAND.sendBtn, borderWidth: 1.5 },
969
  },
970
- }}
971
- />
972
- </Box>
973
- </Paper>
974
- </Box>
975
- </Resizable>
 
 
 
 
 
 
 
 
 
 
976
  );
977
  };
978
 
 
1
  import React, { useState, useEffect, useRef } from 'react';
 
 
2
  import { useAgent } from '../contexts/AgentContext';
3
  import ReportRenderer from './ReportRenderer';
4
  import {
 
12
  List,
13
  ListItem,
14
  ListItemAvatar,
15
+ AppBar,
16
+ Toolbar,
17
  Chip,
18
  } from '@mui/material';
19
  import SendIcon from '@mui/icons-material/Send';
 
27
  Packer,
28
  Paragraph,
29
  ImageRun,
30
+ Header as DocxHeader,
31
+ AlignmentType,
32
  HeadingLevel,
33
+ Table as DocxTable,
34
+ TableRow as DocxTableRow,
35
+ TableCell as DocxTableCell,
36
+ WidthType,
37
  TextRun,
38
  } from 'docx';
 
39
  interface ChatInterfaceProps {
40
  onClose?: () => void;
41
  }
42
 
43
  const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
 
 
 
 
 
44
  const { selectedAgent, setSelectedAgent } = useAgent();
45
  type QuickReplyButton = { title: string; payload: string };
46
  type CustomMultiSelect = {
 
48
  options: string[];
49
  columns?: number;
50
  hint?: string;
51
+ selections?: string[]; // maintained client-side
52
  };
53
 
54
+ // Branding palette + tokens inspired by your screenshot
55
  const BRAND = {
56
  gradientFrom: '#FF6B00',
57
  gradientTo: '#FF3D00',
 
72
  chipText: '#7A3D2B',
73
  };
74
 
75
+ // Helper to push a bot notice message after an operation completes
76
+ const pushBotNotice = (text: string) =>
77
+ setMessages(prev => [...prev, { sender: 'bot', text }]);
78
 
79
  type ChatMessage = {
80
  sender: 'user' | 'bot';
81
  text?: string;
82
  buttons?: QuickReplyButton[];
83
  custom?: CustomMultiSelect;
84
+ json_message?: any; // pass-through for backend control messages
85
  };
86
 
87
  const [messages, setMessages] = useState<ChatMessage[]>([]);
 
91
  const chatContainerRef = useRef<HTMLDivElement>(null);
92
  const lastReportRef = useRef<HTMLDivElement | null>(null);
93
  const reportRefs = useRef<Map<number, HTMLDivElement>>(new Map());
94
+ // Unique sender per agent session to avoid reusing old Rasa tracker state
95
  const senderRef = useRef<string>('');
96
+ // Track previous and last REST responses so we can download the prior one on demand
97
  const lastResponseRef = useRef<any>(null);
98
  const prevResponseRef = useRef<any>(null);
99
 
100
+ // Static public paths for logos (put files in /public)
101
  const YUGEN_LOGO_SRC = '/yugensys.png';
102
+ const BIZ_LOGO_SRC = '/BizInsaights.png'; // NOTE: fixed spelling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ // Util: download an object as a .json file
105
  const downloadJson = (obj: any, filename?: string) => {
106
  try {
107
  const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
 
118
  }
119
  };
120
 
121
+ // ---- PDF: helpers (fetch + downscale) ----
122
  const fetchAsDataURL = async (src: string): Promise<string> => {
123
  const res = await fetch(src, { credentials: 'include', cache: 'force-cache' });
124
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${src}`);
 
130
  });
131
  };
132
 
133
+ const downscaleDataURL = async (
134
+ dataUrl: string,
135
+ maxEdgePx: number,
136
+ mime: 'image/jpeg' | 'image/png',
137
+ quality: number
138
+ ): Promise<string> => {
139
  const img = await new Promise<HTMLImageElement>((resolve, reject) => {
140
  const i = new Image();
141
  i.onload = () => resolve(i);
142
  i.onerror = () => reject(new Error('Image decode failed'));
143
  i.src = dataUrl;
144
  });
145
+
146
  const naturalW = (img as any).naturalWidth || img.width;
147
  const naturalH = (img as any).naturalHeight || img.height;
148
  const longest = Math.max(naturalW, naturalH);
 
155
  canvas.height = h;
156
  const ctx = canvas.getContext('2d');
157
  if (!ctx) throw new Error('Canvas 2D context unavailable');
158
+
159
  if (mime === 'image/jpeg') {
160
  ctx.fillStyle = '#ffffff';
161
  ctx.fillRect(0, 0, w, h);
 
164
  return canvas.toDataURL(mime, quality);
165
  };
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  // ---- PDF: renderer with logos + selectable text ----
168
  const generateReportPdf = async ({
169
  content,
 
182
  maxLogoEdgePx?: number;
183
  jpegQuality?: number;
184
  }): Promise<string> => {
185
+ console.info('[PDF] Generating...', { yugenLogoSrc, bizLogoSrc, logoMime });
186
+
187
  const doc = new jsPDF({ unit: 'mm', format: 'a4', compress: true });
188
  const pageWidth = doc.internal.pageSize.getWidth();
189
  const pageHeight = doc.internal.pageSize.getHeight();
190
  const margin = 15;
191
+ const logoHeight = 15; // mm
192
+ const logoWidth = 40; // mm
193
  const contentWidth = pageWidth - 2 * margin;
194
 
195
+ // Title: derive from first markdown header if not provided
196
  const derivedTitle = (() => {
197
  const firstHeader = (content || '')
198
  .split('\n')
 
201
  return title || (firstHeader ? firstHeader.replace(/^#\s+/, '') : 'Report');
202
  })();
203
 
204
+ // Helper: notify RASA after download completes
205
+ const notifyRasaResearchComplete = async () => {
206
+ try {
207
+ console.log("📡 Calling RASA webhook...");
208
+ const res = await fetch("https://devyugensys-rasa-bizinsight.hf.space/webhooks/rest/webhook", {
209
+ method: "POST",
210
+ headers: { "Content-Type": "application/json" },
211
+ body: JSON.stringify({
212
+ sender: "web-user",
213
+ message: "/research complete",
214
+ }),
215
+ });
216
+ console.log("✅ RASA responded:", res.status);
217
+ } catch (err) {
218
+ console.error("❌ RASA call failed:", err);
219
+ }
220
+ };
221
+
222
  // Load + downscale logos
223
  let yugenDataUrl: string | null = null;
224
  let bizDataUrl: string | null = null;
 
226
  const yugenWithCacheBust = `${yugenLogoSrc}?t=${Date.now()}`;
227
  const yugenRaw = await fetchAsDataURL(yugenWithCacheBust);
228
  yugenDataUrl = await downscaleDataURL(yugenRaw, maxLogoEdgePx, 'image/png', 1.0);
229
+ } catch (e) {
230
+ console.warn('[PDF] Yugen logo load failed, will skip:', yugenLogoSrc, e);
231
+ }
232
  try {
233
  const bizWithCacheBust = `${bizLogoSrc}?t=${Date.now()}`;
234
  const bizRaw = await fetchAsDataURL(bizWithCacheBust);
235
  bizDataUrl = await downscaleDataURL(bizRaw, maxLogoEdgePx, 'image/png', 1.0);
236
+ } catch (e) {
237
+ console.warn('[PDF] Biz logo load failed, will skip:', bizLogoSrc, e);
238
+ }
239
 
240
  const YUGEN_ALIAS = 'YUGEN_LOGO';
241
  const BIZ_ALIAS = 'BIZ_LOGO';
242
  let embeddedOnce = false;
243
  let cursorY = margin + logoHeight + 24;
244
+ const baseLine = 6; // mm baseline leading
245
 
246
  const addHeader = () => {
247
  const fmt = logoMime === 'image/png' ? 'PNG' : 'JPEG';
248
  if (yugenDataUrl) {
249
+ if (!embeddedOnce) {
250
+ doc.addImage(yugenDataUrl, fmt, margin, margin, logoWidth, logoHeight, YUGEN_ALIAS);
251
+ } else {
252
+ doc.addImage(YUGEN_ALIAS, fmt, margin, margin, logoWidth, logoHeight);
253
+ }
254
  }
255
  if (bizDataUrl) {
256
  const x = pageWidth - margin - logoWidth;
257
+ if (!embeddedOnce) {
258
+ doc.addImage(bizDataUrl, fmt, x, margin, logoWidth, logoHeight, BIZ_ALIAS);
259
+ } else {
260
+ doc.addImage(BIZ_ALIAS, fmt, x, margin, logoWidth, logoHeight);
261
+ }
262
  }
263
  embeddedOnce = true;
264
  doc.setDrawColor(200);
 
274
  }
275
  };
276
 
277
+ // First page header + title
278
  addHeader();
279
  doc.setFont('helvetica', 'bold');
280
  doc.setFontSize(18);
 
319
 
320
  if (t.startsWith('# ')) {
321
  const txt = t.replace(/^#\s+/, '');
322
+ doc.setFont('helvetica', 'bold');
323
+ doc.setFontSize(16);
324
  const wrapped = doc.splitTextToSize(txt, contentWidth);
325
  ensurePage(baseLine * wrapped.length + 4);
326
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
327
  cursorY += 2;
328
+ doc.setFont('helvetica', 'normal');
329
+ doc.setFontSize(12);
330
  continue;
331
  }
332
  if (t.startsWith('## ')) {
333
  const txt = t.replace(/^##\s+/, '');
334
+ doc.setFont('helvetica', 'bold');
335
+ doc.setFontSize(14);
336
  const wrapped = doc.splitTextToSize(txt, contentWidth);
337
  ensurePage(baseLine * wrapped.length + 2);
338
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
339
  cursorY += 1;
340
+ doc.setFont('helvetica', 'normal');
341
+ doc.setFontSize(12);
342
  continue;
343
  }
344
  if (t.startsWith('### ')) {
345
  const txt = t.replace(/^###\s+/, '');
346
+ doc.setFont('helvetica', 'bold');
347
+ doc.setFontSize(12);
348
  const wrapped = doc.splitTextToSize(txt, contentWidth);
349
  ensurePage(baseLine * wrapped.length + 1);
350
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
 
377
  if (inCode) flushCode();
378
 
379
  const filename = `report-${new Date().toISOString().replace(/[:.]/g, '-')}.pdf`;
380
+
381
+ // 🟢 Trigger file download
382
  doc.save(filename);
383
 
384
+ // 🟢 Log confirmation and notify RASA with short delay
385
+ console.log(`📄 PDF downloaded: ${filename}`);
386
+ setTimeout(() => {
387
+ console.log("🔔 Triggering RASA research complete call...");
388
+ notifyRasaResearchComplete();
389
+ }, 300);
390
+
391
  return filename;
392
  };
393
 
394
+
395
+ // DOCX (raster fallback)
396
  const captureNodeToDocx = async (node: HTMLElement, onDone?: (filename: string) => void) => {
397
  if (!node) return;
398
  try {
 
409
  node.style.width = '1024px';
410
  node.style.boxShadow = 'none';
411
 
412
+ const canvas = await html2canvas(node, {
413
+ scale: 3,
414
+ useCORS: true,
415
+ backgroundColor: '#ffffff',
416
+ windowWidth: node.scrollWidth,
417
+ });
418
+
419
  const pageWidthTwips = Math.round(8.27 * 1440);
420
  const pageHeightTwips = Math.round(11.69 * 1440);
421
  const marginTwips = 720;
 
440
  const tctx = tempCanvas.getContext('2d');
441
  if (!tctx) break;
442
 
443
+ tctx.drawImage(
444
+ canvas,
445
+ 0,
446
+ offsetY,
447
+ canvas.width,
448
+ sliceHeightPx,
449
+ 0,
450
+ 0,
451
+ tempCanvas.width,
452
+ tempCanvas.height
453
+ );
454
 
455
  const dataUrl = tempCanvas.toDataURL('image/png');
456
  const res = await fetch(dataUrl);
 
469
  }
470
 
471
  const doc = new Document({
472
+ sections: [
473
+ {
474
+ properties: {
475
+ page: {
476
+ size: { width: pageWidthTwips, height: pageHeightTwips },
477
+ margin: {
478
+ top: marginTwips,
479
+ bottom: marginTwips,
480
+ left: marginTwips,
481
+ right: marginTwips,
482
+ },
483
+ },
484
+ },
485
+ children: sectionChildren,
486
+ },
487
+ ],
488
  });
489
 
490
  const blob = await Packer.toBlob(doc);
 
517
  const text = idx >= 0 ? (messages[idx].text || '') : '';
518
  if (!text.trim()) return;
519
  try {
520
+ const filename = await generateReportPdf({
521
+ content: text,
522
+ bizLogoSrc: BIZ_LOGO_SRC
523
+ });
524
  pushBotNotice(`✅ PDF downloaded: ${filename}`);
525
  } catch (e) {
526
  console.error('[PDF] Failed to generate:', e);
 
542
  prevResponseRef.current = lastResponseRef.current;
543
  lastResponseRef.current = data;
544
 
545
+ const botMessages: ChatMessage[] = Array.isArray(data)
546
+ ? data
547
+ .map((m: any): ChatMessage | null => {
548
+ let text = typeof m?.text === 'string' ? m.text : undefined;
549
+ const buttons = Array.isArray(m?.buttons)
550
+ ? m.buttons
551
+ .filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string')
552
+ .map((b: any) => ({ title: b.title as string, payload: b.payload as string }))
553
+ : undefined;
554
+ let custom: ChatMessage['custom'] = undefined;
555
+ if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') {
556
+ const opts = Array.isArray(m.custom.options) ? m.custom.options.filter((o: any) => typeof o === 'string') : [];
557
+ custom = {
558
+ type: 'multi_select_chips',
559
+ options: opts,
560
+ columns: typeof m.custom.columns === 'number' ? m.custom.columns : undefined,
561
+ hint: typeof m.custom.hint === 'string' ? m.custom.hint : undefined,
562
+ selections: [],
563
+ };
564
+ }
565
+ let json_message = m?.json_message;
566
+ if (!json_message && m?.custom && typeof m.custom === 'object' && typeof m.custom.action === 'string') {
567
+ json_message = m.custom;
568
+ }
569
+ const action = json_message?.action;
570
+ if (action === 'json:download' || action === 'pdf:download' || action === 'docx:download') {
571
+ text = undefined;
572
+ }
573
+ if (!text && (!buttons || buttons.length === 0) && !custom && !json_message) return null;
574
+ return { sender: 'bot', text, buttons, custom, json_message } as ChatMessage;
575
+ })
576
+ .filter(Boolean) as ChatMessage[]
577
+ : [];
578
 
579
  if (botMessages.length > 0) {
580
  const visibleMessages = botMessages.filter(
 
620
  }
621
 
622
  if (docxAction) {
623
+ // Raster fallback (kept, you can swap to text-based docx if needed)
624
  setTimeout(() => {
625
  const node = reportRefs.current.get(targetIdx as number);
626
  if (node) {
 
691
  const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
692
  useEffect(() => { scrollToBottom(); }, [messages, isLoading]);
693
 
694
+ // Preload Bizinsight welcome content on first entry
695
  useEffect(() => {
696
  if (!selectedAgent) return;
697
  const isBizInsight = selectedAgent.id === 'rival-lens' || /bizinsight/i.test(selectedAgent.name || '');
 
715
 
716
  if (!selectedAgent) return null;
717
 
718
+ // Determine last bot message with text (rendered in ReportRenderer)
719
  const lastBotIndex = (() => {
720
  for (let i = messages.length - 1; i >= 0; i--) {
721
  const m = messages[i];
 
725
  })();
726
 
727
  return (
728
+ <Box
729
+ sx={{
730
+ position: 'fixed',
731
+ right: { xs: 12, sm: 24 },
732
+ bottom: { xs: 12, sm: 24 },
733
+ width: 'clamp(320px, 36vw, 420px)',
734
+ height: 'clamp(520px, 95dvh, 720px)',
735
+ zIndex: 1300,
736
+ pointerEvents: 'auto',
737
+ bgcolor: 'transparent',
738
+ }}
739
  >
740
+ {/* Chat window */}
741
+ <Paper
742
+ elevation={0}
743
  sx={{
744
+ width: '100%',
745
+ height: '100%',
 
 
 
 
 
 
 
 
746
  display: 'flex',
747
  flexDirection: 'column',
748
+ borderRadius: 10,
749
+ overflow: 'hidden',
750
+ boxShadow: BRAND.headerShadow,
751
+ bgcolor: BRAND.windowBg,
752
  }}
753
  >
754
+ {/* Header with gradient */}
755
+ <Box
756
  sx={{
757
+ background: BRAND.headerBg,
758
+ color: '#fff',
759
+ px: 2,
760
+ py: 1.5,
761
  display: 'flex',
762
+ alignItems: 'center',
763
+ gap: 1.25,
 
 
 
764
  }}
765
  >
766
+ <IconButton
767
+ onClick={() => {
768
+ if (onClose) {
769
+ onClose();
770
+ } else {
771
+ setSelectedAgent(null);
772
+ }
773
+ }}
774
+ size="small"
775
  sx={{
776
+ bgcolor: 'rgba(255,255,255,0.2)',
777
  color: '#fff',
778
+ '&:hover': {
779
+ bgcolor: 'rgba(255,255,255,0.3)',
780
+ transform: 'translateX(-2px)'
781
+ },
782
+ transition: 'all 0.2s ease',
783
+ width: 32,
784
+ height: 32,
785
+ mr: 1
786
  }}
787
  >
788
+ <ArrowBackIcon fontSize="small" />
789
+ </IconButton>
790
+ <Avatar
791
+ sx={{
792
+ width: 28,
793
+ height: 28,
794
+ border: '2px solid rgba(255,255,255,0.35)',
795
+ bgcolor: 'primary.main',
796
+ color: 'white'
797
+ }}
798
+ >
799
+ <AutoAwesomeIcon
 
 
 
 
800
  sx={{
801
+ color: '#FF3D00',
802
+ bgcolor: 'white',
803
+ borderRadius: '50%',
804
+ p: 0.8,
805
+ fontSize: 31,
806
+ transition: 'all 0.3s ease',
807
+ '&:hover': {
808
+ transform: 'scale(1.1)',
809
+ },
810
  }}
811
+ />
812
+ </Avatar>
813
+ <Box sx={{ flex: 1, overflow: 'hidden' }}>
814
+ <Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.1 }}>
815
+ BizInsights Assistant
816
+ </Typography>
817
+ <Typography variant="caption" sx={{ opacity: 0.9 }}>
818
+ Competitive Intelligence Assistant
819
+ </Typography>
820
+ </Box>
821
+
822
+ </Box>
823
+
824
+ {/* Conversation area */}
825
+ <Box
826
+ ref={chatContainerRef}
827
+ sx={{
828
+ flex: 1,
829
+ minHeight: 0, // Important for flexbox scrolling
830
+ overflowY: 'auto',
831
+ p: 1.5,
832
+ overscrollBehavior: 'contain',
833
+ bgcolor: BRAND.windowBg,
834
+ '&::-webkit-scrollbar': { width: 8 },
835
+ '&::-webkit-scrollbar-thumb': {
836
+ backgroundColor: '#E5E7EB',
837
+ borderRadius: 8,
838
+ },
839
+ }}
840
+ >
841
+ <List sx={{ width: '100%', py: 0 }}>
842
+ {messages.map((msg, i) => (
843
+ <ListItem
844
+ key={i}
845
  sx={{
846
+ justifyContent: msg.sender === 'user' ? 'flex-end' : 'flex-start',
847
+ alignItems: 'flex-start',
848
+ px: 0.5,
 
 
 
 
849
  }}
850
+ >
851
+ {msg.sender === 'bot' && (
852
+ <ListItemAvatar sx={{ minWidth: 38, mt: 0.5 }}>
853
+ <Avatar
854
+ sx={{
855
+ width: 28,
856
+ height: 28,
857
+ border: `2px solid ${BRAND.border}`,
858
+ bgcolor: 'primary.main',
859
+ color: 'white'
860
+ }}
861
+ >
862
+ <AutoAwesomeIcon
863
+ sx={{
864
+ color: '#FF3D00',
865
+ bgcolor: 'white',
866
+ borderRadius: '50%',
867
+ p: 0.8,
868
+ fontSize: 28,
869
+ transition: 'all 0.3s ease',
870
+ '&:hover': {
871
+ transform: 'scale(1.1)',
872
+ },
873
+ }}
874
+ />
875
+ </Avatar>
876
+ </ListItemAvatar>
877
+ )}
878
 
879
+
880
+ <Paper
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  sx={{
882
+ p: 1.25,
883
+ px: 1.5,
884
+ bgcolor: msg.sender === 'user' ? BRAND.userBubble : BRAND.botBubble,
885
+ borderRadius: 3,
886
+ boxShadow: '0 4px 14px rgba(0,0,0,0.06)',
887
+ maxWidth: '85%',
888
+ position: 'relative',
889
+ ...(msg.sender === 'user' && { borderTopRightRadius: 4, color: BRAND.userText }),
890
+ ...(msg.sender === 'bot' && { borderTopLeftRadius: 4 }),
891
  }}
892
  >
893
+ {msg.sender === 'bot' && msg.text ? (
894
+ <>
895
+ <Box sx={{
896
+ '& > *:not(:last-child)': { mb: 1.5 },
897
+ '& h1, & h2, & h3, & h4, & h5, & h6': {
898
+ color: 'primary.main',
899
+ mt: 2,
900
+ mb: 1.5
901
+ },
902
+ '& pre': {
903
+ backgroundColor: 'rgba(0,0,0,0.05)',
904
+ p: 2,
905
+ borderRadius: 1,
906
+ overflowX: 'auto'
907
+ },
908
+ '& table': {
909
+ borderCollapse: 'collapse',
910
+ width: '100%',
911
+ mb: 2,
912
+ '& th, & td': {
913
+ border: '1px solid',
914
+ borderColor: 'divider',
915
+ p: 1,
916
+ textAlign: 'left'
917
+ },
918
+ '& th': {
919
+ backgroundColor: 'action.hover',
920
+ fontWeight: 'bold'
921
+ },
922
+ },
923
+ }}>
924
+ <ReportRenderer content={msg.text} />
925
+ </Box>
926
+
927
+ {msg.buttons && msg.buttons.length > 0 && (
928
+ <Box sx={{ mt: 1.5, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
929
+ {msg.buttons.map((btn, idx) => (
930
+ <Chip
931
+ key={idx}
932
+ label={btn.title}
933
+ onClick={() => handleQuickReply(btn)}
934
+ sx={{
935
+ bgcolor: BRAND.chipBg,
936
+ color: BRAND.chipText,
937
+ '&:hover': { bgcolor: '#FFD4BB' },
938
+ cursor: 'pointer',
939
+ }}
940
+ />
941
+ ))}
942
+ </Box>
943
+ )}
944
+ </>
945
+ ) : (
946
+ <>
947
+ {msg.text && (
948
+ <Typography
949
+ variant="body2"
950
+ sx={{ whiteSpace: 'pre-line', lineHeight: 1.5 }}
951
+ dangerouslySetInnerHTML={{ __html: msg.text.replace(/\n/g, '<br />') }}
952
+ />
953
+ )}
954
+
955
+ {msg.buttons && msg.buttons.length > 0 && (
956
+ <Box sx={{ mt: 1.5, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
957
+ {msg.buttons.map((btn, idx) => (
958
+ <Chip
959
+ key={idx}
960
+ label={btn.title}
961
+ onClick={() => handleQuickReply(btn)}
962
+ sx={{
963
+ bgcolor: BRAND.chipBg,
964
+ color: BRAND.chipText,
965
+ '&:hover': { bgcolor: '#FFD4BB' },
966
+ cursor: 'pointer',
967
+ }}
968
+ />
969
+ ))}
970
+ </Box>
971
+ )}
972
+ </>
973
+ )}
974
+
975
+ {/* Multi-select chips (custom payload) */}
976
+ {msg.custom?.type === 'multi_select_chips' && (
977
+ <Box sx={{ mt: 1.5 }}>
978
+ {msg.custom.hint && (
979
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
980
+ {msg.custom.hint}
981
+ </Typography>
982
+ )}
983
+ <Box
984
  sx={{
985
+ display: 'grid',
986
+ gridTemplateColumns: `repeat(${msg.custom?.columns || 2}, minmax(0, 1fr))`,
987
+ gap: 0.75,
 
 
988
  }}
989
  >
990
+ {(msg.custom.options || []).map((opt) => {
991
+ const selected = !!msg.custom?.selections?.includes(opt);
992
+ return (
993
+ <Chip
994
+ key={opt}
995
+ label={opt}
996
+ onClick={() => toggleMultiSelect(i, opt)}
997
+ variant={selected ? 'filled' : 'outlined'}
998
+ color={selected ? 'primary' : undefined}
999
+ sx={{
1000
+ borderRadius: 999,
1001
+ width: '100%',
1002
+ justifyContent: 'flex-start',
1003
+ bgcolor: selected ? BRAND.chipBg : 'transparent',
1004
+ borderColor: BRAND.inputBorder,
1005
+ color: BRAND.chipText,
1006
+ '& .MuiChip-label': { whiteSpace: 'normal' },
1007
+ '&:hover': { bgcolor: selected ? '#FFD4BB' : 'transparent' },
1008
+ }}
1009
+ />
1010
+ );
1011
+ })}
1012
+ </Box>
1013
+ </Box>
1014
  )}
1015
+ </Paper>
1016
 
1017
+
1018
+
1019
+ </ListItem>
1020
+ ))}
1021
+
1022
+ <div ref={messagesEndRef} />
1023
+ </List>
1024
+ </Box>
1025
+
1026
+ {/* Composer */}
1027
+ <Box
1028
+ component="form"
1029
+ onSubmit={handleSendMessage}
1030
+ sx={{
1031
+ p: 1.25,
1032
+ bgcolor: BRAND.windowBg,
1033
+ borderTop: '1px solid #EDF0F5',
1034
+ flexShrink: 0,
1035
+ }}
1036
+ >
1037
+ <TextField
1038
+ fullWidth
1039
+ variant="outlined"
1040
+ placeholder="Type here"
1041
+ value={inputValue}
1042
+ onChange={(e) => setInputValue(e.target.value)}
1043
+ onKeyDown={(e) => {
1044
+ if (e.key === 'Enter' && !e.shiftKey) {
1045
+ e.preventDefault();
1046
+ handleSendMessage(e);
1047
+ }
1048
+ }}
1049
+ multiline
1050
+ maxRows={4}
1051
+ disabled={isLoading}
1052
+ InputProps={{
1053
+ endAdornment: (
1054
+ <InputAdornment position="end" sx={{ mr: 0.25 }}>
1055
+ <IconButton
1056
+ type="submit"
1057
+ disabled={!inputValue.trim() || isLoading}
1058
  sx={{
1059
+ bgcolor: BRAND.sendBtn,
1060
+ color: '#fff',
1061
+ width: 40,
1062
+ height: 40,
1063
+ borderRadius: 2,
1064
+ boxShadow: '0 6px 18px rgba(250, 140, 106, 0.35)',
1065
+ '&:hover': { bgcolor: BRAND.sendBtnHover },
 
 
1066
  }}
1067
+ edge="end"
1068
  >
1069
+ <SendIcon fontSize="small" />
1070
+ </IconButton>
1071
+ </InputAdornment>
1072
+ ),
1073
+ sx: {
1074
+ borderRadius: 3,
1075
+ bgcolor: BRAND.inputBg,
1076
+ '& .MuiOutlinedInput-input::placeholder': {
1077
+ color: BRAND.placeholder,
1078
+ opacity: 1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1079
  },
1080
+ '& .MuiOutlinedInput-notchedOutline': {
1081
+ borderColor: BRAND.inputBorder,
1082
+ },
1083
+ '&:hover .MuiOutlinedInput-notchedOutline': {
1084
+ borderColor: BRAND.inputBorder,
1085
+ },
1086
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
1087
+ borderColor: BRAND.sendBtn,
1088
+ borderWidth: 1.5,
1089
+ },
1090
+ },
1091
+ }}
1092
+ />
1093
+ </Box>
1094
+ </Paper>
1095
+ </Box>
1096
  );
1097
  };
1098