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:
kubet 2025-08-11 22:49:51 +02:00 committed by GitHub
commit 6cb7d903cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 19 deletions

View File

@ -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

View File

@ -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

View File

@ -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>
);
}

View File

@ -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';