diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd452e84..2b8d352f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,6 +47,7 @@ "@tanstack/react-table": "^8.21.3", "@types/jszip": "^3.4.0", "@types/papaparse": "^5.3.15", + "@types/xlsx": "^0.0.35", "@uiw/codemirror-extensions-langs": "^4.23.10", "@uiw/codemirror-theme-vscode": "^4.23.10", "@uiw/codemirror-theme-xcode": "^4.23.10", @@ -113,6 +114,7 @@ "tailwindcss-animate": "^1.0.7", "typescript": "^5", "vaul": "^1.1.2", + "xlsx": "^0.18.5", "zod": "^3.24.2", "zustand": "^5.0.3" }, @@ -5896,6 +5898,12 @@ "@types/node": "*" } }, + "node_modules/@types/xlsx": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.35.tgz", + "integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", @@ -6709,6 +6717,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -7380,6 +7397,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -7811,6 +7841,15 @@ "@lezer/lr": "^1.3.10" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -8380,6 +8419,18 @@ "integrity": "sha512-D/ZkRyj+ywJC6b2IrAN3/tpbReMUqmuRLlcKFoY/o0+EPQN9Ev/e8tV+D3+9scvu/tarxwLErNwS73C3yzxs/g==", "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -10310,6 +10361,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -16152,6 +16212,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -17539,6 +17611,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17576,6 +17666,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b25c6405..6ea0b8a1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,6 +50,7 @@ "@tanstack/react-table": "^8.21.3", "@types/jszip": "^3.4.0", "@types/papaparse": "^5.3.15", + "@types/xlsx": "^0.0.35", "@uiw/codemirror-extensions-langs": "^4.23.10", "@uiw/codemirror-theme-vscode": "^4.23.10", "@uiw/codemirror-theme-xcode": "^4.23.10", @@ -116,6 +117,7 @@ "tailwindcss-animate": "^1.0.7", "typescript": "^5", "vaul": "^1.1.2", + "xlsx": "^0.18.5", "zod": "^3.24.2", "zustand": "^5.0.3" }, diff --git a/frontend/src/components/thread/attachment-group.tsx b/frontend/src/components/thread/attachment-group.tsx index b7caab48..4322fd26 100644 --- a/frontend/src/components/thread/attachment-group.tsx +++ b/frontend/src/components/thread/attachment-group.tsx @@ -118,7 +118,7 @@ export function AttachmentGroup({ return !sandboxId ? file.localUrl : undefined; }; - // Check if a file is HTML, Markdown, CSV, or PDF (previewable types in grid) + // Check if a file is HTML, Markdown, CSV, XLSX, or PDF (previewable types in grid) const isPreviewableFile = (file: string | UploadedFile): boolean => { const path = getFilePath(file); const ext = path.split('.').pop()?.toLowerCase() || ''; @@ -129,6 +129,8 @@ export function AttachmentGroup({ ext === 'markdown' || ext === 'csv' || ext === 'tsv' || + ext === 'xlsx' || + ext === 'xls' || ext === 'pdf' ); }; diff --git a/frontend/src/components/thread/file-attachment.tsx b/frontend/src/components/thread/file-attachment.tsx index 7c067d78..09ebb4f8 100644 --- a/frontend/src/components/thread/file-attachment.tsx +++ b/frontend/src/components/thread/file-attachment.tsx @@ -2,14 +2,21 @@ import React from 'react'; import { FileText, FileImage, FileCode, FileSpreadsheet, FileVideo, FileAudio, FileType, Database, Archive, File, ExternalLink, - Loader2, Download + Loader2, Download, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { AttachmentGroup } from './attachment-group'; import { HtmlRenderer } from './preview-renderers/html-renderer'; import { MarkdownRenderer } from './preview-renderers/markdown-renderer'; import { CsvRenderer } from './preview-renderers/csv-renderer'; +import { XlsxRenderer } from './preview-renderers/xlsx-renderer'; import { PdfRenderer as PdfPreviewRenderer } from './preview-renderers/pdf-renderer'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; import { useFileContent, useImageContent } from '@/hooks/react-query/files'; import { useAuth } from '@/components/AuthProvider'; @@ -187,6 +194,10 @@ export function FileAttachment({ // Simplified state management const [hasError, setHasError] = React.useState(false); + // XLSX sheet management + const [xlsxSheetIndex, setXlsxSheetIndex] = React.useState(0); + const [xlsxSheetNames, setXlsxSheetNames] = React.useState([]); + // Basic file info const filename = filepath.split('/').pop() || 'file'; const extension = filename.split('.').pop()?.toLowerCase() || ''; @@ -205,10 +216,11 @@ export function FileAttachment({ const isGridLayout = customStyle?.gridColumn === '1 / -1' || Boolean(customStyle && ('--attachment-height' in customStyle)); // Define isInlineMode early, before any hooks const isInlineMode = !isGridLayout; - const shouldShowPreview = (isHtmlOrMd || isCsv || isPdf) && showPreview && collapsed === false; + const shouldShowPreview = (isHtmlOrMd || isCsv || isXlsx || isPdf) && showPreview && collapsed === false; // Use the React Query hook to fetch file content // For CSV files, always try to load content for better preview experience + // For XLSX files, we need binary data which is handled by useImageContent const shouldLoadContent = (isHtmlOrMd || isCsv) && (shouldShowPreview || isCsv); const { data: fileContent, @@ -239,12 +251,47 @@ export function FileAttachment({ isPdf && shouldShowPreview ? filepath : undefined ); + // For XLSX files we fetch binary data and convert to base64 + const { + data: xlsxBlobUrl, + isLoading: xlsxLoading, + error: xlsxError + } = useImageContent( + isXlsx && shouldShowPreview && sandboxId ? sandboxId : undefined, + isXlsx && shouldShowPreview ? filepath : undefined + ); + // Set error state based on query errors React.useEffect(() => { - if (fileContentError || imageError || pdfError) { + if (fileContentError || imageError || pdfError || xlsxError) { setHasError(true); } - }, [fileContentError, imageError, pdfError]); + }, [fileContentError, imageError, pdfError, xlsxError]); + + // Parse XLSX to get sheet names when blob URL is available + React.useEffect(() => { + if (isXlsx && xlsxBlobUrl && shouldShowPreview) { + const parseSheetNames = async () => { + try { + // Import XLSX dynamically to avoid bundle size issues + const XLSX = await import('xlsx'); + + // Convert blob URL to binary data + const response = await fetch(xlsxBlobUrl); + const arrayBuffer = await response.arrayBuffer(); + + // Read workbook + const workbook = XLSX.read(arrayBuffer, { type: 'array' }); + setXlsxSheetNames(workbook.SheetNames); + } catch (error) { + console.error('Failed to parse XLSX sheet names:', error); + setXlsxSheetNames([]); + } + }; + + parseSheetNames(); + } + }, [isXlsx, xlsxBlobUrl, shouldShowPreview]); const handleClick = () => { if (onClick) { @@ -438,7 +485,9 @@ export function FileAttachment({ 'md': MarkdownRenderer, 'markdown': MarkdownRenderer, 'csv': CsvRenderer, - 'tsv': CsvRenderer + 'tsv': CsvRenderer, + 'xlsx': XlsxRenderer, + 'xls': XlsxRenderer }; // HTML/MD/CSV/PDF preview when not collapsed and in grid layout @@ -453,7 +502,7 @@ export function FileAttachment({ "rounded-xl border bg-card overflow-hidden pt-10", // Consistent card styling with header space isPdf ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[500px] sm:!min-w-[300px]" : isHtmlOrMd ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[600px] sm:!min-w-[300px]" : - isCsv ? "min-h-[300px] h-full" : // Let CSV take full height + (isCsv || isXlsx) ? "min-h-[300px] h-full" : // Let CSV and XLSX take full height standalone ? "min-h-[300px] h-auto" : "h-[300px]", // Better height handling for standalone className )} @@ -475,7 +524,7 @@ export function FileAttachment({ contain: (isPdf || isHtmlOrMd) ? 'layout size' : undefined }} > - {/* Render PDF or text-based previews */} + {/* Render PDF, XLSX, or text-based previews */} {!hasError && ( <> {isPdf && (() => { @@ -487,7 +536,18 @@ export function FileAttachment({ /> ) : null; })()} - {!isPdf && fileContent && Renderer && ( + {isXlsx && (() => { + const xlsxUrlForRender = localPreviewUrl || (sandboxId ? (xlsxBlobUrl ?? null) : fileUrl); + return xlsxUrlForRender ? ( + setXlsxSheetIndex(index)} + /> + ) : null; + })()} + {!isPdf && !isXlsx && fileContent && Renderer && ( )} + {isXlsx && xlsxLoading && !xlsxBlobUrl && ( +
+ +
+ )} + {/* Empty content state - show when not loading and no content yet */} - {!isPdf && !fileContent && !fileContentLoading && !hasError && ( + {!isPdf && !isXlsx && !fileContent && !fileContentLoading && !hasError && (
Preview available @@ -555,7 +621,34 @@ export function FileAttachment({ {/* Header with filename */}
-
{filename}
+
+
{filename}
+ {/* XLSX Sheet Selector */} + {isXlsx && xlsxSheetNames.length > 1 && ( + + + + + + {xlsxSheetNames.map((sheetName, index) => ( + setXlsxSheetIndex(index)} + className={cn( + "text-xs cursor-pointer", + index === xlsxSheetIndex && "bg-accent" + )} + > + {sheetName} + + ))} + + + )} +
{/*