devyugensys commited on
Commit
64335f3
·
verified ·
1 Parent(s): 3ebfd43

Update src/components/ChatInterface.tsx

Browse files
Files changed (1) hide show
  1. src/components/ChatInterface.tsx +366 -9
src/components/ChatInterface.tsx CHANGED
@@ -18,6 +18,9 @@ import {
18
  } from '@mui/material';
19
  import SendIcon from '@mui/icons-material/Send';
20
  import ArrowBackIcon from '@mui/icons-material/ArrowBack';
 
 
 
21
 
22
  const ChatInterface: React.FC = () => {
23
  const { selectedAgent, setSelectedAgent } = useAgent();
@@ -29,11 +32,16 @@ const ChatInterface: React.FC = () => {
29
  hint?: string;
30
  selections?: string[]; // maintained client-side
31
  };
 
 
 
 
32
  type ChatMessage = {
33
  sender: 'user' | 'bot';
34
  text?: string;
35
  buttons?: QuickReplyButton[];
36
  custom?: CustomMultiSelect;
 
37
  };
38
 
39
  const [messages, setMessages] = useState<ChatMessage[]>([]);
@@ -41,33 +49,271 @@ const ChatInterface: React.FC = () => {
41
  const [isLoading, setIsLoading] = useState(false);
42
  const messagesEndRef = useRef<HTMLDivElement>(null);
43
  const chatContainerRef = useRef<HTMLDivElement>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  const sendMessage = async (outgoing: string) => {
46
  if (!outgoing.trim()) return;
47
  setIsLoading(true);
48
  try {
49
  console.log('[DEBUG] Sending message to Rasa REST webhook...', outgoing);
50
- const res = await fetch(`https://devyugensys-bizinsight-rasa.hf.space/webhooks/rest/webhook`, {
51
  method: 'POST',
52
  headers: { 'Content-Type': 'application/json' },
53
- body: JSON.stringify({ sender: 'web-user', message: outgoing }),
54
  });
55
 
56
  if (!res.ok) throw new Error(`Backend error: ${res.status} ${res.statusText}`);
57
  const data = await res.json();
 
 
 
58
  console.log('[DEBUG] Rasa response:', data);
59
 
60
  // Map Rasa messages to our ChatMessage structure (support text + buttons + custom multi-select)
61
  const botMessages: ChatMessage[] = Array.isArray(data)
62
  ? data
63
  .map((m: any): ChatMessage | null => {
64
- const text = typeof m?.text === 'string' ? m.text : undefined;
65
  const buttons = Array.isArray(m?.buttons)
66
  ? m.buttons
67
  .filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string')
68
  .map((b: any) => ({ title: b.title as string, payload: b.payload as string }))
69
  : undefined;
70
  let custom: ChatMessage['custom'] = undefined;
 
71
  if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') {
72
  const opts = Array.isArray(m.custom.options) ? m.custom.options.filter((o: any) => typeof o === 'string') : [];
73
  custom = {
@@ -78,14 +324,81 @@ const ChatInterface: React.FC = () => {
78
  selections: [],
79
  };
80
  }
81
- if (!text && (!buttons || buttons.length === 0) && !custom) return null;
82
- return { sender: 'bot', text, buttons, custom } as ChatMessage;
 
 
 
 
 
 
 
 
 
 
83
  })
84
  .filter(Boolean) as ChatMessage[]
85
  : [];
86
 
87
  if (botMessages.length > 0) {
88
- setMessages(prev => [...prev, ...botMessages]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
  } catch (error) {
91
  console.error('[DEBUG] sendMessage error:', error);
@@ -98,10 +411,12 @@ const ChatInterface: React.FC = () => {
98
  const handleSendMessage = async (e: React.FormEvent) => {
99
  e.preventDefault();
100
  if (!inputValue.trim()) return;
101
- const userMessage = inputValue;
 
 
102
  setMessages(prev => [...prev, { text: userMessage, sender: 'user' }]);
103
  setInputValue('');
104
- await sendMessage(userMessage);
105
  };
106
 
107
  const handleQuickReply = async (btn: QuickReplyButton) => {
@@ -151,8 +466,39 @@ const ChatInterface: React.FC = () => {
151
  const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
152
  useEffect(() => { scrollToBottom(); }, [messages, isLoading]);
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  if (!selectedAgent) return null;
155
 
 
 
 
 
 
 
 
 
 
156
  return (
157
  <Box sx={{ display: 'flex', height: '100vh', justifyContent: 'center', alignItems: 'center', bgcolor: 'background.default', p: { xs: 1.5, md: 2 } }}>
158
  <Paper elevation={4} sx={{ width: '100%', maxWidth: 900, height: { xs: '90vh', md: '85vh' }, display: 'flex', flexDirection: 'column', borderRadius: 4, overflow: 'hidden' }}>
@@ -181,7 +527,18 @@ const ChatInterface: React.FC = () => {
181
  boxShadow: 1,
182
  }}>
183
  {msg.text && (
184
- <ReportRenderer content={msg.text} isBotMessage={msg.sender === 'bot'} />
 
 
 
 
 
 
 
 
 
 
 
185
  )}
186
  {msg.custom?.type === 'multi_select_chips' && (
187
  <Box sx={{ mt: msg.text ? 1 : 0 }}>
 
18
  } from '@mui/material';
19
  import SendIcon from '@mui/icons-material/Send';
20
  import ArrowBackIcon from '@mui/icons-material/ArrowBack';
21
+ import html2canvas from 'html2canvas';
22
+ import { jsPDF } from 'jspdf';
23
+ import { Document, Packer, Paragraph, ImageRun } from 'docx';
24
 
25
  const ChatInterface: React.FC = () => {
26
  const { selectedAgent, setSelectedAgent } = useAgent();
 
32
  hint?: string;
33
  selections?: string[]; // maintained client-side
34
  };
35
+
36
+ // Helper to push a bot notice message after an operation completes
37
+ const pushBotNotice = (text: string) =>
38
+ setMessages(prev => [...prev, { sender: 'bot', text }]);
39
  type ChatMessage = {
40
  sender: 'user' | 'bot';
41
  text?: string;
42
  buttons?: QuickReplyButton[];
43
  custom?: CustomMultiSelect;
44
+ json_message?: any; // pass-through for backend control messages
45
  };
46
 
47
  const [messages, setMessages] = useState<ChatMessage[]>([]);
 
49
  const [isLoading, setIsLoading] = useState(false);
50
  const messagesEndRef = useRef<HTMLDivElement>(null);
51
  const chatContainerRef = useRef<HTMLDivElement>(null);
52
+ const lastReportRef = useRef<HTMLDivElement | null>(null);
53
+ const reportRefs = useRef<Map<number, HTMLDivElement>>(new Map());
54
+ // Unique sender per agent session to avoid reusing old Rasa tracker state
55
+ const senderRef = useRef<string>('');
56
+ // Track previous and last REST responses so we can download the prior one on demand
57
+ const lastResponseRef = useRef<any>(null);
58
+ const prevResponseRef = useRef<any>(null);
59
+
60
+ // Utility: download an object as a .json file
61
+ const downloadJson = (obj: any, filename?: string) => {
62
+ try {
63
+ const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
64
+ const url = URL.createObjectURL(blob);
65
+ const link = document.createElement('a');
66
+ link.href = url;
67
+ link.download = filename || `response-${new Date().toISOString().slice(0,19).replace(/[:T]/g, '-')}.json`;
68
+ document.body.appendChild(link);
69
+ link.click();
70
+ link.remove();
71
+ URL.revokeObjectURL(url);
72
+ } catch (e) {
73
+ console.error('[JSON] Failed to download JSON:', e);
74
+ }
75
+ };
76
+
77
+ const captureNodeToPdf = async (
78
+ node: HTMLElement,
79
+ { format = 'pdf', onDone }: { format?: 'pdf'; onDone?: (filename: string) => void }
80
+ ) => {
81
+ try {
82
+ if (!node) return;
83
+
84
+ // Temporarily expand node for high-quality capture
85
+ const prev = {
86
+ width: node.style.width,
87
+ maxWidth: (node.style as any).maxWidth,
88
+ background: node.style.background,
89
+ padding: node.style.padding,
90
+ boxShadow: node.style.boxShadow,
91
+ };
92
+ node.style.background = '#ffffff';
93
+ node.style.padding = '16px';
94
+ (node.style as any).maxWidth = 'unset';
95
+ node.style.width = '1024px'; // target layout width for better readability
96
+ node.style.boxShadow = 'none';
97
+
98
+ // Ensure white background for canvas
99
+ const canvas = await html2canvas(node, {
100
+ scale: 3, // higher scale for sharper text
101
+ useCORS: true,
102
+ backgroundColor: '#ffffff',
103
+ windowWidth: node.scrollWidth,
104
+ });
105
+
106
+ const imgData = canvas.toDataURL('image/png');
107
+ const pdf = new jsPDF('p', 'mm', 'a4');
108
+ const pageWidth = pdf.internal.pageSize.getWidth();
109
+ const pageHeight = pdf.internal.pageSize.getHeight();
110
+
111
+ // Scale to full page width with margins and paginate vertically
112
+ const margin = 10; // mm
113
+ const renderableWidth = pageWidth - margin * 2;
114
+ const ratio = renderableWidth / canvas.width;
115
+ const imgWidth = renderableWidth;
116
+ const imgHeight = canvas.height * ratio;
117
+
118
+ let heightLeft = imgHeight;
119
+ let position = margin; // y position in mm
120
+
121
+ // First page
122
+ pdf.addImage(imgData, 'PNG', margin, position, imgWidth, imgHeight);
123
+ heightLeft -= (pageHeight - margin * 2);
124
+
125
+ // Additional pages
126
+ while (heightLeft > 0) {
127
+ pdf.addPage();
128
+ // shift the image up by the amount already printed
129
+ position = margin - (imgHeight - heightLeft);
130
+ pdf.addImage(imgData, 'PNG', margin, position, imgWidth, imgHeight);
131
+ heightLeft -= (pageHeight - margin * 2);
132
+ }
133
+
134
+ const filename = `report-${new Date().toISOString().slice(0,19).replace(/[:T]/g, '-')}.pdf`;
135
+ pdf.save(filename);
136
+ onDone?.(filename);
137
+ // Restore styles
138
+ node.style.width = prev.width;
139
+ (node.style as any).maxWidth = prev.maxWidth;
140
+ node.style.background = prev.background;
141
+ node.style.padding = prev.padding;
142
+ node.style.boxShadow = prev.boxShadow;
143
+ } catch (err) {
144
+ console.error('[PDF] Failed to create PDF:', err);
145
+ }
146
+ };
147
+
148
+ const startDownload = async ({ format = 'pdf' }: { format?: 'pdf' }) => {
149
+ // Default to lastReportRef for backward compatibility
150
+ const node = lastReportRef.current;
151
+ if (!node) {
152
+ console.warn('[PDF] No report node found to capture');
153
+ return;
154
+ }
155
+ await captureNodeToPdf(node, {
156
+ format,
157
+ onDone: (fn) => {
158
+ // Show success bubble, do not send anything to backend
159
+ pushBotNotice(`✅ PDF downloaded: ${fn}`);
160
+ },
161
+ });
162
+ };
163
+
164
+ const captureNodeToDocx = async (node: HTMLElement, onDone?: (filename: string) => void) => {
165
+ if (!node) return;
166
+ try {
167
+ // Expand node for clean capture (reuse same approach)
168
+ const prev = {
169
+ width: node.style.width,
170
+ maxWidth: (node.style as any).maxWidth,
171
+ background: node.style.background,
172
+ padding: node.style.padding,
173
+ boxShadow: node.style.boxShadow,
174
+ };
175
+ node.style.background = '#ffffff';
176
+ node.style.padding = '16px';
177
+ (node.style as any).maxWidth = 'unset';
178
+ node.style.width = '1024px';
179
+ node.style.boxShadow = 'none';
180
+
181
+ const canvas = await html2canvas(node, {
182
+ scale: 3,
183
+ useCORS: true,
184
+ backgroundColor: '#ffffff',
185
+ windowWidth: node.scrollWidth,
186
+ });
187
+
188
+ // DOCX page setup (A4) in twips
189
+ const pageWidthTwips = Math.round(8.27 * 1440);
190
+ const pageHeightTwips = Math.round(11.69 * 1440);
191
+ const marginTwips = 720; // 0.5 inch margins
192
+ const contentWidthTwips = pageWidthTwips - marginTwips * 2;
193
+ const contentHeightTwips = pageHeightTwips - marginTwips * 2;
194
+
195
+ const contentWidthInches = contentWidthTwips / 1440;
196
+ const contentHeightInches = contentHeightTwips / 1440;
197
+ const dpi = 96; // CSS px per inch
198
+ const targetWidthPx = Math.floor(contentWidthInches * dpi);
199
+ const pageSliceHeightPx = Math.floor(contentHeightInches * dpi);
200
+
201
+ // Accumulate children first; construct Document after loop to avoid empty docs
202
+ const sectionChildren: Paragraph[] = [];
203
+
204
+ // Slice the tall canvas into page-height chunks and add each as an image
205
+ const totalHeightPx = canvas.height;
206
+ let offsetY = 0;
207
+
208
+ while (offsetY < totalHeightPx) {
209
+ const sliceHeightPx = Math.min(pageSliceHeightPx, totalHeightPx - offsetY);
210
+ const tempCanvas = document.createElement('canvas');
211
+ tempCanvas.width = canvas.width;
212
+ tempCanvas.height = sliceHeightPx;
213
+ const tctx = tempCanvas.getContext('2d');
214
+ if (!tctx) break;
215
+ // Draw the corresponding slice
216
+ tctx.drawImage(
217
+ canvas,
218
+ 0,
219
+ offsetY,
220
+ canvas.width,
221
+ sliceHeightPx,
222
+ 0,
223
+ 0,
224
+ tempCanvas.width,
225
+ tempCanvas.height
226
+ );
227
+
228
+ // Create image buffer from dataURL
229
+ const dataUrl = tempCanvas.toDataURL('image/png');
230
+ const res = await fetch(dataUrl);
231
+ const blob = await res.blob();
232
+ const arrayBuffer = await blob.arrayBuffer();
233
+
234
+ const ratio = targetWidthPx / canvas.width;
235
+ const targetHeightPx = Math.round(sliceHeightPx * ratio);
236
+ // Create an ImageRun directly (Media.addImage is not available in current docx version)
237
+ // Cast to any to satisfy docx@9.5.1 union typings (raster image with PNG data)
238
+ const imageRun: ImageRun = new ImageRun({
239
+ data: new Uint8Array(arrayBuffer),
240
+ transformation: { width: targetWidthPx, height: targetHeightPx },
241
+ } as any);
242
+ sectionChildren.push(new Paragraph({ children: [imageRun] }));
243
+ offsetY += sliceHeightPx;
244
+ }
245
+
246
+ // Create the document now with populated children
247
+ const doc = new Document({
248
+ sections: [
249
+ {
250
+ properties: {
251
+ page: {
252
+ size: { width: pageWidthTwips, height: pageHeightTwips },
253
+ margin: {
254
+ top: marginTwips,
255
+ bottom: marginTwips,
256
+ left: marginTwips,
257
+ right: marginTwips,
258
+ },
259
+ },
260
+ },
261
+ children: sectionChildren,
262
+ },
263
+ ],
264
+ });
265
+
266
+ const blob = await Packer.toBlob(doc);
267
+ const filename = `report-${new Date().toISOString().slice(0,19).replace(/[:T]/g, '-')}.docx`;
268
+ const link = document.createElement('a');
269
+ link.href = URL.createObjectURL(blob);
270
+ link.download = filename;
271
+ document.body.appendChild(link);
272
+ link.click();
273
+ link.remove();
274
+ onDone?.(filename);
275
+
276
+ // Restore styles
277
+ node.style.width = prev.width;
278
+ (node.style as any).maxWidth = prev.maxWidth;
279
+ node.style.background = prev.background;
280
+ node.style.padding = prev.padding;
281
+ node.style.boxShadow = prev.boxShadow;
282
+ } catch (e) {
283
+ console.error('[DOCX] Failed to create DOCX:', e);
284
+ }
285
+ };
286
 
287
  const sendMessage = async (outgoing: string) => {
288
  if (!outgoing.trim()) return;
289
  setIsLoading(true);
290
  try {
291
  console.log('[DEBUG] Sending message to Rasa REST webhook...', outgoing);
292
+ const res = await fetch(`http://localhost:5005/webhooks/rest/webhook`, {
293
  method: 'POST',
294
  headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({ sender: senderRef.current || 'web-user', message: outgoing }),
296
  });
297
 
298
  if (!res.ok) throw new Error(`Backend error: ${res.status} ${res.statusText}`);
299
  const data = await res.json();
300
+ // Rotate response history: keep previous response for json:download
301
+ prevResponseRef.current = lastResponseRef.current;
302
+ lastResponseRef.current = data;
303
  console.log('[DEBUG] Rasa response:', data);
304
 
305
  // Map Rasa messages to our ChatMessage structure (support text + buttons + custom multi-select)
306
  const botMessages: ChatMessage[] = Array.isArray(data)
307
  ? data
308
  .map((m: any): ChatMessage | null => {
309
+ let text = typeof m?.text === 'string' ? m.text : undefined;
310
  const buttons = Array.isArray(m?.buttons)
311
  ? m.buttons
312
  .filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string')
313
  .map((b: any) => ({ title: b.title as string, payload: b.payload as string }))
314
  : undefined;
315
  let custom: ChatMessage['custom'] = undefined;
316
+ // Preserve existing multi-select UI
317
  if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') {
318
  const opts = Array.isArray(m.custom.options) ? m.custom.options.filter((o: any) => typeof o === 'string') : [];
319
  custom = {
 
324
  selections: [],
325
  };
326
  }
327
+ // Map backend control actions that arrive in `custom`
328
+ let json_message = m?.json_message;
329
+ if (!json_message && m?.custom && typeof m.custom === 'object' && typeof m.custom.action === 'string') {
330
+ json_message = m.custom;
331
+ }
332
+ // If this is a control download action, suppress any text bubble
333
+ const action = json_message?.action;
334
+ if (action === 'json:download' || action === 'pdf:download' || action === 'docx:download') {
335
+ text = undefined;
336
+ }
337
+ if (!text && (!buttons || buttons.length === 0) && !custom && !json_message) return null;
338
+ return { sender: 'bot', text, buttons, custom, json_message } as ChatMessage;
339
  })
340
  .filter(Boolean) as ChatMessage[]
341
  : [];
342
 
343
  if (botMessages.length > 0) {
344
+ // Only show messages that have visible content
345
+ const visibleMessages = botMessages.filter(
346
+ (m) => !!(m.text || (m.buttons && m.buttons.length > 0) || m.custom)
347
+ );
348
+ // Build combined list using only visible ones to keep DOM indices aligned
349
+ const combined = [...messages, ...visibleMessages];
350
+ setMessages(prev => [...prev, ...visibleMessages]);
351
+
352
+ // Check for control actions like pdf:download / docx:download
353
+ const pdfAction = botMessages.find(m => (m as any).json_message?.action === 'pdf:download');
354
+ const docxAction = botMessages.find(m => (m as any).json_message?.action === 'docx:download');
355
+ const jsonAction = botMessages.find(m => (m as any).json_message?.action === 'json:download');
356
+ if (jsonAction) {
357
+ const toDownload = prevResponseRef.current ?? lastResponseRef.current ?? data;
358
+ console.log('[JSON] Detected json:download action. Downloading PRIOR REST response.', {
359
+ hasPrev: !!prevResponseRef.current,
360
+ });
361
+ downloadJson(toDownload);
362
+ pushBotNotice('✅ JSON downloaded.');
363
+ }
364
+ if (pdfAction || docxAction) {
365
+ // Determine the last-to-last bot text message index in the combined list
366
+ const botTextIdxs = combined
367
+ .map((m, idx) => ({ m, idx }))
368
+ .filter(x => x.m.sender === 'bot' && !!x.m.text)
369
+ .map(x => x.idx);
370
+ const targetIdx = botTextIdxs.length >= 2 ? botTextIdxs[botTextIdxs.length - 2] : botTextIdxs[botTextIdxs.length - 1];
371
+
372
+ // Wait for DOM to update, then capture the targeted node
373
+ setTimeout(() => {
374
+ if (pdfAction) console.log('[PDF] Detected pdf:download action. Target index:', targetIdx);
375
+ if (docxAction) console.log('[DOCX] Detected docx:download action. Target index:', targetIdx);
376
+ const node = reportRefs.current.get(targetIdx);
377
+ if (node) {
378
+ if (pdfAction) {
379
+ const fmt = (pdfAction as any).json_message?.format || 'pdf';
380
+ captureNodeToPdf(node, {
381
+ format: fmt,
382
+ onDone: (fn) => {
383
+ // Show success bubble only
384
+ pushBotNotice(`✅ PDF downloaded: ${fn}`);
385
+ },
386
+ });
387
+ } else if (docxAction) {
388
+ captureNodeToDocx(node, (fn) => pushBotNotice(`✅ DOCX downloaded: ${fn}`));
389
+ }
390
+ } else {
391
+ console.warn('[EXPORT] Target report node not found; falling back to lastReportRef');
392
+ if (pdfAction) {
393
+ const fmt = (pdfAction as any).json_message?.format || 'pdf';
394
+ startDownload({ format: fmt });
395
+ } else if (docxAction) {
396
+ const fallback = lastReportRef.current;
397
+ if (fallback) captureNodeToDocx(fallback, (fn) => pushBotNotice(`✅ DOCX downloaded: ${fn}`));
398
+ }
399
+ }
400
+ }, 500);
401
+ }
402
  }
403
  } catch (error) {
404
  console.error('[DEBUG] sendMessage error:', error);
 
411
  const handleSendMessage = async (e: React.FormEvent) => {
412
  e.preventDefault();
413
  if (!inputValue.trim()) return;
414
+ const userMessage = inputValue.trim();
415
+ // Let Rasa NLU classify free text. Do not auto-convert to /inform_company.
416
+ const payload = userMessage;
417
  setMessages(prev => [...prev, { text: userMessage, sender: 'user' }]);
418
  setInputValue('');
419
+ await sendMessage(payload);
420
  };
421
 
422
  const handleQuickReply = async (btn: QuickReplyButton) => {
 
466
  const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
467
  useEffect(() => { scrollToBottom(); }, [messages, isLoading]);
468
 
469
+ // Preload Bizinsight welcome content on first entry
470
+ useEffect(() => {
471
+ if (!selectedAgent) return;
472
+ const isBizInsight = selectedAgent.id === 'rival-lens' || /bizinsight/i.test(selectedAgent.name || '');
473
+ if (!isBizInsight) return;
474
+ if (messages.length > 0) return; // avoid duplicating on re-entry
475
+
476
+ 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?";
477
+ const buttons = [
478
+ { title: 'Hubspot', payload: '/inform_company{"company_name": "Hubspot"}' },
479
+ { title: 'Zoho', payload: '/inform_company{"company_name": "Zoho"}' },
480
+ { title: 'Salesforce', payload: '/inform_company{"company_name": "Salesforce"}' },
481
+ { title: 'Tesla', payload: '/inform_company{"company_name": "Tesla"}' },
482
+ ];
483
+ const followUpText = 'Or you can Enter company name... 🔍';
484
+
485
+ setMessages([
486
+ { sender: 'bot', text: welcomeText, buttons },
487
+ { sender: 'bot', text: followUpText },
488
+ ]);
489
+ }, [selectedAgent]);
490
+
491
  if (!selectedAgent) return null;
492
 
493
+ // Determine last bot message with text (rendered in ReportRenderer)
494
+ const lastBotIndex = (() => {
495
+ for (let i = messages.length - 1; i >= 0; i--) {
496
+ const m = messages[i];
497
+ if (m.sender === 'bot' && m.text) return i;
498
+ }
499
+ return -1;
500
+ })();
501
+
502
  return (
503
  <Box sx={{ display: 'flex', height: '100vh', justifyContent: 'center', alignItems: 'center', bgcolor: 'background.default', p: { xs: 1.5, md: 2 } }}>
504
  <Paper elevation={4} sx={{ width: '100%', maxWidth: 900, height: { xs: '90vh', md: '85vh' }, display: 'flex', flexDirection: 'column', borderRadius: 4, overflow: 'hidden' }}>
 
527
  boxShadow: 1,
528
  }}>
529
  {msg.text && (
530
+ <div
531
+ ref={(el) => {
532
+ if (el) {
533
+ reportRefs.current.set(i, el);
534
+ if (i === lastBotIndex) lastReportRef.current = el;
535
+ } else {
536
+ reportRefs.current.delete(i);
537
+ }
538
+ }}
539
+ >
540
+ <ReportRenderer content={msg.text} isBotMessage={msg.sender === 'bot'} />
541
+ </div>
542
  )}
543
  {msg.custom?.type === 'multi_select_chips' && (
544
  <Box sx={{ mt: msg.text ? 1 : 0 }}>