diff --git a/README.md b/README.md index 3071fcee02..24f9ba14f1 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ project, please check the [project management guide](./PROJECT.md) to get starte - ✅ Add Starter Template Options (@thecodacus) - ✅ Perplexity Integration (@meetpateltech) - ✅ AWS Bedrock Integration (@kunjabijukchhe) -- ✅ Add a "Diff View" to see the changes (@toddyclipsgg) +- ✅ Add "Diff View" to see the changes (@toddyclipsgg) +- ✅ Add files MD, DOCX, TXT and PDF (@toddyclipsgg) - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs) - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start) - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index b8712202ae..01cc101227 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -39,6 +39,12 @@ import { LOCAL_PROVIDERS } from '~/lib/stores/settings'; const TEXTAREA_MIN_HEIGHT = 76; +/* + * Flag to use only fallback method + * const USE_ONLY_FALLBACK = true; + */ +const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB + interface BaseChatProps { textareaRef?: React.RefObject | undefined; messageRef?: RefCallback | undefined; @@ -257,24 +263,209 @@ export const BaseChat = React.forwardRef( const handleFileUpload = () => { const input = document.createElement('input'); input.type = 'file'; - input.accept = 'image/*'; + input.accept = 'image/*,.txt,.md,.docx,.pdf'; + input.multiple = true; input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; + const selectedFiles = Array.from((e.target as HTMLInputElement).files || []); + processNewFiles(selectedFiles, 'upload'); + }; + input.click(); + }; - if (file) { - const reader = new FileReader(); + // Unified file processing function + const processNewFiles = (filesToProcess: File[], source: 'upload' | 'paste') => { + // Validate file types and sizes first + const filteredFiles = filesToProcess.filter((file) => { + // Block script files + if (file.name.match(/\.(sh|bat|ps1)$/i)) { + toast.error( +
+
Script files not allowed
+
+ For security reasons, script files (.sh, .bat, .ps1) are not supported. +
+
, + { autoClose: 5000 }, + ); + return false; + } - reader.onload = (e) => { - const base64Image = e.target?.result as string; - setUploadedFiles?.([...uploadedFiles, file]); - setImageDataList?.([...imageDataList, base64Image]); - }; - reader.readAsDataURL(file); + // Validate file size + if (file.size > MAX_FILE_SIZE) { + toast.warning(`File ${file.name} exceeds maximum size of 5MB and was ignored.`); + return false; } - }; - input.click(); + return true; + }); + + if (filteredFiles.length === 0) { + return; + } + + // Prepare new files array + const newUploadedFiles = [...uploadedFiles, ...filteredFiles]; + const newImageDataList = [ + ...imageDataList, + ...filteredFiles.map((file) => (file.type.startsWith('image/') ? 'loading-image' : 'non-image')), + ]; + + // Update state + setUploadedFiles?.(newUploadedFiles); + setImageDataList?.(newImageDataList); + + // Process individual files + filteredFiles.forEach((file, index) => { + const actualIndex = uploadedFiles.length + index; + processIndividualFiles(file, actualIndex, source); + }); + }; + + const processIndividualFiles = (file: File, index: number, _source: 'upload' | 'paste') => { + if (file.type.startsWith('image/')) { + processImageFile(file, index); + } else if (file.type.includes('text') || file.name.match(/\.(txt|md|pdf|docx)$/i)) { + previewTextFile(file, index); + } + }; + + // Rename and update processPastedFiles to use new unified function + const processPastedFiles = (filesToProcess: File[]) => { + processNewFiles(filesToProcess, 'paste'); + }; + + // Function to process image files + const processImageFile = (file: File, _index: number) => { + // Handle image files for display + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + + reader.onload = (e) => { + if (e.target && e.target.result && setImageDataList) { + setImageDataList([...imageDataList, e.target.result as string]); + } + }; + reader.readAsDataURL(file); + + toast.info( +
+
Image attached:
+
+ {file.name} ({Math.round(file.size / 1024)} KB) +
+
, + { autoClose: 3000 }, + ); + } else if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { + // Special handling for PDF files + const fileSize = Math.round(file.size / 1024); + const isLargePdf = fileSize > 5000; // 5MB threshold + + const toastId = toast.info( +
+
PDF attached:
+
+ {file.name} ({fileSize} KB){isLargePdf ? ' - Large file, processing may take longer' : ''} +
+
+
+
+
+
Extracting text...
+
+
, + { autoClose: false }, + ); + + // Process the PDF file asynchronously + import('~/utils/documentUtils').then(async ({ extractTextFromDocument }) => { + try { + await extractTextFromDocument(file); + + // Update toast with success message + toast.update(toastId, { + render: ( +
+
PDF processed successfully:
+
+ {file.name} ({fileSize} KB) +
+
Text extracted and ready to send
+
+ ), + autoClose: 3000, + type: 'success', + }); + } catch (error) { + console.error('Error processing PDF:', error); + + // Update toast with error message + toast.update(toastId, { + render: ( +
+
Error processing PDF:
+
+ {file.name} ({fileSize} KB) +
+
+ The file will be attached but text extraction had issues +
+
+ ), + autoClose: 5000, + type: 'error', + }); + } + }); + } + }; + + // Function to process text files and show preview + const previewTextFile = (file: File, _index: number) => { + // If it's a PDF or DOCX file, show a special preview + if ( + file.type === 'application/pdf' || + file.name.endsWith('.pdf') || + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + file.name.endsWith('.docx') + ) { + toast.info( +
+
Document file attached:
+
+
+
+
{file.name}
+
+ {Math.round(file.size / 1024)} KB - Text will be extracted when sending +
+
+
+
, + { autoClose: 4000 }, + ); + + return; + } + + // For other file types, maintain previous behavior + toast.info( +
+
File attached:
+
+ {file.name} ({Math.round(file.size / 1024)} KB) +
+
, + { autoClose: 3000 }, + ); }; const handlePaste = async (e: React.ClipboardEvent) => { @@ -284,24 +475,43 @@ export const BaseChat = React.forwardRef( return; } - for (const item of items) { - if (item.type.startsWith('image/')) { - e.preventDefault(); + // Check if there are files in the clipboard + const clipboardFiles: File[] = []; + for (const item of items) { + if (item.kind === 'file') { const file = item.getAsFile(); if (file) { - const reader = new FileReader(); - - reader.onload = (e) => { - const base64Image = e.target?.result as string; - setUploadedFiles?.([...uploadedFiles, file]); - setImageDataList?.([...imageDataList, base64Image]); - }; - reader.readAsDataURL(file); + clipboardFiles.push(file); } + } + } - break; + if (clipboardFiles && clipboardFiles.length > 0) { + // If there are PDF or DOCX files, check possible filters + if ( + clipboardFiles.some( + (file) => + file.type === 'application/pdf' || + file.name.endsWith('.pdf') || + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + file.name.endsWith('.docx'), + ) + ) { + // Filter large files + const filteredFiles = clipboardFiles.filter((file) => file.size <= MAX_FILE_SIZE); + + if (filteredFiles.length < clipboardFiles.length) { + toast.warning('Some files were ignored because they exceed the maximum size of 5MB.'); + + // Continue only with valid files + processPastedFiles(filteredFiles); + } else { + processPastedFiles(clipboardFiles); + } + } else { + processPastedFiles(clipboardFiles); } } }; @@ -464,29 +674,39 @@ export const BaseChat = React.forwardRef( }} onDragOver={(e) => { e.preventDefault(); - e.currentTarget.style.border = '2px solid #1488fc'; - }} - onDragLeave={(e) => { - e.preventDefault(); - e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)'; - }} - onDrop={(e) => { - e.preventDefault(); - e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)'; + e.stopPropagation(); const files = Array.from(e.dataTransfer.files); - files.forEach((file) => { - if (file.type.startsWith('image/')) { - const reader = new FileReader(); - - reader.onload = (e) => { - const base64Image = e.target?.result as string; - setUploadedFiles?.([...uploadedFiles, file]); - setImageDataList?.([...imageDataList, base64Image]); - }; - reader.readAsDataURL(file); - } - }); + + // Check if there are script files + const hasScripts = files.some((file) => file.name.match(/\.(sh|bat|ps1)$/i)); + + let filteredFiles = files; + + if (hasScripts) { + toast.error( +
+
Script files not allowed
+
+ For security reasons, script files (.sh, .bat, .ps1) are not supported. +
+
, + { autoClose: 5000 }, + ); + + // Remove script files + filteredFiles = filteredFiles.filter( + (file) => + !file.name.endsWith('.sh') && !file.name.endsWith('.bat') && !file.name.endsWith('.ps1'), + ); + } + + if (filteredFiles.length === 0) { + return; + } // If there were only unsupported files, cancel processing + + // Process valid files + processPastedFiles(filteredFiles); }} onKeyDown={(event) => { if (event.key === 'Enter') { @@ -542,9 +762,32 @@ export const BaseChat = React.forwardRef(
- handleFileUpload()}> -
-
+ + + handleFileUpload()} + > +
+
+
+ + +

Attach files

+
+

Supported formats:

+

• Images: png, jpg, jpeg, gif, etc.

+

• Text: txt, md, js, py, html, css, json, etc.

+

• Documents: pdf, docx

+
+ +
+
+
=> { + // For DOCX and PDF, use the specialized extractor + if ( + file.type === 'application/pdf' || + file.name.endsWith('.pdf') || + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + file.name.endsWith('.docx') + ) { + return extractTextFromDocument(file); + } + + // For other file types, use the standard method + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + resolve((e.target?.result as string) || ''); + }; + + reader.onerror = () => { + reject(new Error(`Error reading file ${file.name}`)); + }; + reader.readAsText(file); + }); + }; + + // Prepare array to store file reading promises + const fileReadPromises: Promise[] = []; + const fileNames: string[] = []; + const fileTypes: string[] = []; + + // Start reading text files + uploadedFiles + .filter((file) => !file.type.startsWith('image/')) + .forEach((file) => { + fileNames.push(file.name); + fileTypes.push(file.type); + fileReadPromises.push(readFileContent(file)); + }); + + // Wait for all files to be read + const fileContents = await Promise.all(fileReadPromises); + + // Build information about files with content + const textFilesInfo = fileContents + .map((content, index) => { + const file = uploadedFiles.find((f) => f.name === fileNames[index]); + const fileSizeKB = file ? Math.round(file.size / 1024) : 0; + const fileName = fileNames[index]; + + // For DOCX and PDF files, add information about the document type + const isDocFile = fileName.toLowerCase().endsWith('.docx') || fileName.toLowerCase().endsWith('.pdf'); + + const fileTypeLabel = fileName.toLowerCase().endsWith('.docx') + ? 'DOCX' + : fileName.toLowerCase().endsWith('.pdf') + ? 'PDF' + : ''; + + // For document files, use a special format + if (isDocFile) { + return ( + `[File attached: ${fileName} (${fileSizeKB} KB) - ${fileTypeLabel} document]\n\n` + + `Extracted text from ${fileName}:\n` + + `\`\`\`\n${content}\n\`\`\`` + ); + } + + // For markdown files (.md), send a special marker + if (fileName.toLowerCase().endsWith('.md')) { + return ( + `[File attached: ${fileName} (${fileSizeKB} KB)]\n\n` + + `Content of file ${fileName} (being sent to backend):\n` + + `\`\`\`\n[Markdown file content sent only to backend]\n\`\`\`\n\n` + + `\n` + + `Content of file ${fileName}:\n` + + `\`\`\`\n${content}\n\`\`\`\n` + + `` + ); + } + + // For text files, use standard format + return ( + `[File attached: ${fileName} (${fileSizeKB} KB)]\n\n` + + `Content of file ${fileName}:\n` + + `\`\`\`\n${content}\n\`\`\`` + ); + }) + .join('\n\n'); + + const contentWithFilesInfo = textFilesInfo ? `${textFilesInfo}\n\n${messageContent}` : messageContent; + if (!chatStarted) { setFakeLoading(true); if (autoSelectTemplate) { const { template, title } = await selectStarterTemplate({ - message: messageContent, + message: contentWithFilesInfo, model, provider, }); @@ -326,7 +420,7 @@ export const ChatImpl = memo( content: [ { type: 'text', - text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`, + text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${contentWithFilesInfo}`, }, ...imageDataList.map((imageData) => ({ type: 'image', @@ -371,12 +465,25 @@ export const ChatImpl = memo( content: [ { type: 'text', - text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`, + text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${contentWithFilesInfo}`, }, - ...imageDataList.map((imageData) => ({ - type: 'image', - image: imageData, - })), + ...imageDataList + .map((imageData, _index) => { + // If it's an image, send as image + if (imageData !== 'non-image' && imageData !== 'loading-image') { + return { + type: 'image', + image: imageData, + }; + } + + /* + * For non-image files, we don't include in the message content + * because the API doesn't support this format + */ + return null; + }) + .filter(Boolean), ] as any, }, ]); @@ -410,12 +517,25 @@ export const ChatImpl = memo( content: [ { type: 'text', - text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userUpdateArtifact}${messageContent}`, + text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userUpdateArtifact}${contentWithFilesInfo}`, }, - ...imageDataList.map((imageData) => ({ - type: 'image', - image: imageData, - })), + ...imageDataList + .map((imageData, _index) => { + // If it's an image, send as image + if (imageData !== 'non-image' && imageData !== 'loading-image') { + return { + type: 'image', + image: imageData, + }; + } + + /* + * For non-image files, we don't include in the message content + * because the API doesn't support this format + */ + return null; + }) + .filter(Boolean), ] as any, }); @@ -426,12 +546,25 @@ export const ChatImpl = memo( content: [ { type: 'text', - text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`, + text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${contentWithFilesInfo}`, }, - ...imageDataList.map((imageData) => ({ - type: 'image', - image: imageData, - })), + ...imageDataList + .map((imageData, _index) => { + // If it's an image, send as image + if (imageData !== 'non-image' && imageData !== 'loading-image') { + return { + type: 'image', + image: imageData, + }; + } + + /* + * For non-image files, we don't include in the message content + * because the API doesn't support this format + */ + return null; + }) + .filter(Boolean), ] as any, }); } diff --git a/app/components/chat/FilePreview.tsx b/app/components/chat/FilePreview.tsx index 0500d03b91..17dd89ddf7 100644 --- a/app/components/chat/FilePreview.tsx +++ b/app/components/chat/FilePreview.tsx @@ -1,4 +1,14 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { getDocument } from 'pdfjs-dist'; +import { GlobalWorkerOptions } from 'pdfjs-dist'; + +// Import the worker as a virtual URL from Vite (if not configured elsewhere) +const pdfjsWorkerUrl = new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url).href; + +// Configure the worker if not already configured +if (typeof window !== 'undefined' && !GlobalWorkerOptions.workerSrc) { + GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl; +} interface FilePreviewProps { files: File[]; @@ -6,26 +16,186 @@ interface FilePreviewProps { onRemove: (index: number) => void; } +interface PDFThumbnailData { + dataUrl: string; + pageCount: number; +} + const FilePreview: React.FC = ({ files, imageDataList, onRemove }) => { + const [pdfThumbnails, setPdfThumbnails] = useState>({}); + + useEffect(() => { + // Process PDF thumbnails + const processPdfThumbnails = async () => { + for (const file of files) { + // Check if it's a PDF and doesn't have a thumbnail yet + if ( + (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) && + !pdfThumbnails[file.name + file.lastModified] + ) { + try { + // Load the PDF and generate thumbnail of the first page + const arrayBuffer = await file.arrayBuffer(); + const loadingTask = getDocument({ data: arrayBuffer }); + const pdf = await loadingTask.promise; + const pageCount = pdf.numPages; + + // Render the first page as thumbnail + const page = await pdf.getPage(1); + // Reduced scale for smaller thumbnail + const viewport = page.getViewport({ scale: 0.3 }); + + // Create canvas for the thumbnail + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + if (context) { + // Render page on canvas + await page.render({ + canvasContext: context, + viewport, + }).promise; + + // Convert to dataURL + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); + + // Save thumbnail + setPdfThumbnails((prev) => ({ + ...prev, + [file.name + file.lastModified]: { + dataUrl, + pageCount, + }, + })); + } + } catch (error) { + console.error('Error generating PDF thumbnail:', error); + } + } + } + }; + + processPdfThumbnails(); + }, [files, pdfThumbnails]); + if (!files || files.length === 0) { return null; } + // Function to get the icon based on file type + const getFileIcon = (fileType: string) => { + if (fileType.startsWith('image/')) { + return 'i-ph:image'; + } + + const fileName = fileType.toLowerCase(); + + if (fileName.includes('pdf') || fileName.endsWith('.pdf')) { + return 'i-ph:file-pdf'; + } + + if (fileName.includes('docx') || fileName.endsWith('.docx')) { + return 'i-ph:file-doc'; + } + + if (fileName.includes('text') || fileName.includes('txt') || fileName.endsWith('.txt')) { + return 'i-ph:file-text'; + } + + if (fileName.endsWith('.md')) { + return 'i-ph:file-text'; + } + + return 'i-ph:file-text'; + }; + + // Function to check if a file is a PDF + const isPdf = (file: File) => { + return file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); + }; + + // Function to get a PDF thumbnail + const getPdfThumbnail = (file: File) => { + const key = file.name + file.lastModified; + return pdfThumbnails[key]; + }; + + // Function to format file size + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) { + return bytes + ' B'; + } + + if (bytes < 1024 * 1024) { + return (bytes / 1024).toFixed(1) + ' KB'; + } + + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }; + return ( -
+
{files.map((file, index) => ( -
- {imageDataList[index] && ( -
- {file.name} - -
- )} +
+
+ {imageDataList[index] === 'loading-image' ? ( + // Renders loading indicator for images in process +
+
+
Loading...
+
+ ) : imageDataList[index] && imageDataList[index] !== 'non-image' ? ( + // Renders image for already loaded image types +
+
+ {file.name} +
+
+ {file.name} +
+
{formatFileSize(file.size)}
+
+ ) : isPdf(file) && getPdfThumbnail(file) ? ( + // Renders PDF thumbnail +
+
+ {`${file.name} +
+ {getPdfThumbnail(file)?.pageCount || '?'} pgs +
+
+
+ {file.name} +
+
{formatFileSize(file.size)}
+
+ ) : ( + // Renders icon for other file types +
+
+
+ {file.name} +
+
{formatFileSize(file.size)}
+
+ )} + +
))}
diff --git a/app/components/chat/UserMessage.tsx b/app/components/chat/UserMessage.tsx index e7ef54a619..86f3756532 100644 --- a/app/components/chat/UserMessage.tsx +++ b/app/components/chat/UserMessage.tsx @@ -5,8 +5,30 @@ import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants'; import { Markdown } from './Markdown'; +// Function to determine the appropriate icon based on file extension +function getFileIcon(fileName: string) { + const extension = fileName.split('.').pop()?.toLowerCase() || ''; + + switch (extension) { + // Documents + case 'md': + case 'txt': + return 'i-ph:file-text text-amber-400'; + case 'docx': + return 'i-ph:file-doc text-blue-500'; + case 'pdf': + return 'i-ph:file-pdf text-red-500'; + + // Default for other types + default: + return 'i-ph:file-text text-bolt-elements-textSecondary'; + } +} + interface UserMessageProps { - content: string | Array<{ type: string; text?: string; image?: string }>; + content: + | string + | Array<{ type: string; text?: string; image?: string; file?: { name: string; type: string; size: number } }>; } export function UserMessage({ content }: UserMessageProps) { @@ -15,13 +37,88 @@ export function UserMessage({ content }: UserMessageProps) { const textContent = stripMetadata(textItem?.text || ''); const images = content.filter((item) => item.type === 'image' && item.image); + // Extract attached file information from content + const fileRegex = + /\[File attached: (.+?) \((\d+) KB\)(?:\s*-\s*([A-Z]+) document)?\]\n\nContent of file .+?:\n```\n([\s\S]*?)\n```/g; + const docFileRegex = + /\[File attached: (.+?) \((\d+) KB\) - ([A-Z]+) document\]\n\nExtracted text from .+?:\n```\n([\s\S]*?)\n```/g; + + let matches; + const textFiles = []; + let cleanedContent = textContent; + + // Find all occurrences of file information + while ((matches = fileRegex.exec(textContent)) !== null) { + const file = { + name: matches[1], + size: parseInt(matches[2]), + type: matches[3] || '', // Can be empty for normal files + }; + textFiles.push(file); + } + + // Find PDF/DOCX documents + while ((matches = docFileRegex.exec(textContent)) !== null) { + const file = { + name: matches[1], + size: parseInt(matches[2]), + type: matches[3], // DOCX or PDF + }; + textFiles.push(file); + } + + // Remove file information from main content - more aggressive to handle markdown + if (textFiles.length > 0) { + // Remove content between special markdown markers + cleanedContent = cleanedContent.replace( + /[\s\S]*?/g, + '', + ); + + // Remove normal file information + cleanedContent = cleanedContent.replace( + /\n*\[File attached: .+?\n\nContent of file .+?:\n```\n[\s\S]*?\n```\n*/gs, + '', + ); + + // Remove extracted document information + cleanedContent = cleanedContent.replace( + /\n*\[File attached: .+? - [A-Z]+ document\]\n\nExtracted text from .+?:\n```\n[\s\S]*?\n```\n*/gs, + '', + ); + } + return (
- {textContent && {textContent}} + {cleanedContent && cleanedContent.trim() !== '' && {cleanedContent}} + + {textFiles.length > 0 && ( +
+
+ {textFiles.map((file, index) => ( +
+
+
+
+
+ {file.name} + + {file.size} KB {file.type ? `- ${file.type}` : ''} + +
+
+ ))} +
+
+ )} + {images.map((item, index) => ( {`Image 0) { + // Remove content between special markdown markers + cleanedContent = cleanedContent.replace( + /[\s\S]*?/g, + '', + ); + + // Remove normal file information + cleanedContent = cleanedContent.replace( + /\n*\[File attached: .+?\n\nContent of file .+?:\n```\n[\s\S]*?\n```\n*/gs, + '', + ); + + // Remove extracted document information + cleanedContent = cleanedContent.replace( + /\n*\[File attached: .+? - [A-Z]+ document\]\n\nExtracted text from .+?:\n```\n[\s\S]*?\n```\n*/gs, + '', + ); + } + return (
- {textContent} +
+ {cleanedContent && cleanedContent.trim() !== '' && {cleanedContent}} + + {textFiles.length > 0 && ( +
+
+ {textFiles.map((file, index) => ( +
+
+
+
+
+ {file.name} + + {file.size} KB {file.type ? `- ${file.type}` : ''} + +
+
+ ))} +
+
+ )} +
); } diff --git a/app/components/workbench/DiffView.tsx b/app/components/workbench/DiffView.tsx index 146076c5ca..8b9b2cc1b4 100644 --- a/app/components/workbench/DiffView.tsx +++ b/app/components/workbench/DiffView.tsx @@ -395,12 +395,12 @@ const NoChangesView = memo( ), ); -// Otimização do processamento de diferenças com memoização +// Optimization of differences processing with memoization const useProcessChanges = (beforeCode: string, afterCode: string) => { return useMemo(() => processChanges(beforeCode, afterCode), [beforeCode, afterCode]); }; -// Componente otimizado para renderização de linhas de código +// Optimized component for rendering code lines const CodeLine = memo( ({ lineNumber, @@ -469,7 +469,7 @@ const CodeLine = memo( }, ); -// Componente para exibir informações sobre o arquivo +// Component to display file information const FileInfo = memo( ({ filename, @@ -641,13 +641,13 @@ export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => const existingHistory = fileHistory[selectedFile]; const currentContent = currentDocument.value; - // Normalizar o conteúdo para comparação + // Normalize content for comparison const normalizedCurrentContent = currentContent.replace(/\r\n/g, '\n').trim(); const normalizedOriginalContent = (existingHistory?.originalContent || file.content) .replace(/\r\n/g, '\n') .trim(); - // Se não há histórico existente, criar um novo apenas se houver diferenças + // If there's no existing history, create a new one only if there are differences if (!existingHistory) { if (normalizedCurrentContent !== normalizedOriginalContent) { const newChanges = diffLines(file.content, currentContent); @@ -671,22 +671,22 @@ export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => return; } - // Se já existe histórico, verificar se há mudanças reais desde a última versão + // If history already exists, check if there are real changes since the last version const lastVersion = existingHistory.versions[existingHistory.versions.length - 1]; const normalizedLastContent = lastVersion?.content.replace(/\r\n/g, '\n').trim(); if (normalizedCurrentContent === normalizedLastContent) { - return; // Não criar novo histórico se o conteúdo é o mesmo + return; // Don't create new history if the content is the same } - // Verificar se há mudanças significativas usando diffFiles + // Check for significant changes using diffFiles const relativePath = extractRelativePath(selectedFile); const unifiedDiff = diffFiles(relativePath, existingHistory.originalContent, currentContent); if (unifiedDiff) { const newChanges = diffLines(existingHistory.originalContent, currentContent); - // Verificar se as mudanças são significativas + // Check if changes are significant const hasSignificantChanges = newChanges.some( (change) => (change.added || change.removed) && change.value.trim().length > 0, ); @@ -695,14 +695,14 @@ export const DiffView = memo(({ fileHistory, setFileHistory }: DiffViewProps) => const newHistory: FileHistory = { originalContent: existingHistory.originalContent, lastModified: Date.now(), - changes: [...existingHistory.changes, ...newChanges].slice(-100), // Limitar histórico de mudanças + changes: [...existingHistory.changes, ...newChanges].slice(-100), // Limit change history versions: [ ...existingHistory.versions, { timestamp: Date.now(), content: currentContent, }, - ].slice(-10), // Manter apenas as 10 últimas versões + ].slice(-10), // Keep only the last 10 versions changeSource: 'auto-save', }; diff --git a/app/styles/diff-view.css b/app/styles/diff-view.css index e99e8be7a3..de353ef3a3 100644 --- a/app/styles/diff-view.css +++ b/app/styles/diff-view.css @@ -69,4 +69,4 @@ .diff-removed { @apply bg-red-500/20 border-l-4 border-red-500; -} \ No newline at end of file +} diff --git a/app/types/actions.ts b/app/types/actions.ts index 623c49794c..00034bc40c 100644 --- a/app/types/actions.ts +++ b/app/types/actions.ts @@ -44,6 +44,6 @@ export interface FileHistory { content: string; }[]; - // Novo campo para rastrear a origem das mudanças + // New field to track the origin of changes changeSource?: 'user' | 'auto-save' | 'external'; } diff --git a/app/types/pdf.d.ts b/app/types/pdf.d.ts new file mode 100644 index 0000000000..2c2ae74dca --- /dev/null +++ b/app/types/pdf.d.ts @@ -0,0 +1,4 @@ +declare module 'pdfjs-dist/build/pdf.worker.mjs' { + const worker: any; + export default worker; +} diff --git a/app/utils/documentUtils.ts b/app/utils/documentUtils.ts new file mode 100644 index 0000000000..22e6f3b4aa --- /dev/null +++ b/app/utils/documentUtils.ts @@ -0,0 +1,394 @@ +import JSZip from 'jszip'; +import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist'; +import type { PDFDocumentProxy } from 'pdfjs-dist'; + +/* + * Import the worker as a virtual URL from Vite + * @vite-ignore + */ +const pdfjsWorkerUrl = new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url).href; + +/* + * Flag to use only fallback method + * const USE_ONLY_FALLBACK = true; + */ + +/** + * Extracts text from a DOCX file + * + * @param file The DOCX file + * @returns A Promise with the extracted text + */ +export async function extractTextFromDOCX(file: File | Blob): Promise { + try { + // Load the file as a zip + const zip = new JSZip(); + const content = await zip.loadAsync(file); + + // The main content of the document is in word/document.xml + const documentXml = await content.file('word/document.xml')?.async('text'); + + if (!documentXml) { + throw new Error('document.xml not found in DOCX file'); + } + + /* + * Extract text using regular expressions + * This is a simplification and may not capture all the complexities + * of a DOCX document, but it works for simple cases + */ + const textMatches = documentXml.match(/]*>([^<]*)<\/w:t>/g); + + if (!textMatches) { + return 'No text content found in document'; + } + + // Extract the text from each tag + const extractedText = textMatches + .map((match) => { + // Extract the content between and + const content = match.replace(/]*>([^<]*)<\/w:t>/, '$1'); + return content; + }) + .join(' '); + + return extractedText; + } catch (error) { + console.error('Error extracting text from DOCX:', error); + return `Error extracting text: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * Extracts text from PDF using a simplified method that doesn't depend on the worker + * This function analyzes the raw bytes of the PDF to find text strings + */ +async function extractPdfTextSimple(file: File | Blob): Promise { + try { + // Read the file as ArrayBuffer + const arrayBuffer = await file.arrayBuffer(); + const data = new Uint8Array(arrayBuffer); + + // Convert to string + const textDecoder = new TextDecoder('utf-8'); + const pdfString = textDecoder.decode(data); + + /* + * Look for common text patterns in PDFs + * PDF uses parentheses () to delimit strings in many cases + */ + const textChunks = []; + + // Search for text between parentheses - common in PDFs + const parenthesesMatches = pdfString.match(/\(([^\(\)\\]*(?:\\.[^\(\)\\]*)*)\)/g) || []; + + for (const match of parenthesesMatches) { + // Remove parentheses and decode escape sequences + const processed = match + .slice(1, -1) + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\\(/g, '(') + .replace(/\\\)/g, ')') + .replace(/\\\\/g, '\\'); + + // If it looks like valid text (more than just special characters) + if (/[a-zA-Z0-9]{2,}/.test(processed)) { + textChunks.push(processed); + } + } + + /* + * Search for uncompressed text blocks + * Many PDFs have text between /BT and /ET + */ + const btEtRegex = /BT[\s\S]+?ET/g; + const textBlocks = pdfString.match(btEtRegex) || []; + + for (const block of textBlocks) { + // Extract TJ strings within BT/ET blocks that frequently contain text + const tjMatches = block.match(/\[([^\]]+)\][\s]*TJ/g) || []; + + for (const tj of tjMatches) { + // Extract strings within parentheses + const stringMatches = tj.match(/\(([^\(\)\\]*(?:\\.[^\(\)\\]*)*)\)/g) || []; + + for (const str of stringMatches) { + const processed = str + .slice(1, -1) + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\\(/g, '(') + .replace(/\\\)/g, ')') + .replace(/\\\\/g, '\\'); + + if (processed.trim().length > 0) { + textChunks.push(processed); + } + } + } + + // Extract Tj strings that also contain text + const tjSingleMatches = block.match(/\(([^\(\)\\]*(?:\\.[^\(\)\\]*)*)\)[\s]*Tj/g) || []; + + for (const tj of tjSingleMatches) { + const processed = tj + .match(/\(([^\(\)\\]*(?:\\.[^\(\)\\]*)*)\)/)?.[0] + ?.slice(1, -1) + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\\(/g, '(') + .replace(/\\\)/g, ')') + .replace(/\\\\/g, '\\'); + + if (processed && processed.trim().length > 0) { + textChunks.push(processed); + } + } + } + + // Join and clean the extracted text + if (textChunks.length === 0) { + return 'No text could be extracted from this PDF. The file may be scanned, protected, or corrupt.'; + } + + // Process and join the text chunks + let extractedText = ''; + let currentLine = ''; + + // const lastY = null; + + // Sort and group by lines (simulating layout) + for (const chunk of textChunks) { + // Add chunk, preserving line breaks + if (chunk.includes('\n')) { + // If it contains line breaks, split + const lines = chunk.split('\n'); + currentLine += lines[0].trim() + ' '; + + for (let i = 1; i < lines.length; i++) { + if (currentLine.trim().length > 0) { + extractedText += currentLine.trim() + '\n'; + } + + currentLine = lines[i].trim() + ' '; + } + } else { + // Add to current text + currentLine += chunk.trim() + ' '; + + // If the chunk appears to be the end of a sentence, add a line break + if (chunk.trim().match(/[.!?]$/)) { + extractedText += currentLine.trim() + '\n'; + currentLine = ''; + } + } + } + + // Add the last line if there is remaining content + if (currentLine.trim().length > 0) { + extractedText += currentLine.trim(); + } + + // Clean and format + extractedText = extractedText + .replace(/\s+/g, ' ') // Normalize spaces + .replace(/\n\s+/g, '\n') // Remove spaces at the beginning of lines + .replace(/\n+/g, '\n\n') // Normalize line breaks + .trim(); + + return extractedText || 'Limited text was extracted from this PDF. It may be primarily a scanned document.'; + } catch (error) { + console.error('PDF text extraction failed:', error); + return `Failed to extract text from PDF: ${error instanceof Error ? error.message : String(error)}`; + } +} + +// Configure o worker (compatível com Vite) +if (typeof window !== 'undefined') { + GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl; +} + +// Cache for PDF documents to improve performance +const pdfCache = new Map>(); + +/** + * Extracts text from a PDF file using pdfjs-dist with optimizations + * + * @param file The PDF file + * @returns A Promise with the extracted text + */ +export async function extractTextFromPDF(file: File | Blob): Promise { + try { + console.log('Extracting text from PDF using pdfjs-dist'); + + // Generate a unique key for the file cache + const cacheKey = file instanceof File ? file.name + file.lastModified : Math.random().toString(); + + // Convert the file to ArrayBuffer + const arrayBuffer = await file.arrayBuffer(); + + // Use cached PDF document if available + let pdfDocument: PDFDocumentProxy; + + if (pdfCache.has(cacheKey)) { + pdfDocument = await pdfCache.get(cacheKey)!; + } else { + const loadingTask = getDocument({ + data: arrayBuffer, + disableFontFace: true, // Reduces memory usage + /* + * No need for cMapUrl when using built-in cmaps + * cMapUrl: '/assets/cmaps/', + * cMapPacked: true, + */ + }); + + const documentPromise = loadingTask.promise; + pdfCache.set(cacheKey, documentPromise); + pdfDocument = await documentPromise; + } + + const numPages = pdfDocument.numPages; + console.log(`PDF has ${numPages} pages`); + + // For very large PDFs, we might want to process pages in batches + const isLargeDocument = numPages > 100; + const fullText: string[] = []; + + // Process pages either in sequence or in batches + if (isLargeDocument) { + // Process large documents in batches to reduce memory usage + const batchSize = 10; + + for (let i = 0; i < numPages; i += batchSize) { + const batch = []; + + for (let j = 0; j < batchSize && i + j < numPages; j++) { + batch.push(extractPageText(pdfDocument, i + j + 1)); + } + + const batchResults = await Promise.all(batch); + fullText.push(...batchResults); + } + } else { + // For smaller documents, process all pages in parallel + const textPromises = []; + + for (let i = 1; i <= numPages; i++) { + textPromises.push(extractPageText(pdfDocument, i)); + } + + const pageTexts = await Promise.all(textPromises); + fullText.push(...pageTexts); + } + + return fullText.join('\n\n'); + } catch (error) { + console.error('Error extracting text from PDF with pdfjs:', error); + + // Fall back to the simplified method if the main method fails + console.log('Falling back to simplified PDF extraction method'); + + return extractPdfTextSimple(file); + } +} + +/** + * Helper function to extract text from a single PDF page + */ +async function extractPageText(pdfDocument: PDFDocumentProxy, pageNum: number): Promise { + try { + const page = await pdfDocument.getPage(pageNum); + const textContent = await page.getTextContent(); + + // Process the text content to maintain some formatting + const text = processTextContent(textContent); + + // Clean up page resources to reduce memory usage + page.cleanup(); + + return text; + } catch (error) { + console.error(`Error extracting text from page ${pageNum}:`, error); + return `[Failed to extract text from page ${pageNum}]`; + } +} + +/** + * Process text content from a PDF page to maintain formatting + */ +function processTextContent(textContent: any): string { + const textItems = textContent.items; + const lines: { text: string; y: number }[] = []; + let lastY: number | null = null; + let currentLine = ''; + + // Group text by vertical position (y-coordinate) to maintain line breaks + for (const item of textItems) { + // Skip items without string content + if (!item.str || typeof item.str !== 'string') { + continue; + } + + const text = item.str; + const transform = item.transform || [0, 0, 0, 0, 0, 0]; + const y = transform[5]; + + // If this is a new line (different y-coordinate) + if (lastY !== null && Math.abs(y - lastY) > 1) { + lines.push({ text: currentLine, y: lastY }); + currentLine = text; + } else { + // Same line, append text with proper spacing + if (currentLine && text && !currentLine.endsWith(' ') && !text.startsWith(' ')) { + currentLine += ' '; + } + + currentLine += text; + } + + lastY = y; + } + + // Add the last line + if (currentLine) { + lines.push({ text: currentLine, y: lastY || 0 }); + } + + // Sort lines by y-coordinate in descending order (top to bottom) + lines.sort((a, b) => b.y - a.y); + + // Join lines with newlines + return lines.map((line) => line.text).join('\n'); +} + +/** + * Detects the document type and extracts the text + * + * @param file The file (DOCX, PDF, etc.) + * @returns A Promise with the extracted text + */ +export async function extractTextFromDocument(file: File | Blob): Promise { + const fileType = file instanceof File ? file.type : ''; + const fileName = file instanceof File ? file.name.toLowerCase() : ''; + + try { + if ( + fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + fileName.endsWith('.docx') + ) { + return await extractTextFromDOCX(file); + } else if (fileType === 'application/pdf' || fileName.endsWith('.pdf')) { + return await extractTextFromPDF(file); + } else { + return 'Unsupported document type. Please upload DOCX or PDF files.'; + } + } catch (error) { + console.error('Document extraction error:', error); + return `Document processing error: ${error instanceof Error ? error.message : String(error)}`; + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs index eafac089f4..1e6548380e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ export default [ '@typescript-eslint/no-empty-object-type': 'off', '@blitz/comment-syntax': 'off', '@blitz/block-scope-case': 'off', + '@blitz/lines-around-comment': 'off', 'array-bracket-spacing': ['error', 'never'], 'object-curly-newline': ['error', { consistent: true }], 'keyword-spacing': ['error', { before: true, after: true }], diff --git a/package.json b/package.json index 144831fb00..8aeb5ff892 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "nanostores": "^0.10.3", "ollama-ai-provider": "^0.15.2", "path-browserify": "^1.0.1", + "pdfjs-dist": "^4.10.38", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-chartjs-2": "^5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb37289be4..b2434241bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -239,6 +239,9 @@ importers: path-browserify: specifier: ^1.0.1 version: 1.0.1 + pdfjs-dist: + specifier: ^4.10.38 + version: 4.10.38 react: specifier: ^18.3.1 version: 18.3.1 @@ -1793,6 +1796,70 @@ packages: nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 react: '>=18.0.0' + '@napi-rs/canvas-android-arm64@0.1.68': + resolution: {integrity: sha512-h1KcSR4LKLfRfzeBH65xMxbWOGa1OtMFQbCMVlxPCkN1Zr+2gK+70pXO5ktojIYcUrP6KDcOwoc8clho5ccM/w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.68': + resolution: {integrity: sha512-/VURlrAD4gDoxW1GT/b0nP3fRz/fhxmHI/xznTq2FTwkQLPOlLkDLCvTmQ7v6LtGKdc2Ed6rvYpRan+JXThInQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.68': + resolution: {integrity: sha512-tEpvGR6vCLTo1Tx9wmDnoOKROpw57wiCWwCpDOuVlj/7rqEJOUYr9ixW4aRJgmeGBrZHgevI0EURys2ER6whmg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.68': + resolution: {integrity: sha512-U9xbJsumPOiAYeAFZMlHf62b9dGs2HJ6Q5xt7xTB0uEyPeurwhgYBWGgabdsEidyj38YuzI/c3LGBbSQB3vagw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.68': + resolution: {integrity: sha512-KFkn8wEm3mPnWD4l8+OUUkxylSJuN5q9PnJRZJgv15RtCA1bgxIwTkBhI/+xuyVMcHqON9sXq7cDkEJtHm35dg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.68': + resolution: {integrity: sha512-IQzts91rCdOALXBWQxLZRCEDrfFTGDtNRJMNu+2SKZ1uT8cmPQkPwVk5rycvFpvgAcmiFiOSCp1aRrlfU8KPpQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.68': + resolution: {integrity: sha512-e9AS5UttoIKqXSmBzKZdd3NErSVyOEYzJfNOCGtafGk1//gibTwQXGlSXmAKuErqMp09pyk9aqQRSYzm1AQfBw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.68': + resolution: {integrity: sha512-Pa/I36VE3j57I3Obhrr+J48KGFfkZk2cJN/2NmW/vCgmoF7kCP6aTVq5n+cGdGWLd/cN9CJ9JvNwEoMRDghu0g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.68': + resolution: {integrity: sha512-9c6rkc5195wNxuUHJdf4/mmnq433OQey9TNvQ9LspJazvHbfSkTij8wtKjASVQsJyPDva4fkWOeV/OQ7cLw0GQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-x64-msvc@0.1.68': + resolution: {integrity: sha512-Fc5Dez23u0FoSATurT6/w1oMytiRnKWEinHivdMvXpge6nG4YvhrASrtqMk8dGJMVQpHr8QJYF45rOrx2YU2Aw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.68': + resolution: {integrity: sha512-LQESrePLEBLvhuFkXx9jjBXRC2ClYsO5mqQ1m/puth5z9SOuM3N/B3vDuqnC3RJFktDktyK9khGvo7dTkqO9uQ==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5387,6 +5454,10 @@ packages: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} + pdfjs-dist@4.10.38: + resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==} + engines: {node: '>=20'} + peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} @@ -8472,6 +8543,50 @@ snapshots: nanostores: 0.10.3 react: 18.3.1 + '@napi-rs/canvas-android-arm64@0.1.68': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.68': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.68': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.68': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.68': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.68': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.68': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.68': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.68': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.68': + optional: true + + '@napi-rs/canvas@0.1.68': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.68 + '@napi-rs/canvas-darwin-arm64': 0.1.68 + '@napi-rs/canvas-darwin-x64': 0.1.68 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.68 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.68 + '@napi-rs/canvas-linux-arm64-musl': 0.1.68 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.68 + '@napi-rs/canvas-linux-x64-gnu': 0.1.68 + '@napi-rs/canvas-linux-x64-musl': 0.1.68 + '@napi-rs/canvas-win32-x64-msvc': 0.1.68 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13041,6 +13156,10 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 + pdfjs-dist@4.10.38: + optionalDependencies: + '@napi-rs/canvas': 0.1.68 + peek-stream@1.1.3: dependencies: buffer-from: 1.1.2 diff --git a/vite.config.ts b/vite.config.ts index a9351c12ff..03d1575b1d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -95,6 +95,7 @@ export default defineConfig((config) => { build: { target: 'esnext', rollupOptions: { + external: ['pdfjs-dist'], output: { format: 'esm', },