mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1315 from kubet/feat/add-pdf-renderer
Add PDF support to attachment previews and update related logic
This commit is contained in:
commit
6cb7d903cf
|
@ -114,11 +114,19 @@ export function AttachmentGroup({
|
|||
return !sandboxId ? file.localUrl : undefined;
|
||||
};
|
||||
|
||||
// Check if a file is HTML, Markdown, or CSV
|
||||
// Check if a file is HTML, Markdown, CSV, or PDF (previewable types in grid)
|
||||
const isPreviewableFile = (file: string | UploadedFile): boolean => {
|
||||
const path = getFilePath(file);
|
||||
const ext = path.split('.').pop()?.toLowerCase() || '';
|
||||
return ext === 'html' || ext === 'htm' || ext === 'md' || ext === 'markdown' || ext === 'csv' || ext === 'tsv';
|
||||
return (
|
||||
ext === 'html' ||
|
||||
ext === 'htm' ||
|
||||
ext === 'md' ||
|
||||
ext === 'markdown' ||
|
||||
ext === 'csv' ||
|
||||
ext === 'tsv' ||
|
||||
ext === 'pdf'
|
||||
);
|
||||
};
|
||||
|
||||
// Pre-compute any conditional values used in rendering
|
||||
|
|
|
@ -9,6 +9,7 @@ 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 { PdfRenderer as PdfPreviewRenderer } from './preview-renderers/pdf-renderer';
|
||||
import { useFileContent, useImageContent } from '@/hooks/react-query/files';
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
import { Project } from '@/lib/api';
|
||||
|
@ -192,10 +193,11 @@ export function FileAttachment({
|
|||
const isImage = fileType === 'image';
|
||||
const isHtmlOrMd = extension === 'html' || extension === 'htm' || extension === 'md' || extension === 'markdown';
|
||||
const isCsv = extension === 'csv' || extension === 'tsv';
|
||||
const isPdf = extension === 'pdf';
|
||||
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) && showPreview && collapsed === false;
|
||||
const shouldShowPreview = (isHtmlOrMd || isCsv || isPdf) && showPreview && collapsed === false;
|
||||
|
||||
// Use the React Query hook to fetch file content
|
||||
const {
|
||||
|
@ -203,8 +205,8 @@ export function FileAttachment({
|
|||
isLoading: fileContentLoading,
|
||||
error: fileContentError
|
||||
} = useFileContent(
|
||||
shouldShowPreview ? sandboxId : undefined,
|
||||
shouldShowPreview ? filepath : undefined
|
||||
(isHtmlOrMd || isCsv) && shouldShowPreview ? sandboxId : undefined,
|
||||
(isHtmlOrMd || isCsv) && shouldShowPreview ? filepath : undefined
|
||||
);
|
||||
|
||||
// Use the React Query hook to fetch image content with authentication
|
||||
|
@ -217,12 +219,22 @@ export function FileAttachment({
|
|||
isImage && showPreview ? filepath : undefined
|
||||
);
|
||||
|
||||
// For PDFs we also fetch blob URL via the same binary hook used for images
|
||||
const {
|
||||
data: pdfBlobUrl,
|
||||
isLoading: pdfLoading,
|
||||
error: pdfError
|
||||
} = useImageContent(
|
||||
isPdf && shouldShowPreview && sandboxId ? sandboxId : undefined,
|
||||
isPdf && shouldShowPreview ? filepath : undefined
|
||||
);
|
||||
|
||||
// Set error state based on query errors
|
||||
React.useEffect(() => {
|
||||
if (fileContentError || imageError) {
|
||||
if (fileContentError || imageError || pdfError) {
|
||||
setHasError(true);
|
||||
}
|
||||
}, [fileContentError, imageError]);
|
||||
}, [fileContentError, imageError, pdfError]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (onClick) {
|
||||
|
@ -376,7 +388,7 @@ export function FileAttachment({
|
|||
'tsv': CsvRenderer
|
||||
};
|
||||
|
||||
// HTML/MD/CSV preview when not collapsed and in grid layout
|
||||
// HTML/MD/CSV/PDF preview when not collapsed and in grid layout
|
||||
if (shouldShowPreview && isGridLayout) {
|
||||
// Determine the renderer component
|
||||
const Renderer = rendererMap[extension as keyof typeof rendererMap];
|
||||
|
@ -388,7 +400,7 @@ export function FileAttachment({
|
|||
"border",
|
||||
"bg-card",
|
||||
"overflow-hidden",
|
||||
"h-[300px]", // Fixed height for previews
|
||||
isPdf ? "h-[500px]" : "h-[300px]",
|
||||
"pt-10", // Room for header
|
||||
className
|
||||
)}
|
||||
|
@ -401,20 +413,25 @@ export function FileAttachment({
|
|||
>
|
||||
{/* Content area */}
|
||||
<div className="h-full w-full relative">
|
||||
{/* Only show content if we have it and no errors */}
|
||||
{!hasError && fileContent && (
|
||||
{/* Render PDF or text-based previews */}
|
||||
{!hasError && (
|
||||
<>
|
||||
{Renderer ? (
|
||||
{isPdf && (() => {
|
||||
const pdfUrlForRender = localPreviewUrl || (sandboxId ? (pdfBlobUrl ?? null) : fileUrl);
|
||||
return pdfUrlForRender ? (
|
||||
<PdfPreviewRenderer
|
||||
url={pdfUrlForRender}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
{!isPdf && fileContent && Renderer && (
|
||||
<Renderer
|
||||
content={fileContent}
|
||||
previewUrl={fileUrl}
|
||||
className="h-full w-full"
|
||||
project={project}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
No preview available for this file type
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -440,14 +457,20 @@ export function FileAttachment({
|
|||
)}
|
||||
|
||||
{/* Loading state */}
|
||||
{fileContentLoading && (
|
||||
{fileContentLoading && !isPdf && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
|
||||
<Loader2 className="h-6 w-6 text-primary animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPdf && pdfLoading && !pdfBlobUrl && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
|
||||
<Loader2 className="h-6 w-6 text-primary animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty content state - show when not loading and no content yet */}
|
||||
{!fileContent && !fileContentLoading && !hasError && (
|
||||
{!isPdf && !fileContent && !fileContentLoading && !hasError && (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center p-4 pointer-events-none">
|
||||
<div className="text-muted-foreground text-sm mb-2">
|
||||
Preview available
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
|
||||
// Import styles for annotations and text layer
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
|
||||
// Configure PDF.js worker (same as main PDF renderer)
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
interface PdfRendererProps {
|
||||
url?: string | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Minimal inline PDF preview for attachment grid. No toolbar. First page only.
|
||||
export function PdfRenderer({ url, className }: PdfRendererProps) {
|
||||
const [containerWidth, setContainerWidth] = React.useState<number | null>(null);
|
||||
const wrapperRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!wrapperRef.current) return;
|
||||
const element = wrapperRef.current;
|
||||
const setWidth = () => setContainerWidth(element.clientWidth);
|
||||
setWidth();
|
||||
const observer = new ResizeObserver(() => setWidth());
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<div className={cn('w-full h-full flex items-center justify-center bg-muted/20', className)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={cn('w-full h-full overflow-auto bg-background', className)}>
|
||||
<div className="flex justify-center">
|
||||
<Document file={url} className="shadow-none">
|
||||
<Page
|
||||
pageNumber={1}
|
||||
width={containerWidth ?? undefined}
|
||||
renderTextLayer={true}
|
||||
renderAnnotationLayer={true}
|
||||
className="border border-border rounded bg-white"
|
||||
/>
|
||||
</Document>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ export function AskToolView({
|
|||
|
||||
const isPreviewableFile = (filePath: string): boolean => {
|
||||
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
||||
return ext === 'html' || ext === 'htm' || ext === 'md' || ext === 'markdown' || ext === 'csv' || ext === 'tsv';
|
||||
return ext === 'html' || ext === 'htm' || ext === 'md' || ext === 'markdown' || ext === 'csv' || ext === 'tsv' || ext === 'pdf';
|
||||
};
|
||||
|
||||
const toolTitle = getToolTitle(name) || 'Ask User';
|
||||
|
|
Loading…
Reference in New Issue