feat: implement QuestionBank CRUD with pagination and template query
- 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
This commit is contained in:
@@ -0,0 +1,713 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user