added file preview

This commit is contained in:
Ida Dittrich 2025-09-08 16:46:59 +02:00
parent 8341c2e860
commit bf2c2f982c
11 changed files with 368 additions and 17 deletions

View file

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

View file

@ -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')}
/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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