finished file preview
This commit is contained in:
parent
bf2c2f982c
commit
5816a1b752
9 changed files with 362 additions and 150 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||||
import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css';
|
import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css';
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { FilePreview } from '../../FilePreview';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,102 +19,61 @@ const formatFileSize = (bytes?: number) => {
|
||||||
|
|
||||||
|
|
||||||
const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
|
const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
|
||||||
const [downloadingFiles, setDownloadingFiles] = useState<Set<string>>(new Set());
|
const [previewModalOpen, setPreviewModalOpen] = useState<boolean>(false);
|
||||||
const { request } = useApiRequest();
|
const [previewingFile, setPreviewingFile] = useState<Document | null>(null);
|
||||||
|
|
||||||
const handleDocumentClick = async (doc: Document) => {
|
const handlePreview = (doc: Document) => {
|
||||||
console.log('📋 Document object:', doc);
|
console.log('👁️ Opening preview for document:', doc);
|
||||||
|
|
||||||
// Extract the UUID file ID from the downloadUrl (same as files page)
|
// Extract the UUID file ID from the downloadUrl (same as files page)
|
||||||
let actualFileId: string | null = null;
|
let actualFileId: string | null = null;
|
||||||
let downloadUrl: string;
|
|
||||||
|
|
||||||
if (doc.downloadUrl) {
|
if (doc.downloadUrl) {
|
||||||
// Extract UUID from the downloadUrl: /api/workflows/files/{UUID}/download
|
// Extract UUID from the downloadUrl: /api/workflows/files/{UUID}/download
|
||||||
const match = doc.downloadUrl.match(/\/api\/workflows\/files\/([^\/]+)\/download/);
|
const match = doc.downloadUrl.match(/\/api\/workflows\/files\/([^\/]+)\/download/);
|
||||||
if (match) {
|
if (match) {
|
||||||
actualFileId = match[1];
|
actualFileId = match[1];
|
||||||
// Use the regular files endpoint with the UUID (same as files page)
|
|
||||||
downloadUrl = `/api/files/${actualFileId}/download`;
|
|
||||||
console.log(`🔗 Extracted UUID from downloadUrl: ${actualFileId}`);
|
console.log(`🔗 Extracted UUID from downloadUrl: ${actualFileId}`);
|
||||||
console.log(`🌐 Will use files endpoint: ${downloadUrl}`);
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Could not extract file ID from downloadUrl:', doc.downloadUrl);
|
console.error('Could not extract file ID from downloadUrl:', doc.downloadUrl);
|
||||||
alert('Could not extract file ID from download URL');
|
alert('Could not extract file ID from download URL');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (doc.id) {
|
||||||
|
// Fallback to using the document id directly
|
||||||
|
actualFileId = doc.id;
|
||||||
|
console.log(`🔗 Using document id as fileId: ${actualFileId}`);
|
||||||
} else {
|
} else {
|
||||||
console.error('No downloadUrl available in document');
|
console.error('No downloadUrl or id available in document');
|
||||||
alert('No download URL available');
|
alert('No file ID available for preview');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!actualFileId) {
|
if (!actualFileId) {
|
||||||
console.error('Could not determine actual file ID');
|
console.error('Could not determine actual file ID');
|
||||||
alert('Could not determine file ID for download');
|
alert('Could not determine file ID for preview');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent multiple downloads of the same file
|
// Create a modified document with the correct file ID
|
||||||
if (downloadingFiles.has(actualFileId)) {
|
const documentWithFileId = {
|
||||||
console.log('⏭️ Download already in progress for this file');
|
...doc,
|
||||||
return;
|
id: actualFileId
|
||||||
}
|
};
|
||||||
|
|
||||||
|
setPreviewingFile(documentWithFileId);
|
||||||
|
setPreviewModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
setDownloadingFiles(prev => new Set(prev).add(actualFileId));
|
const handleClosePreview = () => {
|
||||||
|
console.log('❌ Closing preview');
|
||||||
|
setPreviewModalOpen(false);
|
||||||
|
setPreviewingFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
const handleDocumentClick = (doc: Document) => {
|
||||||
console.log(`📥 Starting download for file: ${doc.name} (UUID: ${actualFileId})`);
|
console.log('📋 Document clicked, opening preview:', doc);
|
||||||
console.log(`🌐 Making request to: ${downloadUrl}`);
|
handlePreview(doc);
|
||||||
|
|
||||||
const blob = await request({
|
|
||||||
url: downloadUrl,
|
|
||||||
method: 'get',
|
|
||||||
additionalConfig: {
|
|
||||||
responseType: 'blob'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Download successful for: ${doc.name}`, {
|
|
||||||
size: blob.size,
|
|
||||||
type: blob.type
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create filename with extension
|
|
||||||
const fileName = doc.ext ? `${doc.name}.${doc.ext}` : doc.name;
|
|
||||||
|
|
||||||
// Create a download link and trigger the download
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`❌ Download failed for ${doc.name}:`, error);
|
|
||||||
console.error('Full error object:', error);
|
|
||||||
console.error('Error response:', error.response);
|
|
||||||
|
|
||||||
let errorMessage = 'Download failed';
|
|
||||||
if (error.response?.status === 404) {
|
|
||||||
errorMessage = `File "${doc.name}" not found or has been deleted. (404)`;
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
errorMessage = `No permission to download "${doc.name}". (403)`;
|
|
||||||
} else {
|
|
||||||
errorMessage = `Download failed: ${error.message || 'Unknown error'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setDownloadingFiles(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(actualFileId!);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -138,88 +97,92 @@ const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${messageStyles.message_item} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
<>
|
||||||
<div className={messageStyles.message_header}>
|
<div className={`${messageStyles.message_item} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||||
{isUser ? 'You' : message.agentName}
|
<div className={messageStyles.message_header}>
|
||||||
{message.timestamp && (
|
{isUser ? 'You' : message.agentName}
|
||||||
<span className={messageStyles.message_timestamp_inline}>
|
{message.timestamp && (
|
||||||
• {formatTimestamp(message.timestamp)}
|
<span className={messageStyles.message_timestamp_inline}>
|
||||||
</span>
|
• {formatTimestamp(message.timestamp)}
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className={`${messageStyles.message_bubble} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
|
||||||
<div className={messageStyles.message_content}>
|
|
||||||
{message.content || <span className={messageStyles.message_no_content}>[No message content]</span>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasDocuments && (
|
<div className={`${messageStyles.message_bubble} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||||
<div className={`${messageStyles.message_documents} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
<div className={messageStyles.message_content}>
|
||||||
<div className={`${messageStyles.message_documents_header} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
{message.content || <span className={messageStyles.message_no_content}>[No message content]</span>}
|
||||||
{isUser ? 'Uploaded' : 'Attached'} Files ({message.documents?.length || 0})
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
{hasDocuments && (
|
||||||
|
<div className={`${messageStyles.message_documents} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||||
|
<div className={`${messageStyles.message_documents_header} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||||
|
{isUser ? 'Uploaded' : 'Attached'} Files ({message.documents?.length || 0})
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
{message.documents!.map((doc, i) => {
|
{message.documents!.map((doc, i) => {
|
||||||
// Extract UUID from downloadUrl for download state tracking
|
|
||||||
let fileKey: string | null = null;
|
|
||||||
if (doc.downloadUrl) {
|
|
||||||
const match = doc.downloadUrl.match(/\/api\/workflows\/files\/([^\/]+)\/download/);
|
|
||||||
fileKey = match ? match[1] : null;
|
|
||||||
}
|
|
||||||
const isDownloading = fileKey && downloadingFiles.has(fileKey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={doc.id || i}
|
key={doc.id || i}
|
||||||
className={messageStyles.message_document_item}
|
className={messageStyles.message_document_item}
|
||||||
onClick={() => handleDocumentClick(doc)}
|
onClick={() => handleDocumentClick(doc)}
|
||||||
title={isDownloading ? 'Downloading...' : `Click to download ${doc.name}`}
|
title={`Click to preview ${doc.name}`}
|
||||||
style={{
|
style={{
|
||||||
cursor: isDownloading ? 'wait' : 'pointer',
|
cursor: 'pointer'
|
||||||
opacity: isDownloading ? 0.7 : 1
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={messageStyles.message_document_info}>
|
<div className={messageStyles.message_document_info}>
|
||||||
<div className={messageStyles.message_document_name}>
|
<div className={messageStyles.message_document_name}>
|
||||||
{doc.ext ? `${doc.name}.${doc.ext}` : doc.name}
|
{doc.ext ? `${doc.name}.${doc.ext}` : doc.name}
|
||||||
|
</div>
|
||||||
|
{doc.size && (
|
||||||
|
<div className={messageStyles.message_document_size}>
|
||||||
|
{formatFileSize(doc.size)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={messageStyles.message_document_actions}>
|
||||||
|
{/* Preview and Download buttons disabled for now
|
||||||
|
<button
|
||||||
|
onClick={(e) => handlePreview(doc, e)}
|
||||||
|
className={messageStyles.message_document_action_button}
|
||||||
|
title="Preview file"
|
||||||
|
>
|
||||||
|
<div className={messageStyles.message_document_action_icon}>
|
||||||
|
<FaEye size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleDownload(doc, e)}
|
||||||
|
disabled={isDownloading}
|
||||||
|
className={messageStyles.message_document_action_button}
|
||||||
|
title="Download file"
|
||||||
|
>
|
||||||
|
<div className={messageStyles.message_document_action_icon}>
|
||||||
|
<FaDownload size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
{doc.size && (
|
|
||||||
<div className={messageStyles.message_document_size}>
|
|
||||||
{formatFileSize(doc.size)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={messageStyles.message_document_actions}>
|
);
|
||||||
{/* Preview and Download buttons disabled for now
|
})}
|
||||||
<button
|
</div>
|
||||||
onClick={(e) => handlePreview(doc, e)}
|
|
||||||
className={messageStyles.message_document_action_button}
|
|
||||||
title="Preview file"
|
|
||||||
>
|
|
||||||
<div className={messageStyles.message_document_action_icon}>
|
|
||||||
<FaEye size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(e) => handleDownload(doc, e)}
|
|
||||||
disabled={isDownloading}
|
|
||||||
className={messageStyles.message_document_action_button}
|
|
||||||
title="Download file"
|
|
||||||
>
|
|
||||||
<div className={messageStyles.message_document_action_icon}>
|
|
||||||
<FaDownload size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* File Preview Modal */}
|
||||||
|
{previewingFile && (
|
||||||
|
<FilePreview
|
||||||
|
isOpen={previewModalOpen}
|
||||||
|
onClose={handleClosePreview}
|
||||||
|
fileId={previewingFile.id || ''}
|
||||||
|
fileName={previewingFile.ext ? `${previewingFile.name}.${previewingFile.ext}` : previewingFile.name}
|
||||||
|
mimeType={previewingFile.type}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
z-index: 1000;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -232,9 +232,9 @@
|
||||||
.valueContainer {
|
.valueContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: auto;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
min-width: 0;
|
min-width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsonValuePreview {
|
.jsonValuePreview {
|
||||||
|
|
@ -506,8 +506,8 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
width: 100%;
|
width: auto;
|
||||||
min-width: 100%;
|
min-width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -563,9 +563,11 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
word-break: keep-all !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
min-width: 0;
|
min-width: 20rem;
|
||||||
width: 100%;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nestedValueSummary {
|
.nestedValueSummary {
|
||||||
|
|
@ -578,8 +580,8 @@
|
||||||
.arrayItems {
|
.arrayItems {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 0px;
|
||||||
margin-top: 4px;
|
margin-left: -0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Array items when no key is shown (should span full width) */
|
/* Array items when no key is shown (should span full width) */
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ImageRenderer,
|
ImageRenderer,
|
||||||
TextRenderer,
|
TextRenderer,
|
||||||
PdfRenderer,
|
PdfRenderer,
|
||||||
|
HtmlRenderer,
|
||||||
ApplicationRenderer,
|
ApplicationRenderer,
|
||||||
UnsupportedRenderer,
|
UnsupportedRenderer,
|
||||||
LoadingRenderer,
|
LoadingRenderer,
|
||||||
|
|
@ -122,8 +123,8 @@ export function FilePreview({
|
||||||
|
|
||||||
// Create action buttons for the popup header
|
// Create action buttons for the popup header
|
||||||
const actions: PopupAction[] = [
|
const actions: PopupAction[] = [
|
||||||
// Copy Content Button - only show for text-based files (exclude PDFs) or corrupted PDFs
|
// Copy Content Button - only show for text-based files (exclude PDFs and images) or corrupted PDFs
|
||||||
...(mimeType !== 'application/pdf' && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{
|
...(mimeType !== 'application/pdf' && !mimeType?.startsWith('image/') && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{
|
||||||
label: copySuccess ? t('files.preview.copied', 'Copied!') : t(''),
|
label: copySuccess ? t('files.preview.copied', 'Copied!') : t(''),
|
||||||
icon: copySuccess ? '✓' : <IoIosCopy />,
|
icon: copySuccess ? '✓' : <IoIosCopy />,
|
||||||
onClick: handleCopyContent,
|
onClick: handleCopyContent,
|
||||||
|
|
@ -207,6 +208,17 @@ export function FilePreview({
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
|
// Special handling for HTML files
|
||||||
|
if (mimeType === 'text/html') {
|
||||||
|
return (
|
||||||
|
<HtmlRenderer
|
||||||
|
previewUrl={previewUrl}
|
||||||
|
fileName={fileName}
|
||||||
|
onError={() => setError('Failed to load HTML preview')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextRenderer
|
<TextRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl}
|
||||||
|
|
@ -234,6 +246,16 @@ export function FilePreview({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mimeType === 'application/html') {
|
||||||
|
return (
|
||||||
|
<HtmlRenderer
|
||||||
|
previewUrl={previewUrl}
|
||||||
|
fileName={fileName}
|
||||||
|
onError={() => setError('Failed to load HTML preview')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
41
src/components/FilePreview/renderers/HtmlRenderer.tsx
Normal file
41
src/components/FilePreview/renderers/HtmlRenderer.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import styles from '../FilePreview.module.css';
|
||||||
|
|
||||||
|
interface HtmlRendererProps {
|
||||||
|
previewUrl: string;
|
||||||
|
fileName: string;
|
||||||
|
onError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HtmlRenderer({ previewUrl, fileName, onError }: HtmlRendererProps) {
|
||||||
|
const handleLoad = () => {
|
||||||
|
console.log('🌐 HTML loaded successfully:', { previewUrl, fileName });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (event: React.SyntheticEvent<HTMLIFrameElement, Event>) => {
|
||||||
|
console.error('❌ HTML failed to load:', { previewUrl, fileName, event });
|
||||||
|
onError();
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🔍 HtmlRenderer props:', {
|
||||||
|
previewUrl,
|
||||||
|
fileName,
|
||||||
|
hasPreviewUrl: !!previewUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={previewUrl}
|
||||||
|
className={styles.previewIframe}
|
||||||
|
title={`Preview of ${fileName}`}
|
||||||
|
data-mime-type="text/html"
|
||||||
|
onLoad={handleLoad}
|
||||||
|
onError={handleError}
|
||||||
|
style={{
|
||||||
|
background: 'white !important',
|
||||||
|
border: 'none',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,28 @@ interface ImageRendererProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageRenderer({ previewUrl, fileName, onError }: ImageRendererProps) {
|
export function ImageRenderer({ previewUrl, fileName, onError }: ImageRendererProps) {
|
||||||
|
const handleLoad = () => {
|
||||||
|
console.log('🖼️ Image loaded successfully:', { previewUrl, fileName });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (event: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
||||||
|
console.error('❌ Image failed to load:', { previewUrl, fileName, event });
|
||||||
|
onError();
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🔍 ImageRenderer props:', {
|
||||||
|
previewUrl,
|
||||||
|
fileName,
|
||||||
|
hasPreviewUrl: !!previewUrl
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
alt={fileName}
|
alt={fileName}
|
||||||
className={styles.previewImage}
|
className={styles.previewImage}
|
||||||
onError={onError}
|
onLoad={handleLoad}
|
||||||
|
onError={handleError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
||||||
return value.length > 8;
|
return value.length > 8;
|
||||||
}
|
}
|
||||||
if (typeof value === 'object' && value !== null && 'keys' in value) {
|
if (typeof value === 'object' && value !== null && 'keys' in value) {
|
||||||
return value.keys.length > 2;
|
return value.keys.length > 1;
|
||||||
}
|
}
|
||||||
if (typeof value === 'object' && value !== null && 'isArray' in value) {
|
if (typeof value === 'object' && value !== null && 'isArray' in value) {
|
||||||
return value.length > 8;
|
return value.length > 8;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ export { JsonRenderer } from './JsonRenderer';
|
||||||
export { ImageRenderer } from './ImageRenderer';
|
export { ImageRenderer } from './ImageRenderer';
|
||||||
export { TextRenderer } from './TextRenderer';
|
export { TextRenderer } from './TextRenderer';
|
||||||
export { PdfRenderer } from './PdfRenderer';
|
export { PdfRenderer } from './PdfRenderer';
|
||||||
|
export { HtmlRenderer } from './HtmlRenderer';
|
||||||
export { ApplicationRenderer } from './ApplicationRenderer';
|
export { ApplicationRenderer } from './ApplicationRenderer';
|
||||||
export { UnsupportedRenderer } from './UnsupportedRenderer';
|
export { UnsupportedRenderer } from './UnsupportedRenderer';
|
||||||
export { LoadingRenderer } from './LoadingRenderer';
|
export { LoadingRenderer } from './LoadingRenderer';
|
||||||
|
|
|
||||||
|
|
@ -596,6 +596,173 @@ export function useFileOperations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For image files, try JSON response first (API returns base64-encoded images)
|
||||||
|
if (mimeType?.startsWith('image/')) {
|
||||||
|
console.log('🖼️ Image 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('🖼️ Image JSON response received:', {
|
||||||
|
hasContent: 'content' in jsonResponse,
|
||||||
|
hasMimeType: 'mimeType' in jsonResponse,
|
||||||
|
contentLength: jsonResponse.content?.length,
|
||||||
|
mimeType: jsonResponse.mimeType
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if response has base64-encoded image content
|
||||||
|
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
||||||
|
let content = jsonResponse.content;
|
||||||
|
const responseMimeType = jsonResponse.mimeType || mimeType;
|
||||||
|
|
||||||
|
// The content field contains base64-encoded data, 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 decodedString = atob(content);
|
||||||
|
console.log('🖼️ Decoded string:', {
|
||||||
|
length: decodedString.length,
|
||||||
|
startsWith: decodedString.substring(0, 20),
|
||||||
|
isJson: decodedString.startsWith('{'),
|
||||||
|
isImage: decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if it's JSON (nested structure) or direct image data
|
||||||
|
if (decodedString.startsWith('{')) {
|
||||||
|
// It's JSON, parse it
|
||||||
|
const nestedJson = JSON.parse(decodedString);
|
||||||
|
console.log('🖼️ Parsed nested JSON:', {
|
||||||
|
hasContent: 'content' 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 image content
|
||||||
|
content = innerContent;
|
||||||
|
} else {
|
||||||
|
throw new Error('Inner content is not base64-encoded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF') || decodedString.startsWith('GIF8') || decodedString.startsWith('RIFF')) {
|
||||||
|
// It's direct image data, use it as is
|
||||||
|
console.log('🖼️ Direct image data detected, using as is');
|
||||||
|
content = btoa(decodedString); // Re-encode as base64 for processing
|
||||||
|
} else {
|
||||||
|
throw new Error('Decoded content is neither JSON nor image data');
|
||||||
|
}
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.warn('⚠️ Failed to decode base64 content:', decodeError);
|
||||||
|
throw decodeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🖼️ Processing base64 image content:', {
|
||||||
|
contentLength: content?.length,
|
||||||
|
mimeType: responseMimeType,
|
||||||
|
contentPreview: content?.substring(0, 100) + '...',
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify it's actually an image by checking for common image headers
|
||||||
|
const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF');
|
||||||
|
const isPNG = decodedContent.startsWith('\x89PNG\r\n\x1a\n');
|
||||||
|
const isGIF = decodedContent.startsWith('GIF8');
|
||||||
|
const isWebP = decodedContent.startsWith('RIFF') && decodedContent.includes('WEBP');
|
||||||
|
|
||||||
|
console.log('🖼️ Image header verification:', {
|
||||||
|
isJPEG: isJPEG,
|
||||||
|
isPNG: isPNG,
|
||||||
|
isGIF: isGIF,
|
||||||
|
isWebP: isWebP,
|
||||||
|
firstBytes: decodedContent.substring(0, 4)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isJPEG && !isPNG && !isGIF && !isWebP) {
|
||||||
|
console.warn('⚠️ Decoded content does not appear to be a valid image');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.error('❌ Failed to decode base64 image content:', decodeError);
|
||||||
|
throw new Error('Failed to decode image content');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a blob from the decoded image 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: responseMimeType });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
console.log('🔗 Created image blob URL from base64:', url, {
|
||||||
|
blobSize: blob.size,
|
||||||
|
blobType: blob.type,
|
||||||
|
url: url,
|
||||||
|
uint8ArrayLength: uint8Array.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
||||||
|
} else {
|
||||||
|
throw new Error('No content field in image response');
|
||||||
|
}
|
||||||
|
} catch (jsonError) {
|
||||||
|
console.log('🖼️ JSON image 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(`✅ Image blob preview successful for: ${fileName}`, {
|
||||||
|
size: previewData.size,
|
||||||
|
type: previewData.type,
|
||||||
|
expectedType: mimeType
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
// For other files, first try to get JSON response (for text-based files)
|
||||||
try {
|
try {
|
||||||
const jsonResponse = await request({
|
const jsonResponse = await request({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue