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 { Message, Document } from "./dashboardChatAreaTypes";
|
||||
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 [downloadingFiles, setDownloadingFiles] = useState<Set<string>>(new Set());
|
||||
const { request } = useApiRequest();
|
||||
const [previewModalOpen, setPreviewModalOpen] = useState<boolean>(false);
|
||||
const [previewingFile, setPreviewingFile] = useState<Document | null>(null);
|
||||
|
||||
const handleDocumentClick = async (doc: Document) => {
|
||||
console.log('📋 Document object:', doc);
|
||||
const handlePreview = (doc: Document) => {
|
||||
console.log('👁️ Opening preview for document:', doc);
|
||||
|
||||
// Extract the UUID file ID from the downloadUrl (same as files page)
|
||||
let actualFileId: string | null = null;
|
||||
let downloadUrl: string;
|
||||
|
||||
if (doc.downloadUrl) {
|
||||
// Extract UUID from the downloadUrl: /api/workflows/files/{UUID}/download
|
||||
const match = doc.downloadUrl.match(/\/api\/workflows\/files\/([^\/]+)\/download/);
|
||||
if (match) {
|
||||
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(`🌐 Will use files endpoint: ${downloadUrl}`);
|
||||
} else {
|
||||
console.error('Could not extract file ID from downloadUrl:', doc.downloadUrl);
|
||||
alert('Could not extract file ID from download URL');
|
||||
return;
|
||||
}
|
||||
} else if (doc.id) {
|
||||
// Fallback to using the document id directly
|
||||
actualFileId = doc.id;
|
||||
console.log(`🔗 Using document id as fileId: ${actualFileId}`);
|
||||
} else {
|
||||
console.error('No downloadUrl available in document');
|
||||
alert('No download URL available');
|
||||
console.error('No downloadUrl or id available in document');
|
||||
alert('No file ID available for preview');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actualFileId) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Prevent multiple downloads of the same file
|
||||
if (downloadingFiles.has(actualFileId)) {
|
||||
console.log('⏭️ Download already in progress for this file');
|
||||
return;
|
||||
}
|
||||
// Create a modified document with the correct file ID
|
||||
const documentWithFileId = {
|
||||
...doc,
|
||||
id: actualFileId
|
||||
};
|
||||
|
||||
setPreviewingFile(documentWithFileId);
|
||||
setPreviewModalOpen(true);
|
||||
};
|
||||
|
||||
setDownloadingFiles(prev => new Set(prev).add(actualFileId));
|
||||
const handleClosePreview = () => {
|
||||
console.log('❌ Closing preview');
|
||||
setPreviewModalOpen(false);
|
||||
setPreviewingFile(null);
|
||||
};
|
||||
|
||||
try {
|
||||
console.log(`📥 Starting download for file: ${doc.name} (UUID: ${actualFileId})`);
|
||||
console.log(`🌐 Making request to: ${downloadUrl}`);
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
const handleDocumentClick = (doc: Document) => {
|
||||
console.log('📋 Document clicked, opening preview:', doc);
|
||||
handlePreview(doc);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -138,88 +97,92 @@ const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
|
|||
const isUser = message.role === 'user';
|
||||
|
||||
return (
|
||||
<div className={`${messageStyles.message_item} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||
<div className={messageStyles.message_header}>
|
||||
{isUser ? 'You' : message.agentName}
|
||||
{message.timestamp && (
|
||||
<span className={messageStyles.message_timestamp_inline}>
|
||||
• {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 className={`${messageStyles.message_item} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||
<div className={messageStyles.message_header}>
|
||||
{isUser ? 'You' : message.agentName}
|
||||
{message.timestamp && (
|
||||
<span className={messageStyles.message_timestamp_inline}>
|
||||
• {formatTimestamp(message.timestamp)}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
<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>
|
||||
|
||||
{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) => {
|
||||
// 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 (
|
||||
<div
|
||||
key={doc.id || i}
|
||||
className={messageStyles.message_document_item}
|
||||
onClick={() => handleDocumentClick(doc)}
|
||||
title={isDownloading ? 'Downloading...' : `Click to download ${doc.name}`}
|
||||
title={`Click to preview ${doc.name}`}
|
||||
style={{
|
||||
cursor: isDownloading ? 'wait' : 'pointer',
|
||||
opacity: isDownloading ? 0.7 : 1
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<div className={messageStyles.message_document_info}>
|
||||
<div className={messageStyles.message_document_name}>
|
||||
{doc.ext ? `${doc.name}.${doc.ext}` : doc.name}
|
||||
<div className={messageStyles.message_document_info}>
|
||||
<div className={messageStyles.message_document_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>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</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;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -232,9 +232,9 @@
|
|||
.valueContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
overflow: visible !important;
|
||||
min-width: 0;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
.jsonValuePreview {
|
||||
|
|
@ -506,8 +506,8 @@
|
|||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
overflow: visible !important;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: auto;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -563,9 +563,11 @@
|
|||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
white-space: nowrap !important;
|
||||
word-break: keep-all !important;
|
||||
overflow: visible !important;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
min-width: 20rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nestedValueSummary {
|
||||
|
|
@ -578,8 +580,8 @@
|
|||
.arrayItems {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
gap: 0px;
|
||||
margin-left: -0.4rem;
|
||||
}
|
||||
|
||||
/* Array items when no key is shown (should span full width) */
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
ImageRenderer,
|
||||
TextRenderer,
|
||||
PdfRenderer,
|
||||
HtmlRenderer,
|
||||
ApplicationRenderer,
|
||||
UnsupportedRenderer,
|
||||
LoadingRenderer,
|
||||
|
|
@ -122,8 +123,8 @@ export function FilePreview({
|
|||
|
||||
// Create action buttons for the popup header
|
||||
const actions: PopupAction[] = [
|
||||
// Copy Content Button - only show for text-based files (exclude PDFs) or corrupted PDFs
|
||||
...(mimeType !== 'application/pdf' && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{
|
||||
// Copy Content Button - only show for text-based files (exclude PDFs and images) or corrupted PDFs
|
||||
...(mimeType !== 'application/pdf' && !mimeType?.startsWith('image/') && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{
|
||||
label: copySuccess ? t('files.preview.copied', 'Copied!') : t(''),
|
||||
icon: copySuccess ? '✓' : <IoIosCopy />,
|
||||
onClick: handleCopyContent,
|
||||
|
|
@ -207,6 +208,17 @@ export function FilePreview({
|
|||
);
|
||||
|
||||
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 (
|
||||
<TextRenderer
|
||||
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 (
|
||||
|
|
|
|||
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) {
|
||||
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 (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={fileName}
|
||||
className={styles.previewImage}
|
||||
onError={onError}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
|||
return value.length > 8;
|
||||
}
|
||||
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) {
|
||||
return value.length > 8;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export { JsonRenderer } from './JsonRenderer';
|
|||
export { ImageRenderer } from './ImageRenderer';
|
||||
export { TextRenderer } from './TextRenderer';
|
||||
export { PdfRenderer } from './PdfRenderer';
|
||||
export { HtmlRenderer } from './HtmlRenderer';
|
||||
export { ApplicationRenderer } from './ApplicationRenderer';
|
||||
export { UnsupportedRenderer } from './UnsupportedRenderer';
|
||||
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)
|
||||
try {
|
||||
const jsonResponse = await request({
|
||||
|
|
|
|||
Loading…
Reference in a new issue