pranav8tripathi@gmail.com commited on
Commit
09ffe59
·
1 Parent(s): 9b124fd

fixed changes

Browse files
package-lock.json CHANGED
@@ -21,12 +21,14 @@
21
  "@types/node": "^16.18.38",
22
  "@types/react": "^18.2.6",
23
  "@types/react-dom": "^18.2.4",
 
24
  "axios": "^1.12.2",
25
  "docx": "^9.5.1",
26
  "jspdf": "^3.0.3",
27
  "jspdf-autotable": "^5.0.2",
28
  "react": "^18.2.0",
29
  "react-dom": "^18.2.0",
 
30
  "react-scripts": "5.0.1",
31
  "typescript": "^4.9.5",
32
  "web-vitals": "^2.1.4"
@@ -3204,15 +3206,6 @@
3204
  }
3205
  }
3206
  },
3207
- "node_modules/@mui/material/node_modules/clsx": {
3208
- "version": "2.1.1",
3209
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
3210
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
3211
- "license": "MIT",
3212
- "engines": {
3213
- "node": ">=6"
3214
- }
3215
- },
3216
  "node_modules/@mui/material/node_modules/react-is": {
3217
  "version": "19.1.1",
3218
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
@@ -3319,15 +3312,6 @@
3319
  }
3320
  }
3321
  },
3322
- "node_modules/@mui/system/node_modules/clsx": {
3323
- "version": "2.1.1",
3324
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
3325
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
3326
- "license": "MIT",
3327
- "engines": {
3328
- "node": ">=6"
3329
- }
3330
- },
3331
  "node_modules/@mui/types": {
3332
  "version": "7.2.24",
3333
  "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
@@ -3372,15 +3356,6 @@
3372
  }
3373
  }
3374
  },
3375
- "node_modules/@mui/utils/node_modules/clsx": {
3376
- "version": "2.1.1",
3377
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
3378
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
3379
- "license": "MIT",
3380
- "engines": {
3381
- "node": ">=6"
3382
- }
3383
- },
3384
  "node_modules/@mui/utils/node_modules/react-is": {
3385
  "version": "19.1.1",
3386
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
@@ -4330,6 +4305,15 @@
4330
  "@types/react": "^18.0.0"
4331
  }
4332
  },
 
 
 
 
 
 
 
 
 
4333
  "node_modules/@types/react-transition-group": {
4334
  "version": "4.4.12",
4335
  "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
@@ -6243,6 +6227,15 @@
6243
  "wrap-ansi": "^7.0.0"
6244
  }
6245
  },
 
 
 
 
 
 
 
 
 
6246
  "node_modules/co": {
6247
  "version": "4.6.0",
6248
  "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -14862,6 +14855,33 @@
14862
  "node": ">=0.10.0"
14863
  }
14864
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14865
  "node_modules/react-scripts": {
14866
  "version": "5.0.1",
14867
  "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
 
21
  "@types/node": "^16.18.38",
22
  "@types/react": "^18.2.6",
23
  "@types/react-dom": "^18.2.4",
24
+ "@types/react-resizable": "^3.0.8",
25
  "axios": "^1.12.2",
26
  "docx": "^9.5.1",
27
  "jspdf": "^3.0.3",
28
  "jspdf-autotable": "^5.0.2",
29
  "react": "^18.2.0",
30
  "react-dom": "^18.2.0",
31
+ "react-resizable": "^3.0.5",
32
  "react-scripts": "5.0.1",
33
  "typescript": "^4.9.5",
34
  "web-vitals": "^2.1.4"
 
3206
  }
3207
  }
3208
  },
 
 
 
 
 
 
 
 
 
3209
  "node_modules/@mui/material/node_modules/react-is": {
3210
  "version": "19.1.1",
3211
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
 
3312
  }
3313
  }
3314
  },
 
 
 
 
 
 
 
 
 
3315
  "node_modules/@mui/types": {
3316
  "version": "7.2.24",
3317
  "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
 
3356
  }
3357
  }
3358
  },
 
 
 
 
 
 
 
 
 
3359
  "node_modules/@mui/utils/node_modules/react-is": {
3360
  "version": "19.1.1",
3361
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
 
4305
  "@types/react": "^18.0.0"
4306
  }
4307
  },
4308
+ "node_modules/@types/react-resizable": {
4309
+ "version": "3.0.8",
4310
+ "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.8.tgz",
4311
+ "integrity": "sha512-Pcvt2eGA7KNXldt1hkhVhAgZ8hK41m0mp89mFgQi7LAAEZiaLgm4fHJ5zbJZ/4m2LVaAyYrrRRv1LHDcrGQanA==",
4312
+ "license": "MIT",
4313
+ "dependencies": {
4314
+ "@types/react": "*"
4315
+ }
4316
+ },
4317
  "node_modules/@types/react-transition-group": {
4318
  "version": "4.4.12",
4319
  "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
 
6227
  "wrap-ansi": "^7.0.0"
6228
  }
6229
  },
6230
+ "node_modules/clsx": {
6231
+ "version": "2.1.1",
6232
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
6233
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
6234
+ "license": "MIT",
6235
+ "engines": {
6236
+ "node": ">=6"
6237
+ }
6238
+ },
6239
  "node_modules/co": {
6240
  "version": "4.6.0",
6241
  "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
 
14855
  "node": ">=0.10.0"
14856
  }
14857
  },
14858
+ "node_modules/react-resizable": {
14859
+ "version": "3.0.5",
14860
+ "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz",
14861
+ "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==",
14862
+ "license": "MIT",
14863
+ "dependencies": {
14864
+ "prop-types": "15.x",
14865
+ "react-draggable": "^4.0.3"
14866
+ },
14867
+ "peerDependencies": {
14868
+ "react": ">= 16.3"
14869
+ }
14870
+ },
14871
+ "node_modules/react-resizable/node_modules/react-draggable": {
14872
+ "version": "4.5.0",
14873
+ "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
14874
+ "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
14875
+ "license": "MIT",
14876
+ "dependencies": {
14877
+ "clsx": "^2.1.1",
14878
+ "prop-types": "^15.8.1"
14879
+ },
14880
+ "peerDependencies": {
14881
+ "react": ">= 16.3.0",
14882
+ "react-dom": ">= 16.3.0"
14883
+ }
14884
+ },
14885
  "node_modules/react-scripts": {
14886
  "version": "5.0.1",
14887
  "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
package.json CHANGED
@@ -16,12 +16,14 @@
16
  "@types/node": "^16.18.38",
17
  "@types/react": "^18.2.6",
18
  "@types/react-dom": "^18.2.4",
 
19
  "axios": "^1.12.2",
20
  "docx": "^9.5.1",
21
  "jspdf": "^3.0.3",
22
  "jspdf-autotable": "^5.0.2",
23
  "react": "^18.2.0",
24
  "react-dom": "^18.2.0",
 
25
  "react-scripts": "5.0.1",
26
  "typescript": "^4.9.5",
27
  "web-vitals": "^2.1.4"
 
16
  "@types/node": "^16.18.38",
17
  "@types/react": "^18.2.6",
18
  "@types/react-dom": "^18.2.4",
19
+ "@types/react-resizable": "^3.0.8",
20
  "axios": "^1.12.2",
21
  "docx": "^9.5.1",
22
  "jspdf": "^3.0.3",
23
  "jspdf-autotable": "^5.0.2",
24
  "react": "^18.2.0",
25
  "react-dom": "^18.2.0",
26
+ "react-resizable": "^3.0.5",
27
  "react-scripts": "5.0.1",
28
  "typescript": "^4.9.5",
29
  "web-vitals": "^2.1.4"
src/components/ChatInterface.tsx CHANGED
@@ -1,4 +1,6 @@
1
  import React, { useState, useEffect, useRef } from 'react';
 
 
2
  import { useAgent } from '../contexts/AgentContext';
3
  import ReportRenderer from './ReportRenderer';
4
  import {
@@ -12,8 +14,6 @@ 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,20 +27,20 @@ import {
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,10 +48,10 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,16 +72,14 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,17 +89,34 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,7 +133,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,19 +144,13 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,7 +163,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,6 +171,78 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
164
  return canvas.toDataURL(mime, quality);
165
  };
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  // ---- PDF: renderer with logos + selectable text ----
168
  const generateReportPdf = async ({
169
  content,
@@ -182,17 +261,14 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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')
@@ -200,25 +276,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
200
  .find(s => s.startsWith('# '));
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,46 +284,36 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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);
265
  doc.setLineWidth(0.3);
266
  doc.line(margin, margin + logoHeight + 4, pageWidth - margin, margin + logoHeight + 4);
267
  };
268
-
269
  const ensurePage = (nextBlockHeight = 0) => {
270
  if (cursorY + nextBlockHeight > pageHeight - margin) {
271
  doc.addPage();
@@ -273,20 +321,20 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
273
  cursorY = margin + logoHeight + 10;
274
  }
275
  };
276
-
277
- // First page header + title
278
  addHeader();
279
  doc.setFont('helvetica', 'bold');
280
  doc.setFontSize(18);
281
  doc.text(derivedTitle, pageWidth / 2, margin + logoHeight + 14, { align: 'center' });
282
  doc.setFont('helvetica', 'normal');
283
  doc.setFontSize(12);
284
-
285
  // Light markdown handling
286
  const lines = (content || '').split('\n');
287
  let inCode = false;
288
  const codeLines: string[] = [];
289
-
290
  const flushCode = () => {
291
  if (!inCode || codeLines.length === 0) return;
292
  doc.setFont('courier', 'normal');
@@ -304,11 +352,11 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
304
  codeLines.length = 0;
305
  cursorY += 1.5;
306
  };
307
-
308
  for (let raw of lines) {
309
  const line = raw ?? '';
310
  const t = line.trim();
311
-
312
  if (t.startsWith('```')) {
313
  if (inCode) flushCode();
314
  else { inCode = true; codeLines.length = 0; }
@@ -316,42 +364,37 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
316
  }
317
  if (inCode) { codeLines.push(line); continue; }
318
  if (!t) { cursorY += baseLine; continue; }
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; }
351
  doc.setFont('helvetica', 'normal');
352
  continue;
353
  }
354
-
355
  if (/^(\* |- |• )/.test(t)) {
356
  const bulletText = t.replace(/^(\* |- |• )/, '');
357
  const bulletIndent = 6;
@@ -369,30 +412,23 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
369
  }
370
  continue;
371
  }
372
-
373
  const wrapped = doc.splitTextToSize(line, contentWidth);
374
  ensurePage(baseLine * wrapped.length);
375
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
376
  }
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,13 +445,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,17 +470,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,22 +489,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,10 +522,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,39 +544,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,7 +590,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,7 +660,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,7 +684,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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,343 +693,286 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
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' && {
890
- borderTopRightRadius: 4,
891
- bgcolor: BRAND.userBubble,
892
- color: BRAND.userText,
893
- }),
894
- ...(msg.sender === 'bot' && {
895
- borderTopLeftRadius: 4,
896
- bgcolor: BRAND.botBubble,
897
- }),
898
- }}
899
- >
900
- {msg.sender === 'bot' && msg.text ? (
901
- <>
902
- <Box sx={{
903
- '& > *:not(:last-child)': { mb: 1.5 },
904
- '& h1, & h2, & h3, & h4, & h5, & h6': {
905
- color: 'primary.main',
906
- mt: 2,
907
- mb: 1.5,
908
- },
909
- '& pre': {
910
- backgroundColor: 'rgba(0,0,0,0.05)',
911
- p: 2,
912
- borderRadius: 1,
913
- overflowX: 'auto',
914
- },
915
- '& table': {
916
- borderCollapse: 'collapse',
917
- width: '100%',
918
- mb: 2,
919
- '& th, & td': {
920
- border: '1px solid',
921
- borderColor: 'divider',
922
- p: 1,
923
- textAlign: 'left',
924
- },
925
- '& th': {
926
- backgroundColor: 'action.hover',
927
- fontWeight: 'bold',
928
- },
929
- },
930
- }}>
931
- <ReportRenderer content={msg.text} />
932
- </Box>
933
- {msg.buttons && msg.buttons.length > 0 && (
934
- <Box sx={{ mt: 1.5, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
935
- {msg.buttons.map((btn, idx) => (
936
- <Chip
937
- key={idx}
938
- label={btn.title}
939
- onClick={() => handleQuickReply(btn)}
940
- sx={{
941
- bgcolor: BRAND.chipBg,
942
- color: BRAND.chipText,
943
- '&:hover': { bgcolor: '#FFD4BB' },
944
- cursor: 'pointer',
945
- }}
946
- />
947
- ))}
948
- </Box>
949
- )}
950
- </>
951
- ) : (
952
- <>
953
- <Typography
954
- variant="body2"
955
- sx={{
956
- whiteSpace: 'pre-line',
957
- lineHeight: 1.5,
958
- '& a': {
959
- color: 'primary.main',
960
- textDecoration: 'none',
961
- '&:hover': { textDecoration: 'underline' },
962
- },
963
- }}
964
- dangerouslySetInnerHTML={{
965
- __html: msg.text?.replace(/\n/g, '<br />') || '',
966
- }}
967
- />
968
- {msg.buttons && msg.buttons.length > 0 && (
969
- <Box sx={{ mt: 1.5, display: 'flex', flexWrap: 'wrap', gap: 1 }}>
970
- {msg.buttons.map((btn, idx) => (
971
- <Chip
972
- key={idx}
973
- label={btn.title}
974
- onClick={() => handleQuickReply(btn)}
975
- sx={{
976
- bgcolor: BRAND.chipBg,
977
- color: BRAND.chipText,
978
- '&:hover': { bgcolor: '#FFD4BB' },
979
- cursor: 'pointer',
980
- }}
981
- />
982
- ))}
983
- </Box>
984
- )}
985
- </>
986
- )}
987
- </Paper>
988
- </ListItem>
989
- ))}
990
-
991
- <div ref={messagesEndRef} />
992
- </List>
993
- </Box>
994
-
995
- {/* Composer */}
996
- <Box
997
- component="form"
998
- onSubmit={handleSendMessage}
999
- sx={{
1000
- p: 1.25,
1001
- bgcolor: BRAND.windowBg,
1002
- borderTop: '1px solid #EDF0F5',
1003
- flexShrink: 0,
1004
- }}
1005
- >
1006
- <TextField
1007
- fullWidth
1008
- variant="outlined"
1009
- placeholder="Type here"
1010
- value={inputValue}
1011
- onChange={(e) => setInputValue(e.target.value)}
1012
- onKeyDown={(e) => {
1013
- if (e.key === 'Enter' && !e.shiftKey) {
1014
- e.preventDefault();
1015
- handleSendMessage(e);
1016
- }
1017
- }}
1018
- multiline
1019
- maxRows={4}
1020
- disabled={isLoading}
1021
- InputProps={{
1022
- endAdornment: (
1023
- <InputAdornment position="end" sx={{ mr: 0.25 }}>
1024
- <IconButton
1025
- type="submit"
1026
- disabled={!inputValue.trim() || isLoading}
1027
  sx={{
1028
- bgcolor: BRAND.sendBtn,
1029
- color: '#fff',
1030
- width: 40,
1031
- height: 40,
1032
- borderRadius: 2,
1033
- boxShadow: '0 6px 18px rgba(250, 140, 106, 0.35)',
1034
- '&:hover': { bgcolor: BRAND.sendBtnHover },
 
 
1035
  }}
1036
- edge="end"
1037
  >
1038
- <SendIcon fontSize="small" />
1039
- </IconButton>
1040
- </InputAdornment>
1041
- ),
1042
- sx: {
1043
- borderRadius: 3,
1044
- bgcolor: BRAND.inputBg,
1045
- '& .MuiOutlinedInput-input::placeholder': {
1046
- color: BRAND.placeholder,
1047
- opacity: 1,
1048
- },
1049
- '& .MuiOutlinedInput-notchedOutline': {
1050
- borderColor: BRAND.inputBorder,
1051
- },
1052
- '&:hover .MuiOutlinedInput-notchedOutline': {
1053
- borderColor: BRAND.inputBorder,
1054
- },
1055
- '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
1056
- borderColor: BRAND.sendBtn,
1057
- borderWidth: 1.5,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1058
  },
1059
- },
1060
- }}
1061
- />
1062
- </Box>
1063
- </Paper>
1064
- </Box>
1065
  );
1066
  };
1067
 
 
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
  List,
15
  ListItem,
16
  ListItemAvatar,
 
 
17
  Chip,
18
  } from '@mui/material';
19
  import SendIcon from '@mui/icons-material/Send';
 
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
  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
  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
  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
  }
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
  });
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
  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
  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
  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')
 
276
  .find(s => s.startsWith('# '));
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
  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);
313
  doc.setLineWidth(0.3);
314
  doc.line(margin, margin + logoHeight + 4, pageWidth - margin, margin + logoHeight + 4);
315
  };
316
+
317
  const ensurePage = (nextBlockHeight = 0) => {
318
  if (cursorY + nextBlockHeight > pageHeight - margin) {
319
  doc.addPage();
 
321
  cursorY = margin + logoHeight + 10;
322
  }
323
  };
324
+
325
+ // Header + title
326
  addHeader();
327
  doc.setFont('helvetica', 'bold');
328
  doc.setFontSize(18);
329
  doc.text(derivedTitle, pageWidth / 2, margin + logoHeight + 14, { align: 'center' });
330
  doc.setFont('helvetica', 'normal');
331
  doc.setFontSize(12);
332
+
333
  // Light markdown handling
334
  const lines = (content || '').split('\n');
335
  let inCode = false;
336
  const codeLines: string[] = [];
337
+
338
  const flushCode = () => {
339
  if (!inCode || codeLines.length === 0) return;
340
  doc.setFont('courier', 'normal');
 
352
  codeLines.length = 0;
353
  cursorY += 1.5;
354
  };
355
+
356
  for (let raw of lines) {
357
  const line = raw ?? '';
358
  const t = line.trim();
359
+
360
  if (t.startsWith('```')) {
361
  if (inCode) flushCode();
362
  else { inCode = true; codeLines.length = 0; }
 
364
  }
365
  if (inCode) { codeLines.push(line); continue; }
366
  if (!t) { cursorY += baseLine; continue; }
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; }
394
  doc.setFont('helvetica', 'normal');
395
  continue;
396
  }
397
+
398
  if (/^(\* |- |• )/.test(t)) {
399
  const bulletText = t.replace(/^(\* |- |• )/, '');
400
  const bulletIndent = 6;
 
412
  }
413
  continue;
414
  }
415
+
416
  const wrapped = doc.splitTextToSize(line, contentWidth);
417
  ensurePage(baseLine * wrapped.length);
418
  for (const w of wrapped) { doc.text(w, margin, cursorY); cursorY += baseLine; }
419
  }
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
  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
  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
  }
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
  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
  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
  }
591
 
592
  if (docxAction) {
 
593
  setTimeout(() => {
594
  const node = reportRefs.current.get(targetIdx as number);
595
  if (node) {
 
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
 
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
  })();
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
 
src/components/FloatingChat.tsx CHANGED
@@ -3,7 +3,6 @@ import { Box, Fab, Slide, useMediaQuery, useTheme } from '@mui/material';
3
  import ChatIcon from '@mui/icons-material/Chat';
4
  import CloseIcon from '@mui/icons-material/Close';
5
  import ChatInterface from './ChatInterface';
6
-
7
  const FloatingChat: React.FC = () => {
8
  const [isOpen, setIsOpen] = useState(false);
9
  const theme = useTheme();
@@ -16,61 +15,60 @@ const FloatingChat: React.FC = () => {
16
 
17
  return (
18
  <Box
19
- sx={{
20
- position: 'fixed',
21
- bottom: isMobile ? 16 : 24,
22
- right: isMobile ? 16 : 24,
23
- zIndex: 1200,
24
- display: 'flex',
25
- flexDirection: 'column',
26
- alignItems: 'flex-end',
27
- gap: 2,
28
- }}
29
- >
30
- <Slide
31
- direction="up" // Changed from "left" to "up" for swipe-up animation
32
- in={isOpen}
33
- mountOnEnter
34
- unmountOnExit
35
- timeout={{ enter: 300, exit: 200 }} // Customize animation duration
36
- >
37
- <Box
38
- ref={chatRef}
39
  sx={{
40
- width:0,
41
- height: 0,
42
- borderRadius: 2,
43
- overflow: 'hidden',
44
  display: 'flex',
45
  flexDirection: 'column',
46
- boxShadow: 6,
47
- background: 'transparent',
48
  }}
49
  >
50
- <ChatInterface onClose={() => setIsOpen(false)} />
51
- </Box>
52
- </Slide>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- {/* Fab button remains the same */}
55
- <Fab
56
- color="primary"
57
- aria-label="chat"
58
- onClick={toggleChat}
59
- sx={{
60
- width: 60,
61
- height: 60,
62
- backgroundColor: theme => theme.palette.primary.main,
63
- color: theme => theme.palette.primary.contrastText,
64
- '&:hover': {
65
- backgroundColor: theme => theme.palette.primary.dark,
66
- },
67
- transition: 'all 0.3s ease',
68
- zIndex: 1302,
69
- }}
70
- >
71
- {isOpen ? <CloseIcon /> : <ChatIcon fontSize="large" />}
72
- </Fab>
73
- </Box>
74
  );
75
  };
76
 
 
3
  import ChatIcon from '@mui/icons-material/Chat';
4
  import CloseIcon from '@mui/icons-material/Close';
5
  import ChatInterface from './ChatInterface';
 
6
  const FloatingChat: React.FC = () => {
7
  const [isOpen, setIsOpen] = useState(false);
8
  const theme = useTheme();
 
15
 
16
  return (
17
  <Box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  sx={{
19
+ position: 'fixed',
20
+ bottom: isMobile ? 16 : 24,
21
+ right: isMobile ? 16 : 24,
22
+ zIndex: 1200,
23
  display: 'flex',
24
  flexDirection: 'column',
25
+ alignItems: 'flex-end',
26
+ gap: 2,
27
  }}
28
  >
29
+ <Slide
30
+ direction="up"
31
+ in={isOpen}
32
+ mountOnEnter
33
+ unmountOnExit
34
+ timeout={{ enter: 300, exit: 200 }}
35
+ >
36
+ <Box
37
+ ref={chatRef}
38
+ sx={{
39
+ width: 'auto', // Changed from 0 to auto
40
+ height: 'auto', // Changed from 0 to auto
41
+ borderRadius: 2,
42
+ overflow: 'visible', // Changed from 'hidden' to 'visible'
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ boxShadow: 6,
46
+ background: 'transparent',
47
+ }}
48
+ >
49
+ <ChatInterface onClose={() => setIsOpen(false)} />
50
+ </Box>
51
+ </Slide>
52
 
53
+ <Fab
54
+ color="primary"
55
+ aria-label="chat"
56
+ onClick={toggleChat}
57
+ sx={{
58
+ width: 60,
59
+ height: 60,
60
+ backgroundColor: theme => theme.palette.primary.main,
61
+ color: theme => theme.palette.primary.contrastText,
62
+ '&:hover': {
63
+ backgroundColor: theme => theme.palette.primary.dark,
64
+ },
65
+ transition: 'all 0.3s ease',
66
+ zIndex: 1302,
67
+ }}
68
+ >
69
+ {isOpen ? <CloseIcon /> : <ChatIcon fontSize="large" />}
70
+ </Fab>
71
+ </Box>
 
72
  );
73
  };
74
 
src/index.css CHANGED
@@ -11,3 +11,21 @@ code {
11
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
  monospace;
13
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
  monospace;
13
  }
14
+ .react-resizable {
15
+ position: relative;
16
+ }
17
+
18
+ .react-resizable-handle {
19
+ position: absolute;
20
+ width: 20px;
21
+ height: 20px;
22
+ bottom: 0;
23
+ right: 0;
24
+ background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+');
25
+ background-position: bottom right;
26
+ padding: 0 3px 3px 0;
27
+ background-repeat: no-repeat;
28
+ background-origin: content-box;
29
+ box-sizing: border-box;
30
+ cursor: se-resize;
31
+ }