pranav8tripathi@gmail.com commited on
Commit
e8c6328
·
1 Parent(s): ab33547

fixed Word download

Browse files
Files changed (1) hide show
  1. src/components/ChatInterface.tsx +238 -115
src/components/ChatInterface.tsx CHANGED
@@ -35,6 +35,7 @@ import {
35
  TableCell as DocxTableCell,
36
  WidthType,
37
  TextRun,
 
38
  } from 'docx';
39
  interface ChatInterfaceProps {
40
  onClose?: () => void;
@@ -448,121 +449,241 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
448
  return filename;
449
  };
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
 
452
- // DOCX (raster fallback)
453
- const captureNodeToDocx = async (node: HTMLElement, onDone?: (filename: string) => void) => {
454
- if (!node) return;
455
- try {
456
- const prev = {
457
- width: node.style.width,
458
- maxWidth: (node.style as any).maxWidth,
459
- background: node.style.background,
460
- padding: node.style.padding,
461
- boxShadow: node.style.boxShadow,
462
- };
463
- node.style.background = '#ffffff';
464
- node.style.padding = '16px';
465
- (node.style as any).maxWidth = 'unset';
466
- node.style.width = '1024px';
467
- node.style.boxShadow = 'none';
468
-
469
- const canvas = await html2canvas(node, {
470
- scale: 3,
471
- useCORS: true,
472
- backgroundColor: '#ffffff',
473
- windowWidth: node.scrollWidth,
474
- });
475
-
476
- const pageWidthTwips = Math.round(8.27 * 1440);
477
- const pageHeightTwips = Math.round(11.69 * 1440);
478
- const marginTwips = 720;
479
- const contentWidthTwips = pageWidthTwips - marginTwips * 2;
480
- const contentHeightTwips = pageHeightTwips - marginTwips * 2;
481
-
482
- const contentWidthInches = contentWidthTwips / 1440;
483
- const contentHeightInches = contentHeightTwips / 1440;
484
- const dpi = 96;
485
- const targetWidthPx = Math.floor(contentWidthInches * dpi);
486
- const pageSliceHeightPx = Math.floor(contentHeightInches * dpi);
487
-
488
- const sectionChildren: Paragraph[] = [];
489
- const totalHeightPx = canvas.height;
490
- let offsetY = 0;
491
-
492
- while (offsetY < totalHeightPx) {
493
- const sliceHeightPx = Math.min(pageSliceHeightPx, totalHeightPx - offsetY);
494
- const tempCanvas = document.createElement('canvas');
495
- tempCanvas.width = canvas.width;
496
- tempCanvas.height = sliceHeightPx;
497
- const tctx = tempCanvas.getContext('2d');
498
- if (!tctx) break;
499
-
500
- tctx.drawImage(
501
- canvas,
502
- 0,
503
- offsetY,
504
- canvas.width,
505
- sliceHeightPx,
506
- 0,
507
- 0,
508
- tempCanvas.width,
509
- tempCanvas.height
510
- );
511
 
512
- const dataUrl = tempCanvas.toDataURL('image/png');
513
- const res = await fetch(dataUrl);
514
- const blob = await res.blob();
515
- const arrayBuffer = await blob.arrayBuffer();
516
 
517
- const ratio = targetWidthPx / canvas.width;
518
- const targetHeightPx = Math.round(sliceHeightPx * ratio);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
- const imageRun: ImageRun = new ImageRun({
521
- data: new Uint8Array(arrayBuffer),
522
- transformation: { width: targetWidthPx, height: targetHeightPx },
523
- } as any);
524
- sectionChildren.push(new Paragraph({ children: [imageRun] }));
525
- offsetY += sliceHeightPx;
 
 
 
 
 
 
 
 
 
 
 
526
  }
 
527
 
528
- const doc = new Document({
529
- sections: [
530
- {
531
- properties: {
532
- page: {
533
- size: { width: pageWidthTwips, height: pageHeightTwips },
534
- margin: {
535
- top: marginTwips,
536
- bottom: marginTwips,
537
- left: marginTwips,
538
- right: marginTwips,
539
- },
540
- },
541
- },
542
- children: sectionChildren,
543
- },
544
- ],
545
- });
546
 
547
- const blob = await Packer.toBlob(doc);
548
- const filename = `report-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.docx`;
549
- const link = document.createElement('a');
550
- link.href = URL.createObjectURL(blob);
551
- link.download = filename;
552
- document.body.appendChild(link);
553
- link.click();
554
- link.remove();
555
- onDone?.(filename);
 
 
 
556
 
557
- node.style.width = prev.width;
558
- (node.style as any).maxWidth = prev.maxWidth;
559
- node.style.background = prev.background;
560
- node.style.padding = prev.padding;
561
- node.style.boxShadow = prev.boxShadow;
562
- } catch (e) {
563
- console.error('[DOCX] Failed to create DOCX:', e);
564
- }
565
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
  const startDownload = async () => {
568
  const idx = (() => {
@@ -677,16 +798,18 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ onClose }) => {
677
  }
678
 
679
  if (docxAction) {
680
- // Raster fallback (kept, you can swap to text-based docx if needed)
681
- setTimeout(() => {
682
- const node = reportRefs.current.get(targetIdx as number);
683
- if (node) {
684
- captureNodeToDocx(node, (fn) => pushBotNotice(`✅ DOCX downloaded: ${fn}`));
685
- } else {
686
- const fallback = lastReportRef.current;
687
- if (fallback) captureNodeToDocx(fallback, (fn) => pushBotNotice(`✅ DOCX downloaded: ${fn}`));
 
 
688
  }
689
- }, 300);
690
  }
691
  }
692
  }
 
35
  TableCell as DocxTableCell,
36
  WidthType,
37
  TextRun,
38
+ BorderStyle,
39
  } from 'docx';
40
  interface ChatInterfaceProps {
41
  onClose?: () => void;
 
449
  return filename;
450
  };
451
 
452
+ // ---- DOCX: Generator with logos + selectable text ----
453
+ const generateReportDocx = async ({
454
+ content,
455
+ title,
456
+ yugenLogoSrc = YUGEN_LOGO_SRC,
457
+ bizLogoSrc = BIZ_LOGO_SRC,
458
+ }: {
459
+ content: string;
460
+ title?: string;
461
+ yugenLogoSrc?: string;
462
+ bizLogoSrc?: string;
463
+ }): Promise<string> => {
464
+ console.info('[DOCX] Generating text-based document...');
465
 
466
+ // Helper to fetch image as ArrayBuffer for the docx library
467
+ const fetchImageAsArrayBuffer = async (src: string): Promise<ArrayBuffer | null> => {
468
+ try {
469
+ const res = await fetch(src, { credentials: 'include', cache: 'force-cache' });
470
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${src}`);
471
+ return await res.arrayBuffer();
472
+ } catch (e) {
473
+ console.warn(`[DOCX] Image load failed for ${src}:`, e);
474
+ return null;
475
+ }
476
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
+ const [yugenLogoBuffer, bizLogoBuffer] = await Promise.all([
479
+ fetchImageAsArrayBuffer(yugenLogoSrc),
480
+ fetchImageAsArrayBuffer(bizLogoSrc),
481
+ ]);
482
 
483
+ const headerChildren: (Paragraph | DocxTable)[] = [];
484
+ if (yugenLogoBuffer && bizLogoBuffer) {
485
+ headerChildren.push(
486
+ new DocxTable({
487
+ width: { size: '100%', type: WidthType.PERCENTAGE },
488
+ columnWidths: [2500, 5000, 2500],
489
+ borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } },
490
+ rows: [
491
+ new DocxTableRow({
492
+ children: [
493
+ new DocxTableCell({
494
+ children: [new Paragraph({
495
+ children: [new ImageRun({
496
+ data: yugenLogoBuffer,
497
+ transformation: { width: 160, height: 60 },
498
+ type: 'png', // or 'jpeg' based on your image type
499
+ })]
500
+ })],
501
+ borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } },
502
+ }),
503
+ new DocxTableCell({ children: [new Paragraph('')], borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } } }),
504
+ new DocxTableCell({
505
+ children: [new Paragraph({
506
+ alignment: AlignmentType.RIGHT, children: [new ImageRun({
507
+ data: bizLogoBuffer,
508
+ transformation: { width: 160, height: 60 },
509
+ type: 'png', // or 'jpeg' based on your image type
510
+ })]
511
+ })],
512
+ borders: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } },
513
+ }),
514
+ ],
515
+ }),
516
+ ],
517
+ })
518
+ );
519
+ }
520
+ headerChildren.push(new Paragraph({ style: 'header',border: {
521
+ top: { style: BorderStyle.SINGLE },
522
+ bottom: { style: BorderStyle.SINGLE },
523
+ left: { style: BorderStyle.SINGLE },
524
+ right: { style: BorderStyle.SINGLE }
525
+ } }));
526
+
527
+ const derivedTitle = title || (content || '').split('\n').map(s => s.trim()).find(s => s.startsWith('# '))?.replace(/^#\s+/, '') || 'Report';
528
+
529
+ const bodyParagraphs: Paragraph[] = [
530
+ new Paragraph({ text: derivedTitle, heading: HeadingLevel.TITLE, alignment: AlignmentType.CENTER }),
531
+ new Paragraph({ text: '' }), // Spacer
532
+ ];
533
 
534
+ (content || '').split('\n').forEach(line => {
535
+ const t = line.trim();
536
+ if (t.startsWith('### ')) {
537
+ bodyParagraphs.push(new Paragraph({ text: t.replace(/^###\s+/, ''), heading: HeadingLevel.HEADING_3 }));
538
+ } else if (t.startsWith('## ')) {
539
+ bodyParagraphs.push(new Paragraph({ text: t.replace(/^##\s+/, ''), heading: HeadingLevel.HEADING_2 }));
540
+ } else if (t.startsWith('# ') && t.replace(/^#\s+/, '') !== derivedTitle) {
541
+ bodyParagraphs.push(new Paragraph({ text: t.replace(/^#\s+/, ''), heading: HeadingLevel.HEADING_1 }));
542
+ } else if (/^(\* |- |• )/.test(t)) {
543
+ bodyParagraphs.push(new Paragraph({ text: t.replace(/^(\* |- |• )/, ''), bullet: { level: 0 } }));
544
+ } else {
545
+ const runs = (line.split(/(\*\*.*?\*\*|\*.*?\*)/g).filter(Boolean)).map(part => {
546
+ if (part.startsWith('**') && part.endsWith('**')) return new TextRun({ text: part.slice(2, -2), bold: true });
547
+ if (part.startsWith('*') && part.endsWith('*')) return new TextRun({ text: part.slice(1, -1), italics: true });
548
+ return new TextRun(part);
549
+ });
550
+ bodyParagraphs.push(new Paragraph({ children: runs }));
551
  }
552
+ });
553
 
554
+ const doc = new Document({
555
+ sections: [{ headers: { default: new DocxHeader({ children: headerChildren }) }, children: bodyParagraphs }],
556
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
+ const filename = `report-${new Date().toISOString().replace(/[:.]/g, '-')}.docx`;
559
+ const blob = await Packer.toBlob(doc);
560
+ const link = document.createElement('a');
561
+ link.href = URL.createObjectURL(blob);
562
+ link.download = filename;
563
+ document.body.appendChild(link);
564
+ link.click();
565
+ document.body.removeChild(link);
566
+ URL.revokeObjectURL(link.href);
567
+
568
+ // Notify RASA that the research step is complete
569
+ await sendMessage('/research_complete');
570
 
571
+ return filename;
 
 
 
 
 
 
 
572
  };
573
+ // DOCX (raster fallback)
574
+ // const captureNodeToDocx = async (node: HTMLElement, onDone?: (filename: string) => void) => {
575
+ // if (!node) return;
576
+ // try {
577
+ // const prev = {
578
+ // width: node.style.width,
579
+ // maxWidth: (node.style as any).maxWidth,
580
+ // background: node.style.background,
581
+ // padding: node.style.padding,
582
+ // boxShadow: node.style.boxShadow,
583
+ // };
584
+ // node.style.background = '#ffffff';
585
+ // node.style.padding = '16px';
586
+ // (node.style as any).maxWidth = 'unset';
587
+ // node.style.width = '1024px';
588
+ // node.style.boxShadow = 'none';
589
+
590
+ // const canvas = await html2canvas(node, {
591
+ // scale: 3,
592
+ // useCORS: true,
593
+ // backgroundColor: '#ffffff',
594
+ // windowWidth: node.scrollWidth,
595
+ // });
596
+
597
+ // const pageWidthTwips = Math.round(8.27 * 1440);
598
+ // const pageHeightTwips = Math.round(11.69 * 1440);
599
+ // const marginTwips = 720;
600
+ // const contentWidthTwips = pageWidthTwips - marginTwips * 2;
601
+ // const contentHeightTwips = pageHeightTwips - marginTwips * 2;
602
+
603
+ // const contentWidthInches = contentWidthTwips / 1440;
604
+ // const contentHeightInches = contentHeightTwips / 1440;
605
+ // const dpi = 96;
606
+ // const targetWidthPx = Math.floor(contentWidthInches * dpi);
607
+ // const pageSliceHeightPx = Math.floor(contentHeightInches * dpi);
608
+
609
+ // const sectionChildren: Paragraph[] = [];
610
+ // const totalHeightPx = canvas.height;
611
+ // let offsetY = 0;
612
+
613
+ // while (offsetY < totalHeightPx) {
614
+ // const sliceHeightPx = Math.min(pageSliceHeightPx, totalHeightPx - offsetY);
615
+ // const tempCanvas = document.createElement('canvas');
616
+ // tempCanvas.width = canvas.width;
617
+ // tempCanvas.height = sliceHeightPx;
618
+ // const tctx = tempCanvas.getContext('2d');
619
+ // if (!tctx) break;
620
+
621
+ // tctx.drawImage(
622
+ // canvas,
623
+ // 0,
624
+ // offsetY,
625
+ // canvas.width,
626
+ // sliceHeightPx,
627
+ // 0,
628
+ // 0,
629
+ // tempCanvas.width,
630
+ // tempCanvas.height
631
+ // );
632
+
633
+ // const dataUrl = tempCanvas.toDataURL('image/png');
634
+ // const res = await fetch(dataUrl);
635
+ // const blob = await res.blob();
636
+ // const arrayBuffer = await blob.arrayBuffer();
637
+
638
+ // const ratio = targetWidthPx / canvas.width;
639
+ // const targetHeightPx = Math.round(sliceHeightPx * ratio);
640
+
641
+ // const imageRun: ImageRun = new ImageRun({
642
+ // data: new Uint8Array(arrayBuffer),
643
+ // transformation: { width: targetWidthPx, height: targetHeightPx },
644
+ // } as any);
645
+ // sectionChildren.push(new Paragraph({ children: [imageRun] }));
646
+ // offsetY += sliceHeightPx;
647
+ // }
648
+
649
+ // const doc = new Document({
650
+ // sections: [
651
+ // {
652
+ // properties: {
653
+ // page: {
654
+ // size: { width: pageWidthTwips, height: pageHeightTwips },
655
+ // margin: {
656
+ // top: marginTwips,
657
+ // bottom: marginTwips,
658
+ // left: marginTwips,
659
+ // right: marginTwips,
660
+ // },
661
+ // },
662
+ // },
663
+ // children: sectionChildren,
664
+ // },
665
+ // ],
666
+ // });
667
+
668
+ // const blob = await Packer.toBlob(doc);
669
+ // const filename = `report-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.docx`;
670
+ // const link = document.createElement('a');
671
+ // link.href = URL.createObjectURL(blob);
672
+ // link.download = filename;
673
+ // document.body.appendChild(link);
674
+ // link.click();
675
+ // link.remove();
676
+ // onDone?.(filename);
677
+
678
+ // node.style.width = prev.width;
679
+ // (node.style as any).maxWidth = prev.maxWidth;
680
+ // node.style.background = prev.background;
681
+ // node.style.padding = prev.padding;
682
+ // node.style.boxShadow = prev.boxShadow;
683
+ // } catch (e) {
684
+ // console.error('[DOCX] Failed to create DOCX:', e);
685
+ // }
686
+ // };
687
 
688
  const startDownload = async () => {
689
  const idx = (() => {
 
798
  }
799
 
800
  if (docxAction) {
801
+ if (!targetText.trim()) {
802
+ pushBotNotice('❌ Could not find report content to download.');
803
+ } else {
804
+ try {
805
+ // Use the new text-based generator
806
+ const filename = await generateReportDocx({ content: targetText });
807
+ pushBotNotice(`✅ DOCX downloaded: ${filename}`);
808
+ } catch (e) {
809
+ console.error('[DOCX] Failed to generate:', e);
810
+ pushBotNotice('❌ Failed to generate DOCX file.');
811
  }
812
+ }
813
  }
814
  }
815
  }