forked from hangshuo652/aurak
0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
713 lines
25 KiB
TypeScript
713 lines
25 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import { isFormatSupportedForPreview } from '../constants/fileSupport';
|
||
import { PDFStatus } from '../types';
|
||
import { pdfPreviewService } from '../services/pdfPreviewService';
|
||
import { useToast } from '../contexts/ToastContext';
|
||
import { useConfirm } from '../contexts/ConfirmContext';
|
||
import { X, FileText, Loader, AlertCircle, Maximize2, Eye, Download, ExternalLink, RefreshCw, Scissors, ChevronLeft, ChevronRight } from 'lucide-react';
|
||
import { PDFSelectionTool } from './PDFSelectionTool';
|
||
import { CreateNoteFromPDFDialog } from './CreateNoteFromPDFDialog';
|
||
import { noteService } from '../services/noteService';
|
||
import { useLanguage } from '../contexts/LanguageContext';
|
||
import { knowledgeBaseService } from '../services/knowledgeBaseService';
|
||
import * as pdfjs from 'pdfjs-dist';
|
||
|
||
// Set worker path for PDF.js
|
||
pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`;
|
||
|
||
interface PDFPreviewProps {
|
||
fileId: string;
|
||
fileName: string;
|
||
authToken: string;
|
||
groupId?: string;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export const PDFPreview: React.FC<PDFPreviewProps> = ({ fileId, fileName, authToken, groupId, onClose }) => {
|
||
const [status, setStatus] = useState<PDFStatus>({ status: 'pending' });
|
||
const [loading, setLoading] = useState(true);
|
||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||
const [pdfUrl, setPdfUrl] = useState<string>('');
|
||
const [iframeError, setIframeError] = useState(false);
|
||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [pdfBlob, setPdfBlob] = useState<Blob | null>(null);
|
||
const [selectionData, setSelectionData] = useState<{ screenshot: Blob; text: string } | null>(null);
|
||
const [numPages, setNumPages] = useState<number>(0);
|
||
const [pdfDoc, setPdfDoc] = useState<pdfjs.PDFDocumentProxy | null>(null);
|
||
const [zoomLevel, setZoomLevel] = useState<number>(1.0); // Add zoom level state
|
||
const currentRenderTask = useRef<pdfjs.RenderTask | null>(null); // Save current rendering task
|
||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||
const flipDirection = useRef<'next' | 'prev' | null>(null);
|
||
const lastFlipTime = useRef<number>(0);
|
||
|
||
const { showToast } = useToast();
|
||
const { confirm } = useConfirm();
|
||
const { t, language } = useLanguage();
|
||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (status.status === 'ready') {
|
||
pdfPreviewService.getPDFUrl(fileId)
|
||
.then(result => {
|
||
setPdfUrl(result.url); // Set pdfUrl for download
|
||
|
||
// Fetch PDF data and create blob URL
|
||
fetch(result.url)
|
||
.then(async response => {
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.message || 'Failed to fetch PDF data');
|
||
}
|
||
return response.blob();
|
||
})
|
||
.then(blob => {
|
||
setPdfBlob(blob);
|
||
|
||
// Start fetching and rendering PDF document
|
||
loadAndRenderPDF(blob);
|
||
})
|
||
.catch((err) => {
|
||
console.error('PDF fetch error:', err);
|
||
setIframeError(true);
|
||
setStatus({ status: 'failed', error: err.message });
|
||
});
|
||
})
|
||
.catch((err) => {
|
||
console.error('getPDFUrl error:', err);
|
||
setIframeError(true);
|
||
setStatus({ status: 'failed', error: err.message });
|
||
});
|
||
}
|
||
}, [status.status, fileId]);
|
||
|
||
useEffect(() => {
|
||
if (pdfDoc && currentPage) {
|
||
// Re-render on page change or zoom level change
|
||
renderCurrentPage(pdfDoc, currentPage);
|
||
}
|
||
}, [currentPage, pdfDoc, zoomLevel]);
|
||
|
||
const isSupported = isFormatSupportedForPreview(fileName);
|
||
|
||
useEffect(() => {
|
||
if (isSupported) {
|
||
checkPDFStatus();
|
||
const interval = setInterval(checkPDFStatus, 3000);
|
||
return () => clearInterval(interval);
|
||
} else {
|
||
setLoading(false);
|
||
setStatus({ status: 'failed', error: t('previewNotSupported') });
|
||
}
|
||
}, [fileId, isSupported]);
|
||
|
||
const checkPDFStatus = async () => {
|
||
try {
|
||
const pdfStatus = await pdfPreviewService.getPDFStatus(fileId);
|
||
setStatus(pdfStatus);
|
||
|
||
// Actively trigger conversion if status is pending
|
||
if (pdfStatus.status === 'pending') {
|
||
setStatus({ status: 'converting' });
|
||
try {
|
||
// Access PDF URL to trigger conversion
|
||
await pdfPreviewService.preloadPDF(fileId);
|
||
} catch (error) {
|
||
console.log('Preload triggered, conversion should start');
|
||
}
|
||
}
|
||
|
||
if (pdfStatus.status === 'ready' || pdfStatus.status === 'failed') {
|
||
setLoading(false);
|
||
}
|
||
} catch (error: any) {
|
||
setLoading(false);
|
||
const errorMessage = error.message || t('checkPDFStatusFailed');
|
||
setStatus({ status: 'failed', error: errorMessage });
|
||
showToast(errorMessage, 'error');
|
||
}
|
||
};
|
||
|
||
const loadAndRenderPDF = async (blob: Blob) => {
|
||
try {
|
||
const pdfData = await blob.arrayBuffer();
|
||
const pdf = await pdfjs.getDocument({ data: pdfData }).promise;
|
||
|
||
setPdfDoc(pdf);
|
||
setNumPages(pdf.numPages);
|
||
|
||
if (currentPage > pdf.numPages) {
|
||
setCurrentPage(pdf.numPages);
|
||
}
|
||
|
||
renderCurrentPage(pdf, currentPage);
|
||
} catch (error) {
|
||
console.error('Failed to load PDF:', error);
|
||
setIframeError(true);
|
||
}
|
||
};
|
||
|
||
const handleZoomIn = () => {
|
||
setZoomLevel(prev => Math.min(3.0, prev + 0.1));
|
||
};
|
||
|
||
const handleZoomOut = () => {
|
||
setZoomLevel(prev => Math.max(0.5, prev - 0.1));
|
||
};
|
||
|
||
const handleResetZoom = () => {
|
||
setZoomLevel(1.0);
|
||
};
|
||
|
||
const renderCurrentPage = async (pdf: pdfjs.PDFDocumentProxy, pageNum: number) => {
|
||
if (!canvasRef.current) return;
|
||
|
||
try {
|
||
// Cancel rendering task if one is in progress
|
||
if (currentRenderTask.current) {
|
||
currentRenderTask.current.cancel();
|
||
}
|
||
|
||
const page = await pdf.getPage(pageNum);
|
||
const canvas = canvasRef.current;
|
||
const context = canvas.getContext('2d');
|
||
|
||
if (!context) return;
|
||
|
||
// Get container dimensions
|
||
if (!containerRef.current) return;
|
||
const container = containerRef.current;
|
||
const containerWidth = container.clientWidth;
|
||
const containerHeight = container.clientHeight;
|
||
|
||
// Handle high DPI displays
|
||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||
|
||
// Calculate scale to fit page in container width while maintaining aspect ratio
|
||
const viewport = page.getViewport({ scale: 1 });
|
||
const baseScale = (containerWidth - 48) / viewport.width; // Add padding for width
|
||
|
||
// Apply zoom level to base scale
|
||
const scale = baseScale * zoomLevel;
|
||
|
||
const finalScale = scale * devicePixelRatio;
|
||
const scaledViewport = page.getViewport({ scale: finalScale });
|
||
const cssViewport = page.getViewport({ scale: scale });
|
||
|
||
// Set canvas dimensions with device pixel ratio
|
||
canvas.width = scaledViewport.width;
|
||
canvas.height = scaledViewport.height;
|
||
|
||
// Set CSS dimensions explicitly for high-DPI
|
||
canvas.style.width = `${cssViewport.width}px`;
|
||
canvas.style.height = `${cssViewport.height}px`;
|
||
|
||
// Reset any previous transforms
|
||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||
|
||
// Clear canvas
|
||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Fill with white background
|
||
context.fillStyle = 'white';
|
||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Set proper transform for high DPI
|
||
context.scale(devicePixelRatio, devicePixelRatio);
|
||
|
||
// Create and save the new render task
|
||
const renderContext = {
|
||
canvasContext: context,
|
||
viewport: page.getViewport({ scale: scale }),
|
||
};
|
||
|
||
// Save the render task to allow for cancellation
|
||
currentRenderTask.current = page.render(renderContext);
|
||
|
||
// Wait for rendering to complete
|
||
await currentRenderTask.current.promise;
|
||
|
||
// Clear the current render task
|
||
currentRenderTask.current = null;
|
||
|
||
// Adjust scroll position after page turn
|
||
if (flipDirection.current && scrollContainerRef.current) {
|
||
const container = scrollContainerRef.current;
|
||
if (flipDirection.current === 'next') {
|
||
container.scrollTop = 0;
|
||
} else if (flipDirection.current === 'prev') {
|
||
container.scrollTop = container.scrollHeight;
|
||
}
|
||
flipDirection.current = null;
|
||
}
|
||
} catch (error) {
|
||
if (error instanceof Error && error.name !== 'RenderingCancelledException') {
|
||
console.error('Failed to render PDF page:', error);
|
||
}
|
||
// Clear the current render task even if there's an error
|
||
currentRenderTask.current = null;
|
||
}
|
||
};
|
||
|
||
const handleFullscreen = () => {
|
||
setIsFullscreen(!isFullscreen);
|
||
};
|
||
|
||
const handleDownload = () => {
|
||
if (pdfUrl) {
|
||
// Directly download if pdfUrl already exists
|
||
const link = document.createElement('a');
|
||
link.href = pdfUrl;
|
||
link.download = fileName.replace(/\.[^/.]+$/, '.pdf');
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
} else {
|
||
// Try fetching and downloading if pdfUrl does not exist
|
||
pdfPreviewService.getPDFUrl(fileId)
|
||
.then(result => {
|
||
const link = document.createElement('a');
|
||
link.href = result.url;
|
||
link.download = fileName.replace(/\.[^/.]+$/, '.pdf');
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
})
|
||
.catch(error => {
|
||
console.error('Failed to download PDF:', error);
|
||
showToast('error', t('downloadPDFFailed'));
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleOpenInNewTab = () => {
|
||
if (pdfUrl) {
|
||
window.open(pdfUrl, '_blank');
|
||
} else {
|
||
// Try fetching and opening if pdfUrl does not exist
|
||
pdfPreviewService.getPDFUrl(fileId)
|
||
.then(result => {
|
||
window.open(result.url, '_blank');
|
||
})
|
||
.catch(error => {
|
||
console.error('Failed to open PDF in new tab:', error);
|
||
showToast('error', t('openPDFInNewTabFailed'));
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleRegenerate = async () => {
|
||
if (await confirm(t('confirmRegeneratePDF'))) {
|
||
setStatus({ status: 'converting' });
|
||
setLoading(true);
|
||
try {
|
||
await pdfPreviewService.preloadPDF(fileId, true);
|
||
// Reset state and trigger reload
|
||
setPdfUrl('');
|
||
setIframeError(false);
|
||
setPdfDoc(null);
|
||
setPdfBlob(null);
|
||
setNumPages(0);
|
||
} catch (error) {
|
||
showToast('error', t('requestRegenerationFailed'));
|
||
setStatus({ status: 'failed', error: t('requestRegenerationFailed') });
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleIframeError = () => {
|
||
setIframeError(true);
|
||
};
|
||
|
||
const handleWheel = (e: React.WheelEvent) => {
|
||
if (!scrollContainerRef.current || isSelectionMode) return;
|
||
|
||
const container = scrollContainerRef.current;
|
||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||
const now = Date.now();
|
||
const throttleMs = 600; // Prevent rapid page turning
|
||
|
||
// Scroll down for next page
|
||
if (e.deltaY > 0 && scrollTop + clientHeight >= scrollHeight - 1) {
|
||
if (currentPage < numPages && now - lastFlipTime.current > throttleMs) {
|
||
flipDirection.current = 'next';
|
||
lastFlipTime.current = now;
|
||
setCurrentPage(prev => prev + 1);
|
||
}
|
||
}
|
||
// Scroll up for previous page
|
||
else if (e.deltaY < 0 && scrollTop <= 1) {
|
||
if (currentPage > 1 && now - lastFlipTime.current > throttleMs) {
|
||
flipDirection.current = 'prev';
|
||
lastFlipTime.current = now;
|
||
setCurrentPage(prev => prev - 1);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleSelectionComplete = (screenshot: Blob, text: string) => {
|
||
// Set preliminary data and open dialog
|
||
setSelectionData({ screenshot, text });
|
||
setIsSelectionMode(false);
|
||
};
|
||
|
||
const handleSaveNote = async (title: string, content: string, selectedCategoryId?: string) => {
|
||
if (!authToken || !selectionData) return;
|
||
|
||
try {
|
||
await noteService.createFromPDFSelection(
|
||
authToken,
|
||
fileId,
|
||
selectionData.screenshot,
|
||
undefined, // groupId is no longer used for notes from PDF
|
||
selectedCategoryId,
|
||
currentPage
|
||
);
|
||
showToast('success', t('noteCreatedSuccess'));
|
||
setSelectionData(null);
|
||
} catch (error) {
|
||
console.error('Failed to create note:', error);
|
||
showToast('error', t('noteCreatedFailed'));
|
||
}
|
||
};
|
||
|
||
const renderContent = () => {
|
||
switch (status.status) {
|
||
case 'pending':
|
||
return (
|
||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||
<Loader size={48} className="animate-spin mb-4" />
|
||
<div className="text-lg font-medium mb-2">{t('preparingPDFConversion')}</div>
|
||
<div className="text-sm">{t('pleaseWait')}</div>
|
||
</div>
|
||
);
|
||
|
||
case 'converting':
|
||
return (
|
||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||
<Loader size={48} className="animate-spin mb-4" />
|
||
<div className="text-lg font-medium mb-2">{t('convertingPDF')}</div>
|
||
<div className="text-sm">{t('pleaseWait')}</div>
|
||
</div>
|
||
);
|
||
|
||
case 'failed':
|
||
return (
|
||
<div className="flex flex-col items-center justify-center h-full text-red-500">
|
||
<AlertCircle size={48} className="mb-4" />
|
||
<div className="text-lg font-medium mb-2">{t('pdfConversionFailed')}</div>
|
||
<div className="text-sm text-gray-500 text-center max-w-md">
|
||
{status.error || t('pdfConversionError')}
|
||
</div>
|
||
<button
|
||
onClick={checkPDFStatus}
|
||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||
>
|
||
{t('retry')}
|
||
</button>
|
||
</div>
|
||
);
|
||
|
||
case 'ready':
|
||
if (iframeError) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||
<AlertCircle size={48} className="mb-4" />
|
||
<div className="text-lg font-medium mb-2">{t('pdfLoadFailed')}</div>
|
||
<div className="text-sm text-gray-500 mb-4">{t('pdfLoadError')}</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={handleDownload}
|
||
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||
>
|
||
<Download size={16} />
|
||
{t('downloadPDF')}
|
||
</button>
|
||
<button
|
||
onClick={handleOpenInNewTab}
|
||
className="flex items-center gap-2 px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
|
||
>
|
||
<ExternalLink size={16} />
|
||
{t('openInNewWindow')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!pdfDoc) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||
<Loader size={48} className="animate-spin mb-4" />
|
||
<div className="text-lg font-medium mb-2">{t('loadingPDF')}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="relative w-full h-full flex flex-col" ref={containerRef}>
|
||
<div
|
||
ref={scrollContainerRef}
|
||
onWheel={handleWheel}
|
||
className="flex-grow overflow-auto pdf-canvas-container bg-gray-100"
|
||
>
|
||
<div className="flex flex-col items-center py-12 pb-32 min-h-full">
|
||
<canvas
|
||
ref={canvasRef}
|
||
className="bg-white shadow-xl max-w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{isSelectionMode && (
|
||
<PDFSelectionTool
|
||
containerRef={containerRef}
|
||
canvasRef={canvasRef}
|
||
pdfBlob={pdfBlob}
|
||
pageNumber={currentPage}
|
||
authToken={authToken}
|
||
zoomLevel={zoomLevel}
|
||
onSelectionComplete={handleSelectionComplete}
|
||
onCancel={() => setIsSelectionMode(false)}
|
||
/>
|
||
)}
|
||
|
||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2 bg-white/90 border border-slate-200 px-3 py-1.5 rounded-full shadow-lg z-40">
|
||
<div className="flex items-center border-r border-slate-200 pr-2 mr-2">
|
||
<button
|
||
onClick={handleZoomOut}
|
||
className="p-1 hover:bg-slate-100 rounded text-slate-600 min-w-[28px] flex items-center justify-center"
|
||
title={t('zoomOut')}
|
||
>
|
||
<span className="text-lg">−</span>
|
||
</button>
|
||
<span className="mx-1 text-sm text-slate-600 min-w-[40px] text-center">{Math.round(zoomLevel * 100)}%</span>
|
||
<button
|
||
onClick={handleZoomIn}
|
||
className="p-1 hover:bg-slate-100 rounded text-slate-600 min-w-[28px] flex items-center justify-center"
|
||
title={t('zoomIn')}
|
||
>
|
||
<span className="text-lg">+</span>
|
||
</button>
|
||
<button
|
||
onClick={handleResetZoom}
|
||
className="ml-1 p-1 px-2 hover:bg-slate-100 rounded text-slate-600 text-xs"
|
||
title={t('resetZoom')}
|
||
>
|
||
100%
|
||
</button>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
const newPage = Math.max(1, currentPage - 1);
|
||
setCurrentPage(newPage);
|
||
}}
|
||
className="p-1 hover:bg-slate-100 rounded text-slate-600"
|
||
>
|
||
<ChevronLeft size={16} />
|
||
</button>
|
||
<div className="flex items-center gap-1">
|
||
<input
|
||
type="number"
|
||
value={currentPage}
|
||
onChange={(e) => {
|
||
const val = Math.max(1, Math.min(numPages, parseInt(e.target.value) || 1));
|
||
setCurrentPage(val);
|
||
}}
|
||
className="w-12 text-center text-sm border-none focus:ring-0 bg-transparent font-medium"
|
||
/>
|
||
<span className="text-sm text-slate-500">/ {numPages}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
const newPage = Math.min(numPages, currentPage + 1);
|
||
setCurrentPage(newPage);
|
||
}}
|
||
className="p-1 hover:bg-slate-100 rounded text-slate-600"
|
||
>
|
||
<ChevronRight size={16} />
|
||
</button>
|
||
</div>
|
||
|
||
{selectionData && (
|
||
<CreateNoteFromPDFDialog
|
||
screenshot={selectionData.screenshot}
|
||
extractedText={selectionData.text}
|
||
authToken={authToken}
|
||
initialCategoryId={undefined} // Notes don't inherit KB group
|
||
initialPageNumber={currentPage}
|
||
onSave={handleSaveNote}
|
||
onCancel={() => setSelectionData(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={`fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[999] ${isFullscreen ? 'p-0' : 'p-4'
|
||
}`}>
|
||
<div className={`bg-white rounded-lg overflow-hidden flex flex-col ${isFullscreen ? 'w-full h-full' : 'w-full max-w-4xl h-5/6'
|
||
}`}>
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
|
||
<div className="flex items-center space-x-3">
|
||
<FileText size={20} className="text-gray-600" />
|
||
<div>
|
||
<div className="font-medium text-gray-900">{fileName}</div>
|
||
<div className="text-sm text-gray-500">{t('pdfPreview')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
{status.status === 'ready' && !iframeError && (
|
||
<>
|
||
<div className="flex items-center gap-2 mr-2 border-r pr-2">
|
||
<span className="text-sm text-gray-500">{t('selectPageNumber')}</span>
|
||
<input
|
||
type="number"
|
||
min={1}
|
||
max={numPages}
|
||
value={currentPage}
|
||
onChange={(e) => {
|
||
const val = Math.max(1, Math.min(numPages, parseInt(e.target.value) || 1));
|
||
setCurrentPage(val);
|
||
}}
|
||
className="w-16 px-2 py-1 border rounded text-sm"
|
||
title={t('enterPageNumber')}
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={() => setIsSelectionMode(!isSelectionMode)}
|
||
className={`p-2 transition-colors ${isSelectionMode ? 'bg-blue-100 text-blue-600 rounded' : 'text-gray-400 hover:text-blue-600'}`}
|
||
title={isSelectionMode ? t('exitSelectionMode') : t('clickToSelectAndNote')}
|
||
>
|
||
<Scissors size={18} />
|
||
</button>
|
||
<button
|
||
onClick={handleRegenerate}
|
||
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
||
title={t('regeneratePDF')}
|
||
>
|
||
<RefreshCw size={18} />
|
||
</button>
|
||
<button
|
||
onClick={handleDownload}
|
||
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||
title={t('downloadPDF')}
|
||
>
|
||
<Download size={18} />
|
||
</button>
|
||
<button
|
||
onClick={handleOpenInNewTab}
|
||
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||
title={t('openInNewWindow')}
|
||
>
|
||
<ExternalLink size={18} />
|
||
</button>
|
||
<button
|
||
onClick={handleFullscreen}
|
||
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||
title={isFullscreen ? t('exitFullscreen') : t('fullscreenDisplay')}
|
||
>
|
||
<Maximize2 size={18} />
|
||
</button>
|
||
</>
|
||
)}
|
||
<button
|
||
onClick={onClose}
|
||
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||
>
|
||
<X size={18} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content Area */}
|
||
<div className="flex-1 h-full">
|
||
{renderContent()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
interface PDFPreviewButtonProps {
|
||
fileId: string;
|
||
fileName: string;
|
||
onPreview: () => void;
|
||
}
|
||
|
||
export const PDFPreviewButton: React.FC<PDFPreviewButtonProps> = ({
|
||
fileId,
|
||
fileName,
|
||
onPreview
|
||
}) => {
|
||
const [status, setStatus] = useState<PDFStatus>({ status: 'pending' });
|
||
const [loading, setLoading] = useState(true);
|
||
const { t } = useLanguage();
|
||
const isSupported = isFormatSupportedForPreview(fileName);
|
||
|
||
useEffect(() => {
|
||
if (isSupported) {
|
||
checkStatus();
|
||
} else {
|
||
setLoading(false);
|
||
}
|
||
}, [fileId, isSupported]);
|
||
|
||
const checkStatus = async () => {
|
||
try {
|
||
const pdfStatus = await pdfPreviewService.getPDFStatus(fileId);
|
||
setStatus(pdfStatus);
|
||
} catch (error) {
|
||
// Ignore error and use default state
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const getIcon = () => {
|
||
if (!isSupported) {
|
||
return <Eye className="w-3 h-3 text-slate-200" />;
|
||
}
|
||
if (loading || status.status === 'converting') {
|
||
return <Loader className="w-3 h-3 animate-spin" />;
|
||
}
|
||
if (status.status === 'failed') {
|
||
return <AlertCircle className="w-3 h-3" />;
|
||
}
|
||
return <Eye className="w-3 h-3" />;
|
||
};
|
||
|
||
const getTitle = () => {
|
||
if (!isSupported) return t('previewNotSupported');
|
||
switch (status.status) {
|
||
case 'ready': return t('pdfPreviewReady');
|
||
case 'converting': return t('convertingInProgress');
|
||
case 'failed': return t('conversionFailed');
|
||
default: return t('generatePDFPreviewButton');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<button
|
||
onClick={onPreview}
|
||
disabled={loading || status.status === 'converting' || !isSupported}
|
||
className={`p-1 rounded transition-colors ${!isSupported
|
||
? 'text-slate-200 cursor-not-allowed'
|
||
: status.status === 'failed'
|
||
? 'text-red-400 hover:text-red-500 hover:bg-red-50'
|
||
: 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'
|
||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||
title={getTitle()}
|
||
>
|
||
{getIcon()}
|
||
</button>
|
||
);
|
||
}; |