added file preview
This commit is contained in:
parent
8341c2e860
commit
bf2c2f982c
11 changed files with 368 additions and 17 deletions
|
|
@ -305,6 +305,9 @@
|
|||
display: flex;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jsonTableValue {
|
||||
|
|
@ -325,6 +328,9 @@
|
|||
background: transparent;
|
||||
line-height: 1.4;
|
||||
width: 100%;
|
||||
|
||||
word-wrap: break-word;
|
||||
white-space: -wrap;
|
||||
}
|
||||
|
||||
.jsonValue {
|
||||
|
|
@ -347,7 +353,8 @@
|
|||
font-weight: 600;
|
||||
font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
white-space: nowrap !important;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.jsonValueNumber {
|
||||
|
|
@ -750,3 +757,85 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Text Container Styles */
|
||||
.textContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.textHeader {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.textTitle {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.warningMessage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.1rem;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.warningText {
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.textPreview {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
background: var(--color-background);
|
||||
overflow: auto;
|
||||
font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.textCode {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
white-space: inherit;
|
||||
word-wrap: inherit;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
|
|||
|
||||
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
|
||||
|
||||
|
||||
import { Popup, PopupAction } from '../Popup/Popup';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { useFileOperations } from '../../hooks/useFiles';
|
||||
|
|
@ -68,13 +67,16 @@ export function FilePreview({
|
|||
try {
|
||||
setError(null);
|
||||
setPreviewContent(null);
|
||||
const result = await handleFilePreview(fileId, fileName);
|
||||
const result = await handleFilePreview(fileId, fileName, mimeType);
|
||||
|
||||
if (result.success && result.previewUrl) {
|
||||
setPreviewUrl(result.previewUrl);
|
||||
if (result.success) {
|
||||
if (result.previewUrl) {
|
||||
setPreviewUrl(result.previewUrl);
|
||||
}
|
||||
if (result.decodedContent) {
|
||||
setPreviewContent(result.decodedContent);
|
||||
}
|
||||
// If it's text content but MIME type says PDF, we'll handle it in renderPreview
|
||||
} else {
|
||||
setError(result.error || 'Failed to load preview');
|
||||
}
|
||||
|
|
@ -115,9 +117,12 @@ export function FilePreview({
|
|||
const isPreviewing = previewingFiles.has(fileId);
|
||||
const hasError = error || previewError;
|
||||
|
||||
// Check if this is a corrupted PDF (text content instead of PDF)
|
||||
const isCorruptedPdf = mimeType === 'application/pdf' && previewContent && !previewUrl;
|
||||
|
||||
// Create action buttons for the popup header
|
||||
const actions: PopupAction[] = [
|
||||
// Copy Content Button - only show for text-based files (exclude PDFs)
|
||||
// Copy Content Button - only show for text-based files (exclude PDFs) or corrupted PDFs
|
||||
...(mimeType !== 'application/pdf' && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{
|
||||
label: copySuccess ? t('files.preview.copied', 'Copied!') : t(''),
|
||||
icon: copySuccess ? '✓' : <IoIosCopy />,
|
||||
|
|
@ -126,18 +131,31 @@ export function FilePreview({
|
|||
variant: 'primary' as const
|
||||
}] : []),
|
||||
|
||||
// Download Button
|
||||
{
|
||||
// Download Button - hide for corrupted PDFs
|
||||
...(isCorruptedPdf ? [] : [{
|
||||
label: String(''),
|
||||
icon: downloadingFiles.has(fileId) ? undefined : <IoIosDownload />,
|
||||
onClick: handleDownloadFile,
|
||||
disabled: downloadingFiles.has(fileId),
|
||||
loading: downloadingFiles.has(fileId),
|
||||
variant: 'success' as const
|
||||
}
|
||||
}])
|
||||
];
|
||||
|
||||
const renderPreview = () => {
|
||||
// Handle text content in PDF files (corrupted files) - check this first
|
||||
if (previewContent && !previewUrl && mimeType === 'application/pdf') {
|
||||
console.log('🔍 FilePreview: Rendering corrupted PDF with text content');
|
||||
return (
|
||||
<PdfRenderer
|
||||
previewUrl={undefined}
|
||||
previewContent={previewContent}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load PDF preview')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!previewUrl) {
|
||||
if (isPreviewing) {
|
||||
return <LoadingRenderer />;
|
||||
|
|
@ -200,9 +218,16 @@ export function FilePreview({
|
|||
|
||||
case 'application':
|
||||
if (mimeType === 'application/pdf') {
|
||||
console.log('🔍 FilePreview passing normal PDF to PdfRenderer:', {
|
||||
previewUrl,
|
||||
previewContent: previewContent ? `${previewContent.substring(0, 50)}...` : null,
|
||||
fileName,
|
||||
mimeType
|
||||
});
|
||||
return (
|
||||
<PdfRenderer
|
||||
previewUrl={previewUrl}
|
||||
previewContent={previewContent || undefined}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load PDF preview')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,53 @@
|
|||
import { IoIosWarning } from 'react-icons/io';
|
||||
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||
import styles from '../FilePreview.module.css';
|
||||
|
||||
interface PdfRendererProps {
|
||||
previewUrl: string;
|
||||
previewUrl?: string;
|
||||
previewContent?: string;
|
||||
fileName: string;
|
||||
onError: () => void;
|
||||
}
|
||||
|
||||
export function PdfRenderer({ previewUrl, fileName, onError }: PdfRendererProps) {
|
||||
export function PdfRenderer({ previewUrl, previewContent, fileName, onError }: PdfRendererProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
|
||||
const handleLoad = () => {
|
||||
console.log('📄 PDF iframe loaded successfully:', { previewUrl, fileName });
|
||||
};
|
||||
|
||||
const handleError = (event: React.SyntheticEvent<HTMLIFrameElement, Event>) => {
|
||||
console.error('❌ PDF iframe failed to load:', { previewUrl, fileName, event });
|
||||
onError();
|
||||
};
|
||||
|
||||
// Handle corrupted PDF files (text content instead of PDF)
|
||||
if (previewContent && !previewUrl) {
|
||||
console.log('📄 Rendering corrupted PDF warning');
|
||||
return (
|
||||
<div className={styles.textContainer}>
|
||||
<div className={styles.textHeader}>
|
||||
<div className={styles.warningMessage}>
|
||||
<span className={styles.warningIcon}><IoIosWarning /></span>
|
||||
<span className={styles.warningText}>
|
||||
{t('files.preview.pdfFileCorrupted', 'This file appears to be corrupted. It has a PDF extension but contains text content. Please re-upload the file if possible.')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Normal PDF rendering
|
||||
return (
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
className={styles.previewIframe}
|
||||
title={`Preview of ${fileName}`}
|
||||
data-mime-type="application/pdf"
|
||||
onError={onError}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,33 @@
|
|||
import styles from '../FilePreview.module.css';
|
||||
|
||||
// Updated to handle both previewUrl and previewContent
|
||||
|
||||
interface TextRendererProps {
|
||||
previewUrl: string;
|
||||
previewUrl?: string;
|
||||
previewContent?: string;
|
||||
fileName: string;
|
||||
mimeType?: string;
|
||||
onError: () => void;
|
||||
}
|
||||
|
||||
export function TextRenderer({ previewUrl, fileName, mimeType, onError }: TextRendererProps) {
|
||||
export function TextRenderer({ previewUrl, previewContent, fileName, mimeType, onError }: TextRendererProps) {
|
||||
// If we have previewContent directly, display it as text
|
||||
if (previewContent && !previewUrl) {
|
||||
return (
|
||||
<div className={styles.textContainer}>
|
||||
<div className={styles.textHeader}>
|
||||
<span className={styles.textTitle}>Text Preview</span>
|
||||
</div>
|
||||
<pre className={styles.textPreview}>
|
||||
<code className={styles.textCode}>
|
||||
{previewContent}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, use iframe with previewUrl
|
||||
return (
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ export function useApiRequest<RequestData = any, ResponseData = any>() {
|
|||
params,
|
||||
...additionalConfig
|
||||
});
|
||||
|
||||
// For blob responses, return the blob data directly
|
||||
if (additionalConfig.responseType === 'blob') {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const errorMessage = formatApiError(error, `Fehler bei ${method.toUpperCase()} ${url}`);
|
||||
|
|
|
|||
|
|
@ -418,14 +418,185 @@ export function useFileOperations() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleFilePreview = async (fileId: string, fileName: string) => {
|
||||
const handleFilePreview = async (fileId: string, fileName: string, mimeType?: string) => {
|
||||
setPreviewError(null);
|
||||
setPreviewingFiles(prev => new Set(prev).add(fileId));
|
||||
|
||||
try {
|
||||
console.log(`👁️ Starting preview for file: ${fileName} (ID: ${fileId})`);
|
||||
console.log(`👁️ Starting preview for file: ${fileName} (ID: ${fileId})`, { mimeType });
|
||||
|
||||
// First try to get JSON response (for text-based files)
|
||||
// For PDF files, try JSON response first (API returns base64-encoded PDF)
|
||||
if (mimeType === 'application/pdf') {
|
||||
console.log('📄 PDF file detected, trying JSON response with base64 content');
|
||||
|
||||
try {
|
||||
const jsonResponse = await request({
|
||||
url: `/api/files/${fileId}/preview`,
|
||||
method: 'get',
|
||||
additionalConfig: {
|
||||
responseType: 'json',
|
||||
validateStatus: function (status: number) {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📄 PDF JSON response received:', {
|
||||
hasContent: 'content' in jsonResponse,
|
||||
hasMimeType: 'mimeType' in jsonResponse,
|
||||
contentLength: jsonResponse.content?.length,
|
||||
mimeType: jsonResponse.mimeType,
|
||||
contentType: typeof jsonResponse.content,
|
||||
contentStartsWith: jsonResponse.content?.substring(0, 10),
|
||||
fullResponse: jsonResponse
|
||||
});
|
||||
|
||||
// Check if response has base64-encoded PDF content
|
||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
||||
let content = jsonResponse.content;
|
||||
const responseMimeType = jsonResponse.mimeType || 'application/pdf';
|
||||
|
||||
// The content field contains base64-encoded JSON, so decode it first
|
||||
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
||||
console.log('📄 Content appears to be base64-encoded, decoding first...');
|
||||
try {
|
||||
const decodedJsonString = atob(content);
|
||||
console.log('📄 Decoded JSON string:', {
|
||||
length: decodedJsonString.length,
|
||||
startsWith: decodedJsonString.substring(0, 20),
|
||||
isJson: decodedJsonString.startsWith('{')
|
||||
});
|
||||
|
||||
// Parse the decoded JSON string
|
||||
const nestedJson = JSON.parse(decodedJsonString);
|
||||
console.log('📄 Parsed nested JSON:', {
|
||||
hasContent: 'content' in nestedJson,
|
||||
hasDocumentCount: 'documentCount' in nestedJson,
|
||||
keys: Object.keys(nestedJson)
|
||||
});
|
||||
|
||||
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
|
||||
const innerContent = nestedJson.content;
|
||||
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
|
||||
|
||||
console.log('📄 Extracted inner content:', {
|
||||
innerContentLength: innerContent?.length,
|
||||
innerContentPreview: innerContent?.substring(0, 100) + '...',
|
||||
isBase64: isBase64
|
||||
});
|
||||
|
||||
if (isBase64) {
|
||||
// It's base64-encoded PDF content
|
||||
content = innerContent;
|
||||
} else {
|
||||
// It's plain text content, not a PDF
|
||||
console.log('📄 Inner content is plain text, not PDF. This appears to be a text file with PDF extension.');
|
||||
// Return the text content for the FilePreview to handle as text
|
||||
return {
|
||||
success: true,
|
||||
previewUrl: null,
|
||||
blob: null,
|
||||
isJsonContent: true,
|
||||
decodedContent: innerContent,
|
||||
isTextContent: true
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.warn('⚠️ Failed to decode base64 content or parse JSON:', decodeError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📄 Processing base64 PDF content:', {
|
||||
contentLength: content?.length,
|
||||
mimeType: responseMimeType,
|
||||
contentPreview: content?.substring(0, 100) + '...',
|
||||
firstChars: content?.substring(0, 20),
|
||||
lastChars: content?.substring(content.length - 20),
|
||||
isBase64: /^[A-Za-z0-9+/=]+$/.test(content)
|
||||
});
|
||||
|
||||
// Decode base64 content
|
||||
let decodedContent;
|
||||
try {
|
||||
decodedContent = atob(content);
|
||||
console.log('📄 Base64 decode successful:', {
|
||||
originalLength: content.length,
|
||||
decodedLength: decodedContent.length,
|
||||
firstBytes: decodedContent.substring(0, 10),
|
||||
firstBytesHex: Array.from(decodedContent.substring(0, 10)).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ')
|
||||
});
|
||||
|
||||
// Verify it's actually a PDF
|
||||
const isPDF = decodedContent.startsWith('%PDF');
|
||||
console.log('📄 PDF header verification:', {
|
||||
isPDF: isPDF,
|
||||
firstBytes: decodedContent.substring(0, 4),
|
||||
firstBytesHex: Array.from(decodedContent.substring(0, 4)).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' '),
|
||||
first20Chars: decodedContent.substring(0, 20)
|
||||
});
|
||||
|
||||
if (!isPDF) {
|
||||
console.warn('⚠️ Decoded content does not appear to be a valid PDF');
|
||||
console.log('📄 Full decoded content preview:', decodedContent.substring(0, 200));
|
||||
}
|
||||
|
||||
} catch (decodeError) {
|
||||
console.error('❌ Failed to decode base64 PDF content:', decodeError);
|
||||
throw new Error('Failed to decode PDF content');
|
||||
}
|
||||
|
||||
// Create a blob from the decoded PDF content
|
||||
// Convert string to Uint8Array for proper binary handling
|
||||
const uint8Array = new Uint8Array(decodedContent.length);
|
||||
for (let i = 0; i < decodedContent.length; i++) {
|
||||
uint8Array[i] = decodedContent.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([uint8Array], { type: 'application/pdf' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
console.log('🔗 Created PDF blob URL from base64:', url, {
|
||||
blobSize: blob.size,
|
||||
blobType: blob.type,
|
||||
url: url,
|
||||
uint8ArrayLength: uint8Array.length,
|
||||
firstBytes: Array.from(uint8Array.slice(0, 4)).map(b => String.fromCharCode(b)).join(''),
|
||||
firstBytesHex: Array.from(uint8Array.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
||||
});
|
||||
|
||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
||||
} else {
|
||||
throw new Error('No content field in PDF response');
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.log('📄 JSON PDF response failed, trying blob response...', jsonError);
|
||||
|
||||
// Fallback to blob response
|
||||
const previewData = await request({
|
||||
url: `/api/files/${fileId}/preview`,
|
||||
method: 'get',
|
||||
additionalConfig: {
|
||||
responseType: 'blob',
|
||||
validateStatus: function (status: number) {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ PDF blob preview successful for: ${fileName}`, {
|
||||
size: previewData.size,
|
||||
type: previewData.type,
|
||||
expectedType: 'application/pdf'
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(previewData);
|
||||
|
||||
return { success: true, previewUrl: url, blob: previewData, isJsonContent: false };
|
||||
}
|
||||
}
|
||||
|
||||
// For other files, first try to get JSON response (for text-based files)
|
||||
try {
|
||||
const jsonResponse = await request({
|
||||
url: `/api/files/${fileId}/preview`,
|
||||
|
|
|
|||
|
|
@ -375,6 +375,8 @@ export default {
|
|||
'files.preview.loading': 'Vorschau wird geladen...',
|
||||
'files.preview.unsupported': 'Vorschau für diesen Dateityp nicht verfügbar',
|
||||
'files.preview.error': 'Fehler beim Laden der Vorschau',
|
||||
'files.preview.textInPdfFile': 'Textvorschau',
|
||||
'files.preview.pdfFileCorrupted': 'Diese Datei scheint beschädigt zu sein. Sie hat eine PDF-Erweiterung, enthält aber Textinhalte. Bitte laden Sie die Datei erneut hoch, falls möglich.',
|
||||
|
||||
// Workflows Page
|
||||
'workflows.title': 'Workflows',
|
||||
|
|
|
|||
|
|
@ -378,6 +378,8 @@ export default {
|
|||
'files.preview.loading': 'Loading preview...',
|
||||
'files.preview.unsupported': 'Preview not available for this file type',
|
||||
'files.preview.error': 'Error loading preview',
|
||||
'files.preview.textInPdfFile': 'Text Preview',
|
||||
'files.preview.pdfFileCorrupted': 'This file appears to be corrupted. It has a PDF extension but contains text content. Please re-upload the file if possible.',
|
||||
|
||||
// Workflows Page
|
||||
'workflows.title': 'Workflows',
|
||||
|
|
|
|||
|
|
@ -378,6 +378,8 @@ export default {
|
|||
'files.preview.loading': 'Chargement de l\'aperçu...',
|
||||
'files.preview.unsupported': 'Aperçu non disponible pour ce type de fichier',
|
||||
'files.preview.error': 'Erreur lors du chargement de l\'aperçu',
|
||||
'files.preview.textInPdfFile': 'Aperçu du texte',
|
||||
'files.preview.pdfFileCorrupted': 'Ce fichier semble être corrompu. Il a une extension PDF mais contient du contenu texte. Veuillez le télécharger à nouveau si possible.',
|
||||
|
||||
// Workflows Page
|
||||
'workflows.title': 'Workflows',
|
||||
|
|
|
|||
Loading…
Reference in a new issue