Stijnus commited on
Commit
84f45dd
·
1 Parent(s): fc3dd8c

Add support for export JSON, CSV, PDF, Text

Browse files

## Changes to DebugTab.tsx & EventLogsTab.tsx

### Debug Tab Enhancements
- Added multi-page support for PDF exports
- Implemented proper page breaks and content flow
- Added styled headers, key-value pairs, and horizontal lines
- Added title and timestamp at the top of the PDF
- Improved PDF layout with sections for system info, web app info, and performance metrics
- Added footer with page numbers
- Fixed memory usage calculations with proper null checks
- Added error handling for undefined values

### Event Logs Tab Enhancements
- Added comprehensive PDF export functionality with:
- Professional header with bolt.diy branding
- Report summary section
- Log statistics with color-coded categories
- Detailed log entries with proper formatting
- Multi-page support with proper page breaks
- Footer with page numbers and timestamp
- Added multiple export formats (JSON, CSV, PDF, Text)
- Fixed linter errors and improved type safety
- Enhanced dark mode compatibility

app/components/@settings/tabs/debug/DebugTab.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState, useMemo } from 'react';
2
  import { toast } from 'react-toastify';
3
  import { classNames } from '~/utils/classNames';
4
  import { logStore, type LogEntry } from '~/lib/stores/logs';
@@ -7,6 +7,8 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/component
7
  import { Progress } from '~/components/ui/Progress';
8
  import { ScrollArea } from '~/components/ui/ScrollArea';
9
  import { Badge } from '~/components/ui/Badge';
 
 
10
 
11
  interface SystemInfo {
12
  os: string;
@@ -138,6 +140,13 @@ interface OllamaServiceStatus {
138
  error?: string;
139
  }
140
 
 
 
 
 
 
 
 
141
  const DependencySection = ({
142
  title,
143
  deps,
@@ -541,7 +550,7 @@ export default function DebugTab() {
541
  const resourceEntries = performance.getEntriesByType('resource');
542
  const resourceStats = {
543
  totalResources: resourceEntries.length,
544
- totalSize: resourceEntries.reduce((total, entry) => total + (entry as any).transferSize || 0, 0),
545
  totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)),
546
  };
547
 
@@ -651,6 +660,438 @@ export default function DebugTab() {
651
  }
652
  };
653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  // Add Ollama health check function
655
  const checkOllamaHealth = async () => {
656
  try {
@@ -687,6 +1128,77 @@ export default function DebugTab() {
687
  return () => clearInterval(interval);
688
  }, []);
689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  return (
691
  <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
692
  {/* Quick Stats Banner */}
@@ -830,20 +1342,7 @@ export default function DebugTab() {
830
  Fetch WebApp Info
831
  </button>
832
 
833
- <button
834
- onClick={exportDebugInfo}
835
- className={classNames(
836
- 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
837
- 'bg-white dark:bg-[#0A0A0A]',
838
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
839
- 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
840
- 'hover:border-purple-200 dark:hover:border-purple-900/30',
841
- 'text-bolt-elements-textPrimary',
842
- )}
843
- >
844
- <div className="i-ph:download w-4 h-4" />
845
- Export Debug Info
846
- </button>
847
  </div>
848
 
849
  {/* System Information */}
 
1
+ import React, { useEffect, useState, useMemo, useCallback } from 'react';
2
  import { toast } from 'react-toastify';
3
  import { classNames } from '~/utils/classNames';
4
  import { logStore, type LogEntry } from '~/lib/stores/logs';
 
7
  import { Progress } from '~/components/ui/Progress';
8
  import { ScrollArea } from '~/components/ui/ScrollArea';
9
  import { Badge } from '~/components/ui/Badge';
10
+ import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
11
+ import { jsPDF } from 'jspdf';
12
 
13
  interface SystemInfo {
14
  os: string;
 
140
  error?: string;
141
  }
142
 
143
+ interface ExportFormat {
144
+ id: string;
145
+ label: string;
146
+ icon: string;
147
+ handler: () => void;
148
+ }
149
+
150
  const DependencySection = ({
151
  title,
152
  deps,
 
550
  const resourceEntries = performance.getEntriesByType('resource');
551
  const resourceStats = {
552
  totalResources: resourceEntries.length,
553
+ totalSize: resourceEntries.reduce((total, entry) => total + ((entry as any).transferSize || 0), 0),
554
  totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)),
555
  };
556
 
 
660
  }
661
  };
662
 
663
+ const exportAsCSV = () => {
664
+ try {
665
+ const debugData = {
666
+ system: systemInfo,
667
+ webApp: webAppInfo,
668
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
669
+ performance: {
670
+ memory: (performance as any).memory || {},
671
+ timing: performance.timing,
672
+ navigation: performance.navigation,
673
+ },
674
+ };
675
+
676
+ // Convert the data to CSV format
677
+ const csvData = [
678
+ ['Category', 'Key', 'Value'],
679
+ ...Object.entries(debugData).flatMap(([category, data]) =>
680
+ Object.entries(data || {}).map(([key, value]) => [
681
+ category,
682
+ key,
683
+ typeof value === 'object' ? JSON.stringify(value) : String(value),
684
+ ]),
685
+ ),
686
+ ];
687
+
688
+ // Create CSV content
689
+ const csvContent = csvData.map((row) => row.join(',')).join('\n');
690
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
691
+ const url = window.URL.createObjectURL(blob);
692
+ const a = document.createElement('a');
693
+ a.href = url;
694
+ a.download = `bolt-debug-info-${new Date().toISOString()}.csv`;
695
+ document.body.appendChild(a);
696
+ a.click();
697
+ window.URL.revokeObjectURL(url);
698
+ document.body.removeChild(a);
699
+ toast.success('Debug information exported as CSV');
700
+ } catch (error) {
701
+ console.error('Failed to export CSV:', error);
702
+ toast.error('Failed to export debug information as CSV');
703
+ }
704
+ };
705
+
706
+ const exportAsPDF = () => {
707
+ try {
708
+ const debugData = {
709
+ system: systemInfo,
710
+ webApp: webAppInfo,
711
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
712
+ performance: {
713
+ memory: (performance as any).memory || {},
714
+ timing: performance.timing,
715
+ navigation: performance.navigation,
716
+ },
717
+ };
718
+
719
+ // Create new PDF document
720
+ const doc = new jsPDF();
721
+ const lineHeight = 7;
722
+ let yPos = 20;
723
+ const margin = 20;
724
+ const pageWidth = doc.internal.pageSize.getWidth();
725
+ const maxLineWidth = pageWidth - 2 * margin;
726
+
727
+ // Add key-value pair with better formatting
728
+ const addKeyValue = (key: string, value: any, indent = 0) => {
729
+ // Check if we need a new page
730
+ if (yPos > doc.internal.pageSize.getHeight() - 20) {
731
+ doc.addPage();
732
+ yPos = margin;
733
+ }
734
+
735
+ doc.setFontSize(10);
736
+ doc.setTextColor('#374151');
737
+ doc.setFont('helvetica', 'bold');
738
+
739
+ // Format the key with proper spacing
740
+ const formattedKey = key.replace(/([A-Z])/g, ' $1').trim();
741
+ doc.text(formattedKey + ':', margin + indent, yPos);
742
+ doc.setFont('helvetica', 'normal');
743
+ doc.setTextColor('#6B7280');
744
+
745
+ let valueText;
746
+
747
+ if (typeof value === 'object' && value !== null) {
748
+ // Skip rendering if value is empty object
749
+ if (Object.keys(value).length === 0) {
750
+ return;
751
+ }
752
+
753
+ yPos += lineHeight;
754
+ Object.entries(value).forEach(([subKey, subValue]) => {
755
+ // Check for page break before each sub-item
756
+ if (yPos > doc.internal.pageSize.getHeight() - 20) {
757
+ doc.addPage();
758
+ yPos = margin;
759
+ }
760
+
761
+ const formattedSubKey = subKey.replace(/([A-Z])/g, ' $1').trim();
762
+ addKeyValue(formattedSubKey, subValue, indent + 10);
763
+ });
764
+
765
+ return;
766
+ } else {
767
+ valueText = String(value);
768
+ }
769
+
770
+ const valueX = margin + indent + doc.getTextWidth(formattedKey + ': ');
771
+ const maxValueWidth = maxLineWidth - indent - doc.getTextWidth(formattedKey + ': ');
772
+ const lines = doc.splitTextToSize(valueText, maxValueWidth);
773
+
774
+ // Check if we need a new page for the value
775
+ if (yPos + lines.length * lineHeight > doc.internal.pageSize.getHeight() - 20) {
776
+ doc.addPage();
777
+ yPos = margin;
778
+ }
779
+
780
+ doc.text(lines, valueX, yPos);
781
+ yPos += lines.length * lineHeight;
782
+ };
783
+
784
+ // Add section header with page break check
785
+ const addSectionHeader = (title: string) => {
786
+ // Check if we need a new page
787
+ if (yPos + 20 > doc.internal.pageSize.getHeight() - 20) {
788
+ doc.addPage();
789
+ yPos = margin;
790
+ }
791
+
792
+ yPos += lineHeight;
793
+ doc.setFillColor('#F3F4F6');
794
+ doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F');
795
+ doc.setFont('helvetica', 'bold');
796
+ doc.setTextColor('#111827');
797
+ doc.setFontSize(12);
798
+ doc.text(title.toUpperCase(), margin, yPos);
799
+ doc.setFont('helvetica', 'normal');
800
+ yPos += lineHeight * 1.5;
801
+ };
802
+
803
+ // Add horizontal line with page break check
804
+ const addHorizontalLine = () => {
805
+ // Check if we need a new page
806
+ if (yPos + 10 > doc.internal.pageSize.getHeight() - 20) {
807
+ doc.addPage();
808
+ yPos = margin;
809
+
810
+ return; // Skip drawing line if we just started a new page
811
+ }
812
+
813
+ doc.setDrawColor('#E5E5E5');
814
+ doc.line(margin, yPos, pageWidth - margin, yPos);
815
+ yPos += lineHeight;
816
+ };
817
+
818
+ // Helper function to add footer to all pages
819
+ const addFooters = () => {
820
+ const totalPages = doc.internal.pages.length - 1;
821
+
822
+ for (let i = 1; i <= totalPages; i++) {
823
+ doc.setPage(i);
824
+ doc.setFontSize(8);
825
+ doc.setTextColor('#9CA3AF');
826
+ doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, {
827
+ align: 'center',
828
+ });
829
+ }
830
+ };
831
+
832
+ // Title and Header (first page only)
833
+ doc.setFillColor('#6366F1');
834
+ doc.rect(0, 0, pageWidth, 40, 'F');
835
+ doc.setTextColor('#FFFFFF');
836
+ doc.setFontSize(24);
837
+ doc.setFont('helvetica', 'bold');
838
+ doc.text('Debug Information Report', margin, 25);
839
+ yPos = 50;
840
+
841
+ // Timestamp and metadata
842
+ doc.setTextColor('#6B7280');
843
+ doc.setFontSize(10);
844
+ doc.setFont('helvetica', 'normal');
845
+
846
+ const timestamp = new Date().toLocaleString(undefined, {
847
+ year: 'numeric',
848
+ month: '2-digit',
849
+ day: '2-digit',
850
+ hour: '2-digit',
851
+ minute: '2-digit',
852
+ second: '2-digit',
853
+ });
854
+ doc.text(`Generated: ${timestamp}`, margin, yPos);
855
+ yPos += lineHeight * 2;
856
+
857
+ // System Information Section
858
+ if (debugData.system) {
859
+ addSectionHeader('System Information');
860
+
861
+ // OS and Architecture
862
+ addKeyValue('Operating System', debugData.system.os);
863
+ addKeyValue('Architecture', debugData.system.arch);
864
+ addKeyValue('Platform', debugData.system.platform);
865
+ addKeyValue('CPU Cores', debugData.system.cpus);
866
+
867
+ // Memory
868
+ const memory = debugData.system.memory;
869
+ addKeyValue('Memory', {
870
+ 'Total Memory': memory.total,
871
+ 'Used Memory': memory.used,
872
+ 'Free Memory': memory.free,
873
+ Usage: memory.percentage + '%',
874
+ });
875
+
876
+ // Browser Information
877
+ const browser = debugData.system.browser;
878
+ addKeyValue('Browser', {
879
+ Name: browser.name,
880
+ Version: browser.version,
881
+ Language: browser.language,
882
+ Platform: browser.platform,
883
+ 'Cookies Enabled': browser.cookiesEnabled ? 'Yes' : 'No',
884
+ 'Online Status': browser.online ? 'Online' : 'Offline',
885
+ });
886
+
887
+ // Screen Information
888
+ const screen = debugData.system.screen;
889
+ addKeyValue('Screen', {
890
+ Resolution: `${screen.width}x${screen.height}`,
891
+ 'Color Depth': screen.colorDepth + ' bit',
892
+ 'Pixel Ratio': screen.pixelRatio + 'x',
893
+ });
894
+
895
+ // Time Information
896
+ const time = debugData.system.time;
897
+ addKeyValue('Time Settings', {
898
+ Timezone: time.timezone,
899
+ 'UTC Offset': time.offset / 60 + ' hours',
900
+ Locale: time.locale,
901
+ });
902
+
903
+ addHorizontalLine();
904
+ }
905
+
906
+ // Web App Information Section
907
+ if (debugData.webApp) {
908
+ addSectionHeader('Web App Information');
909
+
910
+ // Basic Info
911
+ addKeyValue('Application', {
912
+ Name: debugData.webApp.name,
913
+ Version: debugData.webApp.version,
914
+ Environment: debugData.webApp.environment,
915
+ 'Node Version': debugData.webApp.runtimeInfo.nodeVersion,
916
+ });
917
+
918
+ // Git Information
919
+ if (debugData.webApp.gitInfo) {
920
+ const gitInfo = debugData.webApp.gitInfo.local;
921
+ addKeyValue('Git Information', {
922
+ Branch: gitInfo.branch,
923
+ Commit: gitInfo.commitHash,
924
+ Author: gitInfo.author,
925
+ 'Commit Time': gitInfo.commitTime,
926
+ Repository: gitInfo.repoName,
927
+ });
928
+
929
+ if (debugData.webApp.gitInfo.github) {
930
+ const githubInfo = debugData.webApp.gitInfo.github.currentRepo;
931
+ addKeyValue('GitHub Information', {
932
+ Repository: githubInfo.fullName,
933
+ 'Default Branch': githubInfo.defaultBranch,
934
+ Stars: githubInfo.stars,
935
+ Forks: githubInfo.forks,
936
+ 'Open Issues': githubInfo.openIssues || 0,
937
+ });
938
+ }
939
+ }
940
+
941
+ addHorizontalLine();
942
+ }
943
+
944
+ // Performance Section
945
+ if (debugData.performance) {
946
+ addSectionHeader('Performance Metrics');
947
+
948
+ // Memory Usage
949
+ const memory = debugData.performance.memory || {};
950
+ const totalHeap = memory.totalJSHeapSize || 0;
951
+ const usedHeap = memory.usedJSHeapSize || 0;
952
+ const usagePercentage = memory.usagePercentage || 0;
953
+
954
+ addKeyValue('Memory Usage', {
955
+ 'Total Heap Size': formatBytes(totalHeap),
956
+ 'Used Heap Size': formatBytes(usedHeap),
957
+ Usage: usagePercentage.toFixed(1) + '%',
958
+ });
959
+
960
+ // Timing Metrics
961
+ const timing = debugData.performance.timing || {};
962
+ const navigationStart = timing.navigationStart || 0;
963
+ const loadEventEnd = timing.loadEventEnd || 0;
964
+ const domContentLoadedEventEnd = timing.domContentLoadedEventEnd || 0;
965
+ const responseEnd = timing.responseEnd || 0;
966
+ const requestStart = timing.requestStart || 0;
967
+
968
+ const loadTime = loadEventEnd > navigationStart ? loadEventEnd - navigationStart : 0;
969
+ const domReadyTime =
970
+ domContentLoadedEventEnd > navigationStart ? domContentLoadedEventEnd - navigationStart : 0;
971
+ const requestTime = responseEnd > requestStart ? responseEnd - requestStart : 0;
972
+
973
+ addKeyValue('Page Load Metrics', {
974
+ 'Total Load Time': (loadTime / 1000).toFixed(2) + ' seconds',
975
+ 'DOM Ready Time': (domReadyTime / 1000).toFixed(2) + ' seconds',
976
+ 'Request Time': (requestTime / 1000).toFixed(2) + ' seconds',
977
+ });
978
+
979
+ // Network Information
980
+ if (debugData.system?.network) {
981
+ const network = debugData.system.network;
982
+ addKeyValue('Network Information', {
983
+ 'Connection Type': network.type || 'Unknown',
984
+ 'Effective Type': network.effectiveType || 'Unknown',
985
+ 'Download Speed': (network.downlink || 0) + ' Mbps',
986
+ 'Latency (RTT)': (network.rtt || 0) + ' ms',
987
+ 'Data Saver': network.saveData ? 'Enabled' : 'Disabled',
988
+ });
989
+ }
990
+
991
+ addHorizontalLine();
992
+ }
993
+
994
+ // Errors Section
995
+ if (debugData.errors && debugData.errors.length > 0) {
996
+ addSectionHeader('Error Log');
997
+
998
+ debugData.errors.forEach((error: LogEntry, index: number) => {
999
+ doc.setTextColor('#DC2626');
1000
+ doc.setFontSize(10);
1001
+ doc.setFont('helvetica', 'bold');
1002
+ doc.text(`Error ${index + 1}:`, margin, yPos);
1003
+ yPos += lineHeight;
1004
+
1005
+ doc.setFont('helvetica', 'normal');
1006
+ doc.setTextColor('#6B7280');
1007
+ addKeyValue('Message', error.message, 10);
1008
+
1009
+ if (error.stack) {
1010
+ addKeyValue('Stack', error.stack, 10);
1011
+ }
1012
+
1013
+ if (error.source) {
1014
+ addKeyValue('Source', error.source, 10);
1015
+ }
1016
+
1017
+ yPos += lineHeight;
1018
+ });
1019
+ }
1020
+
1021
+ // Add footers to all pages at the end
1022
+ addFooters();
1023
+
1024
+ // Save the PDF
1025
+ doc.save(`bolt-debug-info-${new Date().toISOString()}.pdf`);
1026
+ toast.success('Debug information exported as PDF');
1027
+ } catch (error) {
1028
+ console.error('Failed to export PDF:', error);
1029
+ toast.error('Failed to export debug information as PDF');
1030
+ }
1031
+ };
1032
+
1033
+ const exportAsText = () => {
1034
+ try {
1035
+ const debugData = {
1036
+ system: systemInfo,
1037
+ webApp: webAppInfo,
1038
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
1039
+ performance: {
1040
+ memory: (performance as any).memory || {},
1041
+ timing: performance.timing,
1042
+ navigation: performance.navigation,
1043
+ },
1044
+ };
1045
+
1046
+ const textContent = Object.entries(debugData)
1047
+ .map(([category, data]) => {
1048
+ return `${category.toUpperCase()}\n${'-'.repeat(30)}\n${JSON.stringify(data, null, 2)}\n\n`;
1049
+ })
1050
+ .join('\n');
1051
+
1052
+ const blob = new Blob([textContent], { type: 'text/plain' });
1053
+ const url = window.URL.createObjectURL(blob);
1054
+ const a = document.createElement('a');
1055
+ a.href = url;
1056
+ a.download = `bolt-debug-info-${new Date().toISOString()}.txt`;
1057
+ document.body.appendChild(a);
1058
+ a.click();
1059
+ window.URL.revokeObjectURL(url);
1060
+ document.body.removeChild(a);
1061
+ toast.success('Debug information exported as text file');
1062
+ } catch (error) {
1063
+ console.error('Failed to export text file:', error);
1064
+ toast.error('Failed to export debug information as text file');
1065
+ }
1066
+ };
1067
+
1068
+ const exportFormats: ExportFormat[] = [
1069
+ {
1070
+ id: 'json',
1071
+ label: 'Export as JSON',
1072
+ icon: 'i-ph:file-json',
1073
+ handler: exportDebugInfo,
1074
+ },
1075
+ {
1076
+ id: 'csv',
1077
+ label: 'Export as CSV',
1078
+ icon: 'i-ph:file-csv',
1079
+ handler: exportAsCSV,
1080
+ },
1081
+ {
1082
+ id: 'pdf',
1083
+ label: 'Export as PDF',
1084
+ icon: 'i-ph:file-pdf',
1085
+ handler: exportAsPDF,
1086
+ },
1087
+ {
1088
+ id: 'txt',
1089
+ label: 'Export as Text',
1090
+ icon: 'i-ph:file-text',
1091
+ handler: exportAsText,
1092
+ },
1093
+ ];
1094
+
1095
  // Add Ollama health check function
1096
  const checkOllamaHealth = async () => {
1097
  try {
 
1128
  return () => clearInterval(interval);
1129
  }, []);
1130
 
1131
+ // Replace the existing export button with this new component
1132
+ const ExportButton = () => {
1133
+ const [isOpen, setIsOpen] = useState(false);
1134
+
1135
+ const handleOpenChange = useCallback((open: boolean) => {
1136
+ setIsOpen(open);
1137
+ }, []);
1138
+
1139
+ const handleFormatClick = useCallback((handler: () => void) => {
1140
+ handler();
1141
+ setIsOpen(false);
1142
+ }, []);
1143
+
1144
+ return (
1145
+ <DialogRoot open={isOpen} onOpenChange={handleOpenChange}>
1146
+ <button
1147
+ onClick={() => setIsOpen(true)}
1148
+ className={classNames(
1149
+ 'group flex items-center gap-2',
1150
+ 'rounded-lg px-3 py-1.5',
1151
+ 'text-sm text-gray-900 dark:text-white',
1152
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
1153
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1154
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
1155
+ 'transition-all duration-200',
1156
+ )}
1157
+ >
1158
+ <span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
1159
+ Export
1160
+ </button>
1161
+
1162
+ <Dialog showCloseButton>
1163
+ <div className="p-6">
1164
+ <DialogTitle className="flex items-center gap-2">
1165
+ <div className="i-ph:download w-5 h-5" />
1166
+ Export Debug Information
1167
+ </DialogTitle>
1168
+
1169
+ <div className="mt-4 flex flex-col gap-2">
1170
+ {exportFormats.map((format) => (
1171
+ <button
1172
+ key={format.id}
1173
+ onClick={() => handleFormatClick(format.handler)}
1174
+ className={classNames(
1175
+ 'flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-colors w-full text-left',
1176
+ 'bg-white dark:bg-[#0A0A0A]',
1177
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1178
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
1179
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
1180
+ 'text-bolt-elements-textPrimary',
1181
+ )}
1182
+ >
1183
+ <div className={classNames(format.icon, 'w-5 h-5')} />
1184
+ <div>
1185
+ <div className="font-medium">{format.label}</div>
1186
+ <div className="text-xs text-bolt-elements-textSecondary mt-0.5">
1187
+ {format.id === 'json' && 'Export as a structured JSON file'}
1188
+ {format.id === 'csv' && 'Export as a CSV spreadsheet'}
1189
+ {format.id === 'pdf' && 'Export as a formatted PDF document'}
1190
+ {format.id === 'txt' && 'Export as a formatted text file'}
1191
+ </div>
1192
+ </div>
1193
+ </button>
1194
+ ))}
1195
+ </div>
1196
+ </div>
1197
+ </Dialog>
1198
+ </DialogRoot>
1199
+ );
1200
+ };
1201
+
1202
  return (
1203
  <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
1204
  {/* Quick Stats Banner */}
 
1342
  Fetch WebApp Info
1343
  </button>
1344
 
1345
+ <ExportButton />
 
 
 
 
 
 
 
 
 
 
 
 
 
1346
  </div>
1347
 
1348
  {/* System Information */}
app/components/@settings/tabs/event-logs/EventLogsTab.tsx CHANGED
@@ -5,6 +5,9 @@ import { logStore, type LogEntry } from '~/lib/stores/logs';
5
  import { useStore } from '@nanostores/react';
6
  import { classNames } from '~/utils/classNames';
7
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
 
 
 
8
 
9
  interface SelectOption {
10
  value: string;
@@ -252,6 +255,13 @@ const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp
252
  );
253
  };
254
 
 
 
 
 
 
 
 
255
  export function EventLogsTab() {
256
  const logs = useStore(logStore.logs);
257
  const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all');
@@ -329,51 +339,6 @@ export function EventLogsTab() {
329
  return () => clearTimeout(timeoutId);
330
  }, [searchQuery, filteredLogs.length]);
331
 
332
- // Enhanced export logs handler
333
- const handleExportLogs = useCallback(() => {
334
- const startTime = performance.now();
335
-
336
- try {
337
- const exportData = {
338
- timestamp: new Date().toISOString(),
339
- logs: filteredLogs,
340
- filters: {
341
- level: selectedLevel,
342
- searchQuery,
343
- },
344
- };
345
-
346
- const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
347
- const url = URL.createObjectURL(blob);
348
- const a = document.createElement('a');
349
- a.href = url;
350
- a.download = `bolt-logs-${new Date().toISOString()}.json`;
351
- document.body.appendChild(a);
352
- a.click();
353
- document.body.removeChild(a);
354
- URL.revokeObjectURL(url);
355
-
356
- const duration = performance.now() - startTime;
357
- logStore.logSuccess('Logs exported successfully', {
358
- type: 'export',
359
- message: `Successfully exported ${filteredLogs.length} logs`,
360
- component: 'EventLogsTab',
361
- exportedCount: filteredLogs.length,
362
- filters: {
363
- level: selectedLevel,
364
- searchQuery,
365
- },
366
- duration,
367
- });
368
- } catch (error) {
369
- logStore.logError('Failed to export logs', error, {
370
- type: 'export_error',
371
- message: 'Failed to export logs',
372
- component: 'EventLogsTab',
373
- });
374
- }
375
- }, [filteredLogs, selectedLevel, searchQuery]);
376
-
377
  // Enhanced refresh handler
378
  const handleRefresh = useCallback(async () => {
379
  const startTime = performance.now();
@@ -442,6 +407,455 @@ export function EventLogsTab() {
442
 
443
  const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  return (
446
  <div className="flex h-full flex-col gap-6">
447
  <div className="flex items-center justify-between">
@@ -540,21 +954,7 @@ export function EventLogsTab() {
540
  Refresh
541
  </button>
542
 
543
- <button
544
- onClick={handleExportLogs}
545
- className={classNames(
546
- 'group flex items-center gap-2',
547
- 'rounded-lg px-3 py-1.5',
548
- 'text-sm text-gray-900 dark:text-white',
549
- 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
550
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
551
- 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
552
- 'transition-all duration-200',
553
- )}
554
- >
555
- <span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
556
- Export
557
- </button>
558
  </div>
559
  </div>
560
 
 
5
  import { useStore } from '@nanostores/react';
6
  import { classNames } from '~/utils/classNames';
7
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
8
+ import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
9
+ import { jsPDF } from 'jspdf';
10
+ import { toast } from 'react-toastify';
11
 
12
  interface SelectOption {
13
  value: string;
 
255
  );
256
  };
257
 
258
+ interface ExportFormat {
259
+ id: string;
260
+ label: string;
261
+ icon: string;
262
+ handler: () => void;
263
+ }
264
+
265
  export function EventLogsTab() {
266
  const logs = useStore(logStore.logs);
267
  const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all');
 
339
  return () => clearTimeout(timeoutId);
340
  }, [searchQuery, filteredLogs.length]);
341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  // Enhanced refresh handler
343
  const handleRefresh = useCallback(async () => {
344
  const startTime = performance.now();
 
407
 
408
  const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
409
 
410
+ // Export functions
411
+ const exportAsJSON = () => {
412
+ try {
413
+ const exportData = {
414
+ timestamp: new Date().toISOString(),
415
+ logs: filteredLogs,
416
+ filters: {
417
+ level: selectedLevel,
418
+ searchQuery,
419
+ },
420
+ preferences: {
421
+ use24Hour,
422
+ showTimestamps,
423
+ autoExpand,
424
+ },
425
+ };
426
+
427
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
428
+ const url = window.URL.createObjectURL(blob);
429
+ const a = document.createElement('a');
430
+ a.href = url;
431
+ a.download = `bolt-event-logs-${new Date().toISOString()}.json`;
432
+ document.body.appendChild(a);
433
+ a.click();
434
+ window.URL.revokeObjectURL(url);
435
+ document.body.removeChild(a);
436
+ toast.success('Event logs exported successfully as JSON');
437
+ } catch (error) {
438
+ console.error('Failed to export JSON:', error);
439
+ toast.error('Failed to export event logs as JSON');
440
+ }
441
+ };
442
+
443
+ const exportAsCSV = () => {
444
+ try {
445
+ // Convert logs to CSV format
446
+ const headers = ['Timestamp', 'Level', 'Category', 'Message', 'Details'];
447
+ const csvData = [
448
+ headers,
449
+ ...filteredLogs.map((log) => [
450
+ new Date(log.timestamp).toISOString(),
451
+ log.level,
452
+ log.category || '',
453
+ log.message,
454
+ log.details ? JSON.stringify(log.details) : '',
455
+ ]),
456
+ ];
457
+
458
+ const csvContent = csvData
459
+ .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
460
+ .join('\n');
461
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
462
+ const url = window.URL.createObjectURL(blob);
463
+ const a = document.createElement('a');
464
+ a.href = url;
465
+ a.download = `bolt-event-logs-${new Date().toISOString()}.csv`;
466
+ document.body.appendChild(a);
467
+ a.click();
468
+ window.URL.revokeObjectURL(url);
469
+ document.body.removeChild(a);
470
+ toast.success('Event logs exported successfully as CSV');
471
+ } catch (error) {
472
+ console.error('Failed to export CSV:', error);
473
+ toast.error('Failed to export event logs as CSV');
474
+ }
475
+ };
476
+
477
+ const exportAsPDF = () => {
478
+ try {
479
+ // Create new PDF document
480
+ const doc = new jsPDF();
481
+ const lineHeight = 7;
482
+ let yPos = 20;
483
+ const margin = 20;
484
+ const pageWidth = doc.internal.pageSize.getWidth();
485
+ const maxLineWidth = pageWidth - 2 * margin;
486
+
487
+ // Helper function to add section header
488
+ const addSectionHeader = (title: string) => {
489
+ // Check if we need a new page
490
+ if (yPos > doc.internal.pageSize.getHeight() - 30) {
491
+ doc.addPage();
492
+ yPos = margin;
493
+ }
494
+
495
+ doc.setFillColor('#F3F4F6');
496
+ doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F');
497
+ doc.setFont('helvetica', 'bold');
498
+ doc.setTextColor('#111827');
499
+ doc.setFontSize(12);
500
+ doc.text(title.toUpperCase(), margin, yPos);
501
+ yPos += lineHeight * 2;
502
+ };
503
+
504
+ // Add title and header
505
+ doc.setFillColor('#6366F1');
506
+ doc.rect(0, 0, pageWidth, 50, 'F');
507
+ doc.setTextColor('#FFFFFF');
508
+ doc.setFontSize(24);
509
+ doc.setFont('helvetica', 'bold');
510
+ doc.text('Event Logs Report', margin, 35);
511
+
512
+ // Add subtitle with bolt.diy
513
+ doc.setFontSize(12);
514
+ doc.setFont('helvetica', 'normal');
515
+ doc.text('bolt.diy - AI Development Platform', margin, 45);
516
+ yPos = 70;
517
+
518
+ // Add report summary section
519
+ addSectionHeader('Report Summary');
520
+
521
+ doc.setFontSize(10);
522
+ doc.setFont('helvetica', 'normal');
523
+ doc.setTextColor('#374151');
524
+
525
+ const summaryItems = [
526
+ { label: 'Generated', value: new Date().toLocaleString() },
527
+ { label: 'Total Logs', value: filteredLogs.length.toString() },
528
+ { label: 'Filter Applied', value: selectedLevel === 'all' ? 'All Types' : selectedLevel },
529
+ { label: 'Search Query', value: searchQuery || 'None' },
530
+ { label: 'Time Format', value: use24Hour ? '24-hour' : '12-hour' },
531
+ ];
532
+
533
+ summaryItems.forEach((item) => {
534
+ doc.setFont('helvetica', 'bold');
535
+ doc.text(`${item.label}:`, margin, yPos);
536
+ doc.setFont('helvetica', 'normal');
537
+ doc.text(item.value, margin + 60, yPos);
538
+ yPos += lineHeight;
539
+ });
540
+
541
+ yPos += lineHeight * 2;
542
+
543
+ // Add statistics section
544
+ addSectionHeader('Log Statistics');
545
+
546
+ // Calculate statistics
547
+ const stats = {
548
+ error: filteredLogs.filter((log) => log.level === 'error').length,
549
+ warning: filteredLogs.filter((log) => log.level === 'warning').length,
550
+ info: filteredLogs.filter((log) => log.level === 'info').length,
551
+ debug: filteredLogs.filter((log) => log.level === 'debug').length,
552
+ provider: filteredLogs.filter((log) => log.category === 'provider').length,
553
+ api: filteredLogs.filter((log) => log.category === 'api').length,
554
+ };
555
+
556
+ // Create two columns for statistics
557
+ const leftStats = [
558
+ { label: 'Error Logs', value: stats.error, color: '#DC2626' },
559
+ { label: 'Warning Logs', value: stats.warning, color: '#F59E0B' },
560
+ { label: 'Info Logs', value: stats.info, color: '#3B82F6' },
561
+ ];
562
+
563
+ const rightStats = [
564
+ { label: 'Debug Logs', value: stats.debug, color: '#6B7280' },
565
+ { label: 'LLM Logs', value: stats.provider, color: '#10B981' },
566
+ { label: 'API Logs', value: stats.api, color: '#3B82F6' },
567
+ ];
568
+
569
+ const colWidth = (pageWidth - 2 * margin) / 2;
570
+
571
+ // Draw statistics in two columns
572
+ leftStats.forEach((stat, index) => {
573
+ doc.setTextColor(stat.color);
574
+ doc.setFont('helvetica', 'bold');
575
+ doc.text(stat.value.toString(), margin, yPos);
576
+ doc.setTextColor('#374151');
577
+ doc.setFont('helvetica', 'normal');
578
+ doc.text(stat.label, margin + 20, yPos);
579
+
580
+ if (rightStats[index]) {
581
+ doc.setTextColor(rightStats[index].color);
582
+ doc.setFont('helvetica', 'bold');
583
+ doc.text(rightStats[index].value.toString(), margin + colWidth, yPos);
584
+ doc.setTextColor('#374151');
585
+ doc.setFont('helvetica', 'normal');
586
+ doc.text(rightStats[index].label, margin + colWidth + 20, yPos);
587
+ }
588
+
589
+ yPos += lineHeight;
590
+ });
591
+
592
+ yPos += lineHeight * 2;
593
+
594
+ // Add logs section
595
+ addSectionHeader('Event Logs');
596
+
597
+ // Helper function to add a log entry with improved formatting
598
+ const addLogEntry = (log: LogEntry) => {
599
+ const entryHeight = 20 + (log.details ? 40 : 0); // Estimate entry height
600
+
601
+ // Check if we need a new page
602
+ if (yPos + entryHeight > doc.internal.pageSize.getHeight() - 20) {
603
+ doc.addPage();
604
+ yPos = margin;
605
+ }
606
+
607
+ // Add timestamp and level
608
+ const timestamp = new Date(log.timestamp).toLocaleString(undefined, {
609
+ year: 'numeric',
610
+ month: '2-digit',
611
+ day: '2-digit',
612
+ hour: '2-digit',
613
+ minute: '2-digit',
614
+ second: '2-digit',
615
+ hour12: !use24Hour,
616
+ });
617
+
618
+ // Draw log level badge background
619
+ const levelColors: Record<string, string> = {
620
+ error: '#FEE2E2',
621
+ warning: '#FEF3C7',
622
+ info: '#DBEAFE',
623
+ debug: '#F3F4F6',
624
+ };
625
+
626
+ const textColors: Record<string, string> = {
627
+ error: '#DC2626',
628
+ warning: '#F59E0B',
629
+ info: '#3B82F6',
630
+ debug: '#6B7280',
631
+ };
632
+
633
+ const levelWidth = doc.getTextWidth(log.level.toUpperCase()) + 10;
634
+ doc.setFillColor(levelColors[log.level] || '#F3F4F6');
635
+ doc.roundedRect(margin, yPos - 4, levelWidth, lineHeight + 4, 1, 1, 'F');
636
+
637
+ // Add log level text
638
+ doc.setTextColor(textColors[log.level] || '#6B7280');
639
+ doc.setFont('helvetica', 'bold');
640
+ doc.setFontSize(8);
641
+ doc.text(log.level.toUpperCase(), margin + 5, yPos);
642
+
643
+ // Add timestamp
644
+ doc.setTextColor('#6B7280');
645
+ doc.setFont('helvetica', 'normal');
646
+ doc.setFontSize(9);
647
+ doc.text(timestamp, margin + levelWidth + 10, yPos);
648
+
649
+ // Add category if present
650
+ if (log.category) {
651
+ const categoryX = margin + levelWidth + doc.getTextWidth(timestamp) + 20;
652
+ doc.setFillColor('#F3F4F6');
653
+
654
+ const categoryWidth = doc.getTextWidth(log.category) + 10;
655
+ doc.roundedRect(categoryX, yPos - 4, categoryWidth, lineHeight + 4, 2, 2, 'F');
656
+ doc.setTextColor('#6B7280');
657
+ doc.text(log.category, categoryX + 5, yPos);
658
+ }
659
+
660
+ yPos += lineHeight * 1.5;
661
+
662
+ // Add message
663
+ doc.setTextColor('#111827');
664
+ doc.setFontSize(10);
665
+
666
+ const messageLines = doc.splitTextToSize(log.message, maxLineWidth - 10);
667
+ doc.text(messageLines, margin + 5, yPos);
668
+ yPos += messageLines.length * lineHeight;
669
+
670
+ // Add details if present
671
+ if (log.details) {
672
+ doc.setTextColor('#6B7280');
673
+ doc.setFontSize(8);
674
+
675
+ const detailsStr = JSON.stringify(log.details, null, 2);
676
+ const detailsLines = doc.splitTextToSize(detailsStr, maxLineWidth - 15);
677
+
678
+ // Add details background
679
+ doc.setFillColor('#F9FAFB');
680
+ doc.roundedRect(margin + 5, yPos - 2, maxLineWidth - 10, detailsLines.length * lineHeight + 8, 1, 1, 'F');
681
+
682
+ doc.text(detailsLines, margin + 10, yPos + 4);
683
+ yPos += detailsLines.length * lineHeight + 10;
684
+ }
685
+
686
+ // Add separator line
687
+ doc.setDrawColor('#E5E7EB');
688
+ doc.setLineWidth(0.1);
689
+ doc.line(margin, yPos, pageWidth - margin, yPos);
690
+ yPos += lineHeight * 1.5;
691
+ };
692
+
693
+ // Add all logs
694
+ filteredLogs.forEach((log) => {
695
+ addLogEntry(log);
696
+ });
697
+
698
+ // Add footer to all pages
699
+ const totalPages = doc.internal.pages.length - 1;
700
+
701
+ for (let i = 1; i <= totalPages; i++) {
702
+ doc.setPage(i);
703
+ doc.setFontSize(8);
704
+ doc.setTextColor('#9CA3AF');
705
+
706
+ // Add page numbers
707
+ doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, {
708
+ align: 'center',
709
+ });
710
+
711
+ // Add footer text
712
+ doc.text('Generated by bolt.diy', margin, doc.internal.pageSize.getHeight() - 10);
713
+
714
+ const dateStr = new Date().toLocaleDateString();
715
+ doc.text(dateStr, pageWidth - margin, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
716
+ }
717
+
718
+ // Save the PDF
719
+ doc.save(`bolt-event-logs-${new Date().toISOString()}.pdf`);
720
+ toast.success('Event logs exported successfully as PDF');
721
+ } catch (error) {
722
+ console.error('Failed to export PDF:', error);
723
+ toast.error('Failed to export event logs as PDF');
724
+ }
725
+ };
726
+
727
+ const exportAsText = () => {
728
+ try {
729
+ const textContent = filteredLogs
730
+ .map((log) => {
731
+ const timestamp = new Date(log.timestamp).toLocaleString();
732
+ let content = `[${timestamp}] ${log.level.toUpperCase()}: ${log.message}\n`;
733
+
734
+ if (log.category) {
735
+ content += `Category: ${log.category}\n`;
736
+ }
737
+
738
+ if (log.details) {
739
+ content += `Details:\n${JSON.stringify(log.details, null, 2)}\n`;
740
+ }
741
+
742
+ return content + '-'.repeat(80) + '\n';
743
+ })
744
+ .join('\n');
745
+
746
+ const blob = new Blob([textContent], { type: 'text/plain' });
747
+ const url = window.URL.createObjectURL(blob);
748
+ const a = document.createElement('a');
749
+ a.href = url;
750
+ a.download = `bolt-event-logs-${new Date().toISOString()}.txt`;
751
+ document.body.appendChild(a);
752
+ a.click();
753
+ window.URL.revokeObjectURL(url);
754
+ document.body.removeChild(a);
755
+ toast.success('Event logs exported successfully as text file');
756
+ } catch (error) {
757
+ console.error('Failed to export text file:', error);
758
+ toast.error('Failed to export event logs as text file');
759
+ }
760
+ };
761
+
762
+ const exportFormats: ExportFormat[] = [
763
+ {
764
+ id: 'json',
765
+ label: 'Export as JSON',
766
+ icon: 'i-ph:file-json',
767
+ handler: exportAsJSON,
768
+ },
769
+ {
770
+ id: 'csv',
771
+ label: 'Export as CSV',
772
+ icon: 'i-ph:file-csv',
773
+ handler: exportAsCSV,
774
+ },
775
+ {
776
+ id: 'pdf',
777
+ label: 'Export as PDF',
778
+ icon: 'i-ph:file-pdf',
779
+ handler: exportAsPDF,
780
+ },
781
+ {
782
+ id: 'txt',
783
+ label: 'Export as Text',
784
+ icon: 'i-ph:file-text',
785
+ handler: exportAsText,
786
+ },
787
+ ];
788
+
789
+ const ExportButton = () => {
790
+ const [isOpen, setIsOpen] = useState(false);
791
+
792
+ const handleOpenChange = useCallback((open: boolean) => {
793
+ setIsOpen(open);
794
+ }, []);
795
+
796
+ const handleFormatClick = useCallback((handler: () => void) => {
797
+ handler();
798
+ setIsOpen(false);
799
+ }, []);
800
+
801
+ return (
802
+ <DialogRoot open={isOpen} onOpenChange={handleOpenChange}>
803
+ <button
804
+ onClick={() => setIsOpen(true)}
805
+ className={classNames(
806
+ 'group flex items-center gap-2',
807
+ 'rounded-lg px-3 py-1.5',
808
+ 'text-sm text-gray-900 dark:text-white',
809
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
810
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
811
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
812
+ 'transition-all duration-200',
813
+ )}
814
+ >
815
+ <span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
816
+ Export
817
+ </button>
818
+
819
+ <Dialog showCloseButton>
820
+ <div className="p-6">
821
+ <DialogTitle className="flex items-center gap-2">
822
+ <div className="i-ph:download w-5 h-5" />
823
+ Export Event Logs
824
+ </DialogTitle>
825
+
826
+ <div className="mt-4 flex flex-col gap-2">
827
+ {exportFormats.map((format) => (
828
+ <button
829
+ key={format.id}
830
+ onClick={() => handleFormatClick(format.handler)}
831
+ className={classNames(
832
+ 'flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-colors w-full text-left',
833
+ 'bg-white dark:bg-[#0A0A0A]',
834
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
835
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
836
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
837
+ 'text-bolt-elements-textPrimary',
838
+ )}
839
+ >
840
+ <div className={classNames(format.icon, 'w-5 h-5')} />
841
+ <div>
842
+ <div className="font-medium">{format.label}</div>
843
+ <div className="text-xs text-bolt-elements-textSecondary mt-0.5">
844
+ {format.id === 'json' && 'Export as a structured JSON file'}
845
+ {format.id === 'csv' && 'Export as a CSV spreadsheet'}
846
+ {format.id === 'pdf' && 'Export as a formatted PDF document'}
847
+ {format.id === 'txt' && 'Export as a formatted text file'}
848
+ </div>
849
+ </div>
850
+ </button>
851
+ ))}
852
+ </div>
853
+ </div>
854
+ </Dialog>
855
+ </DialogRoot>
856
+ );
857
+ };
858
+
859
  return (
860
  <div className="flex h-full flex-col gap-6">
861
  <div className="flex items-center justify-between">
 
954
  Refresh
955
  </button>
956
 
957
+ <ExportButton />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
  </div>
959
  </div>
960
 
package.json CHANGED
@@ -101,6 +101,7 @@
101
  "istextorbinary": "^9.5.0",
102
  "jose": "^5.9.6",
103
  "js-cookie": "^3.0.5",
 
104
  "jszip": "^3.10.1",
105
  "nanostores": "^0.10.3",
106
  "next": "^15.1.5",
 
101
  "istextorbinary": "^9.5.0",
102
  "jose": "^5.9.6",
103
  "js-cookie": "^3.0.5",
104
+ "jspdf": "^2.5.2",
105
  "jszip": "^3.10.1",
106
  "nanostores": "^0.10.3",
107
  "next": "^15.1.5",
pnpm-lock.yaml CHANGED
@@ -221,6 +221,9 @@ importers:
221
  js-cookie:
222
  specifier: ^3.0.5
223
  version: 3.0.5
 
 
 
224
  jszip:
225
  specifier: ^3.10.1
226
  version: 3.10.1
@@ -2841,6 +2844,9 @@ packages:
2841
  '@types/[email protected]':
2842
  resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
2843
 
 
 
 
2844
  '@types/[email protected]':
2845
  resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
2846
 
@@ -3167,6 +3173,11 @@ packages:
3167
3168
  resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
3169
 
 
 
 
 
 
3170
3171
  resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
3172
  engines: {node: '>= 0.4'}
@@ -3177,6 +3188,10 @@ packages:
3177
3178
  resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
3179
 
 
 
 
 
3180
3181
  resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
3182
 
@@ -3254,6 +3269,11 @@ packages:
3254
  engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
3255
  hasBin: true
3256
 
 
 
 
 
 
3257
3258
  resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
3259
 
@@ -3310,6 +3330,10 @@ packages:
3310
3311
  resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==}
3312
 
 
 
 
 
3313
3314
  resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
3315
 
@@ -3469,6 +3493,9 @@ packages:
3469
  resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
3470
  engines: {node: '>= 0.6'}
3471
 
 
 
 
3472
3473
  resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
3474
 
@@ -3503,6 +3530,9 @@ packages:
3503
3504
  resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
3505
 
 
 
 
3506
3507
  resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
3508
  engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -3641,6 +3671,9 @@ packages:
3641
  resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
3642
  engines: {node: '>=10'}
3643
 
 
 
 
3644
3645
  resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
3646
  engines: {node: '>=12'}
@@ -3956,6 +3989,9 @@ packages:
3956
  resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
3957
  engines: {node: ^12.20 || >= 14.13}
3958
 
 
 
 
3959
3960
  resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
3961
  engines: {node: '>=16.0.0'}
@@ -4199,6 +4235,10 @@ packages:
4199
4200
  resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
4201
 
 
 
 
 
4202
4203
  resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
4204
  engines: {node: '>= 0.8'}
@@ -4460,6 +4500,9 @@ packages:
4460
4461
  resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
4462
 
 
 
 
4463
4464
  resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
4465
 
@@ -5209,6 +5252,9 @@ packages:
5209
5210
  resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
5211
 
 
 
 
5212
5213
  resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
5214
 
@@ -5405,6 +5451,9 @@ packages:
5405
5406
  resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
5407
 
 
 
 
5408
5409
  resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
5410
 
@@ -5567,6 +5616,9 @@ packages:
5567
5568
  resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
5569
 
 
 
 
5570
5571
  resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
5572
 
@@ -5685,6 +5737,10 @@ packages:
5685
  resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
5686
  engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
5687
 
 
 
 
 
5688
5689
  resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
5690
 
@@ -5986,6 +6042,10 @@ packages:
5986
5987
  resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
5988
 
 
 
 
 
5989
5990
  resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
5991
 
@@ -6093,6 +6153,10 @@ packages:
6093
  resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
6094
  engines: {node: '>= 0.4'}
6095
 
 
 
 
 
6096
6097
  resolution: {integrity: sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==}
6098
  peerDependencies:
@@ -6131,6 +6195,9 @@ packages:
6131
  resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
6132
  engines: {node: '>=10'}
6133
 
 
 
 
6134
6135
  resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
6136
  engines: {node: '>=4'}
@@ -6395,6 +6462,9 @@ packages:
6395
  resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
6396
  engines: {node: '>= 0.4.0'}
6397
 
 
 
 
6398
6399
  resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
6400
  hasBin: true
@@ -9417,6 +9487,9 @@ snapshots:
9417
 
9418
  '@types/[email protected]': {}
9419
 
 
 
 
9420
  '@types/[email protected]':
9421
  dependencies:
9422
  '@types/react': 18.3.18
@@ -9897,6 +9970,8 @@ snapshots:
9897
 
9898
9899
 
 
 
9900
9901
  dependencies:
9902
  possible-typed-array-names: 1.0.0
@@ -9905,6 +9980,9 @@ snapshots:
9905
 
9906
9907
 
 
 
 
9908
9909
 
9910
@@ -10021,6 +10099,8 @@ snapshots:
10021
  node-releases: 2.0.19
10022
  update-browserslist-db: 1.1.2([email protected])
10023
 
 
 
10024
10025
 
10026
@@ -10083,6 +10163,18 @@ snapshots:
10083
 
10084
10085
 
 
 
 
 
 
 
 
 
 
 
 
 
10086
10087
  dependencies:
10088
  debug: 4.4.0
@@ -10218,6 +10310,9 @@ snapshots:
10218
 
10219
10220
 
 
 
 
10221
10222
 
10223
@@ -10273,6 +10368,11 @@ snapshots:
10273
  dependencies:
10274
  tiny-invariant: 1.3.3
10275
 
 
 
 
 
 
10276
10277
  dependencies:
10278
  mdn-data: 2.0.30
@@ -10378,6 +10478,9 @@ snapshots:
10378
 
10379
10380
 
 
 
 
10381
10382
 
10383
@@ -10821,6 +10924,8 @@ snapshots:
10821
  node-domexception: 1.0.0
10822
  web-streams-polyfill: 3.3.3
10823
 
 
 
10824
10825
  dependencies:
10826
  flat-cache: 4.0.1
@@ -11148,6 +11253,12 @@ snapshots:
11148
 
11149
11150
 
 
 
 
 
 
 
11151
11152
  dependencies:
11153
  depd: 2.0.0
@@ -11379,6 +11490,18 @@ snapshots:
11379
  optionalDependencies:
11380
  graceful-fs: 4.2.11
11381
 
 
 
 
 
 
 
 
 
 
 
 
 
11382
11383
  dependencies:
11384
  lie: 3.3.0
@@ -12550,6 +12673,9 @@ snapshots:
12550
 
12551
12552
 
 
 
 
12553
12554
  dependencies:
12555
  '@types/estree': 1.0.6
@@ -12728,6 +12854,11 @@ snapshots:
12728
 
12729
12730
 
 
 
 
 
 
12731
12732
  dependencies:
12733
  safe-buffer: 5.2.1
@@ -12910,6 +13041,9 @@ snapshots:
12910
  dependencies:
12911
  '@babel/runtime': 7.26.7
12912
 
 
 
 
12913
12914
 
12915
@@ -13045,6 +13179,9 @@ snapshots:
13045
 
13046
13047
 
 
 
 
13048
13049
  dependencies:
13050
  hash-base: 3.0.5
@@ -13394,6 +13531,9 @@ snapshots:
13394
 
13395
13396
 
 
 
 
13397
13398
  dependencies:
13399
  as-table: 1.0.55
@@ -13493,6 +13633,9 @@ snapshots:
13493
 
13494
13495
 
 
 
 
13496
13497
  dependencies:
13498
  dequal: 2.0.3
@@ -13542,6 +13685,11 @@ snapshots:
13542
  mkdirp: 1.0.4
13543
  yallist: 4.0.0
13544
 
 
 
 
 
 
13545
13546
  dependencies:
13547
  editions: 6.21.0
@@ -13829,6 +13977,11 @@ snapshots:
13829
 
13830
13831
 
 
 
 
 
 
13832
13833
 
13834
 
221
  js-cookie:
222
  specifier: ^3.0.5
223
  version: 3.0.5
224
+ jspdf:
225
+ specifier: ^2.5.2
226
+ version: 2.5.2
227
  jszip:
228
  specifier: ^3.10.1
229
  version: 3.10.1
 
2844
  '@types/[email protected]':
2845
  resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
2846
 
2847
+ '@types/[email protected]':
2848
+ resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
2849
+
2850
  '@types/[email protected]':
2851
  resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
2852
 
 
3173
3174
  resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
3175
 
3176
3177
+ resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
3178
+ engines: {node: '>= 4.5.0'}
3179
+ hasBin: true
3180
+
3181
3182
  resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
3183
  engines: {node: '>= 0.4'}
 
3188
3189
  resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
3190
 
3191
3192
+ resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
3193
+ engines: {node: '>= 0.6.0'}
3194
+
3195
3196
  resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
3197
 
 
3269
  engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
3270
  hasBin: true
3271
 
3272
3273
+ resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
3274
+ engines: {node: '>= 0.4.0'}
3275
+ hasBin: true
3276
+
3277
3278
  resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
3279
 
 
3330
3331
  resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==}
3332
 
3333
3334
+ resolution: {integrity: sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==}
3335
+ engines: {node: '>=10.0.0'}
3336
+
3337
3338
  resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
3339
 
 
3493
  resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
3494
  engines: {node: '>= 0.6'}
3495
 
3496
3497
+ resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==}
3498
+
3499
3500
  resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
3501
 
 
3530
3531
  resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
3532
 
3533
3534
+ resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
3535
+
3536
3537
  resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
3538
  engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
 
3671
  resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
3672
  engines: {node: '>=10'}
3673
 
3674
3675
+ resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==}
3676
+
3677
3678
  resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
3679
  engines: {node: '>=12'}
 
3989
  resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
3990
  engines: {node: ^12.20 || >= 14.13}
3991
 
3992
3993
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
3994
+
3995
3996
  resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
3997
  engines: {node: '>=16.0.0'}
 
4235
4236
  resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
4237
 
4238
4239
+ resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
4240
+ engines: {node: '>=8.0.0'}
4241
+
4242
4243
  resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
4244
  engines: {node: '>= 0.8'}
 
4500
4501
  resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
4502
 
4503
4504
+ resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==}
4505
+
4506
4507
  resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
4508
 
 
5252
5253
  resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
5254
 
5255
5256
+ resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
5257
+
5258
5259
  resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
5260
 
 
5451
5452
  resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
5453
 
5454
5455
+ resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
5456
+
5457
5458
  resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
5459
 
 
5616
5617
  resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
5618
 
5619
5620
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
5621
+
5622
5623
  resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
5624
 
 
5737
  resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
5738
  engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
5739
 
5740
5741
+ resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
5742
+ engines: {node: '>= 0.8.15'}
5743
+
5744
5745
  resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
5746
 
 
6042
6043
  resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
6044
 
6045
6046
+ resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
6047
+ engines: {node: '>=0.1.14'}
6048
+
6049
6050
  resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
6051
 
 
6153
  resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
6154
  engines: {node: '>= 0.4'}
6155
 
6156
6157
+ resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
6158
+ engines: {node: '>=12.0.0'}
6159
+
6160
6161
  resolution: {integrity: sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==}
6162
  peerDependencies:
 
6195
  resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
6196
  engines: {node: '>=10'}
6197
 
6198
6199
+ resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
6200
+
6201
6202
  resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
6203
  engines: {node: '>=4'}
 
6462
  resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
6463
  engines: {node: '>= 0.4.0'}
6464
 
6465
6466
+ resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
6467
+
6468
6469
  resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
6470
  hasBin: true
 
9487
 
9488
  '@types/[email protected]': {}
9489
 
9490
+ '@types/[email protected]':
9491
+ optional: true
9492
+
9493
  '@types/[email protected]':
9494
  dependencies:
9495
  '@types/react': 18.3.18
 
9970
 
9971
9972
 
9973
9974
+
9975
9976
  dependencies:
9977
  possible-typed-array-names: 1.0.0
 
9980
 
9981
9982
 
9983
9984
+ optional: true
9985
+
9986
9987
 
9988
 
10099
  node-releases: 2.0.19
10100
  update-browserslist-db: 1.1.2([email protected])
10101
 
10102
10103
+
10104
10105
 
10106
 
10163
 
10164
10165
 
10166
10167
+ dependencies:
10168
+ '@babel/runtime': 7.26.7
10169
+ '@types/raf': 3.4.3
10170
+ core-js: 3.40.0
10171
+ raf: 3.4.1
10172
+ regenerator-runtime: 0.13.11
10173
+ rgbcolor: 1.0.1
10174
+ stackblur-canvas: 2.7.0
10175
+ svg-pathdata: 6.0.3
10176
+ optional: true
10177
+
10178
10179
  dependencies:
10180
  debug: 4.4.0
 
10310
 
10311
10312
 
10313
10314
+ optional: true
10315
+
10316
10317
 
10318
 
10368
  dependencies:
10369
  tiny-invariant: 1.3.3
10370
 
10371
10372
+ dependencies:
10373
+ utrie: 1.0.2
10374
+ optional: true
10375
+
10376
10377
  dependencies:
10378
  mdn-data: 2.0.30
 
10478
 
10479
10480
 
10481
10482
+ optional: true
10483
+
10484
10485
 
10486
 
10924
  node-domexception: 1.0.0
10925
  web-streams-polyfill: 3.3.3
10926
 
10927
10928
+
10929
10930
  dependencies:
10931
  flat-cache: 4.0.1
 
11253
 
11254
11255
 
11256
11257
+ dependencies:
11258
+ css-line-break: 2.1.0
11259
+ text-segmentation: 1.0.3
11260
+ optional: true
11261
+
11262
11263
  dependencies:
11264
  depd: 2.0.0
 
11490
  optionalDependencies:
11491
  graceful-fs: 4.2.11
11492
 
11493
11494
+ dependencies:
11495
+ '@babel/runtime': 7.26.7
11496
+ atob: 2.1.2
11497
+ btoa: 1.2.1
11498
+ fflate: 0.8.2
11499
+ optionalDependencies:
11500
+ canvg: 3.0.10
11501
+ core-js: 3.40.0
11502
+ dompurify: 2.5.8
11503
+ html2canvas: 1.4.1
11504
+
11505
11506
  dependencies:
11507
  lie: 3.3.0
 
12673
 
12674
12675
 
12676
12677
+ optional: true
12678
+
12679
12680
  dependencies:
12681
  '@types/estree': 1.0.6
 
12854
 
12855
12856
 
12857
12858
+ dependencies:
12859
+ performance-now: 2.1.0
12860
+ optional: true
12861
+
12862
12863
  dependencies:
12864
  safe-buffer: 5.2.1
 
13041
  dependencies:
13042
  '@babel/runtime': 7.26.7
13043
 
13044
13045
+ optional: true
13046
+
13047
13048
 
13049
 
13179
 
13180
13181
 
13182
13183
+ optional: true
13184
+
13185
13186
  dependencies:
13187
  hash-base: 3.0.5
 
13531
 
13532
13533
 
13534
13535
+ optional: true
13536
+
13537
13538
  dependencies:
13539
  as-table: 1.0.55
 
13633
 
13634
13635
 
13636
13637
+ optional: true
13638
+
13639
13640
  dependencies:
13641
  dequal: 2.0.3
 
13685
  mkdirp: 1.0.4
13686
  yallist: 4.0.0
13687
 
13688
13689
+ dependencies:
13690
+ utrie: 1.0.2
13691
+ optional: true
13692
+
13693
13694
  dependencies:
13695
  editions: 6.21.0
 
13977
 
13978
13979
 
13980
13981
+ dependencies:
13982
+ base64-arraybuffer: 1.0.2
13983
+ optional: true
13984
+
13985
13986
 
13987