153 lines
5 KiB
TypeScript
153 lines
5 KiB
TypeScript
/**
|
|
* FilePreview -- File preview / editor panel in the right sidebar.
|
|
*
|
|
* Displays content preview for selected files based on their MIME type:
|
|
* - Text files: rendered as text with optional editing
|
|
* - Images: rendered as preview
|
|
* - PDFs: link to download
|
|
* - Other: metadata display
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import api from '../../../api';
|
|
import type { WorkspaceFile } from './useWorkspace';
|
|
|
|
interface FilePreviewProps {
|
|
instanceId: string;
|
|
fileId: string | null;
|
|
files: WorkspaceFile[];
|
|
}
|
|
|
|
export const FilePreview: React.FC<FilePreviewProps> = ({ instanceId, fileId, files }) => {
|
|
const [content, setContent] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
|
|
const file = fileId ? files.find(f => f.id === fileId) : null;
|
|
|
|
useEffect(() => {
|
|
setContent(null);
|
|
setPreviewUrl(null);
|
|
if (!file || !instanceId) return;
|
|
|
|
const isText = _isTextMime(file.mimeType);
|
|
const isImage = file.mimeType.startsWith('image/');
|
|
|
|
if (isText && file.fileSize < 500_000) {
|
|
setLoading(true);
|
|
api.get(`/api/files/${file.id}/download`, { responseType: 'text' })
|
|
.then(res => setContent(typeof res.data === 'string' ? res.data : JSON.stringify(res.data, null, 2)))
|
|
.catch(() => setContent(null))
|
|
.finally(() => setLoading(false));
|
|
} else if (isImage) {
|
|
const baseUrl = api.defaults.baseURL || '';
|
|
setPreviewUrl(`${baseUrl}/api/files/${file.id}/download`);
|
|
}
|
|
}, [file, instanceId]);
|
|
|
|
if (!file) {
|
|
return (
|
|
<div style={{ padding: 24, textAlign: 'center', color: '#999', fontSize: 13 }}>
|
|
Select a file to preview
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
{/* Header */}
|
|
<div style={{
|
|
padding: '8px 0',
|
|
borderBottom: '1px solid var(--border-color, #e0e0e0)',
|
|
marginBottom: 8,
|
|
}}>
|
|
<div style={{ fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{file.fileName}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: '#888', marginTop: 2, display: 'flex', gap: 12 }}>
|
|
<span>{file.mimeType}</span>
|
|
<span>{_formatFileSize(file.fileSize)}</span>
|
|
{file.status && <span style={{ color: file.status === 'ready' ? '#4caf50' : '#ff9800' }}>{file.status}</span>}
|
|
</div>
|
|
{file.description && (
|
|
<div style={{ fontSize: 12, color: '#555', marginTop: 4 }}>{file.description}</div>
|
|
)}
|
|
{file.tags && file.tags.length > 0 && (
|
|
<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
|
|
{file.tags.map(tag => (
|
|
<span key={tag} style={{ fontSize: 10, padding: '1px 6px', borderRadius: 3, background: '#e3f2fd', color: '#1565c0' }}>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content area */}
|
|
<div style={{ flex: 1, overflow: 'auto' }}>
|
|
{loading && (
|
|
<div style={{ padding: 16, textAlign: 'center', color: '#999', fontSize: 12 }}>Loading...</div>
|
|
)}
|
|
|
|
{content !== null && !loading && (
|
|
<pre style={{
|
|
fontSize: 12,
|
|
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-word',
|
|
lineHeight: 1.5,
|
|
padding: 8,
|
|
background: '#f8f9fa',
|
|
borderRadius: 4,
|
|
margin: 0,
|
|
maxHeight: '100%',
|
|
overflow: 'auto',
|
|
}}>
|
|
{content}
|
|
</pre>
|
|
)}
|
|
|
|
{previewUrl && (
|
|
<div style={{ textAlign: 'center', padding: 8 }}>
|
|
<img
|
|
src={previewUrl}
|
|
alt={file.fileName}
|
|
style={{ maxWidth: '100%', maxHeight: 400, borderRadius: 4, objectFit: 'contain' }}
|
|
onError={() => setPreviewUrl(null)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && content === null && !previewUrl && (
|
|
<div style={{ padding: 16, textAlign: 'center', color: '#999', fontSize: 12 }}>
|
|
{file.fileSize > 500_000
|
|
? 'File too large for inline preview'
|
|
: `No preview available for ${file.mimeType}`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function _isTextMime(mime: string): boolean {
|
|
if (mime.startsWith('text/')) return true;
|
|
const textTypes = [
|
|
'application/json',
|
|
'application/xml',
|
|
'application/javascript',
|
|
'application/typescript',
|
|
'application/x-python',
|
|
'application/x-yaml',
|
|
'application/yaml',
|
|
'application/sql',
|
|
'application/csv',
|
|
];
|
|
return textTypes.includes(mime);
|
|
}
|
|
|
|
function _formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|