frontend_nyla/src/pages/views/workspace/FilePreview.tsx
2026-03-15 23:38:44 +01:00

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