import { useState, useEffect, useMemo, Suspense } from 'react'; import { Editor } from '@/lib/monacoLoader'; import { AlertCircle, FileIcon, Download, Loader2, Save } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { toast } from '@/components/ui/toast-store'; import { readStackFile, writeStackFile, downloadStackFile } from '@/lib/stackFilesApi'; import { extensionToLanguage } from '@/lib/monacoLanguages'; import { formatBytes } from '@/lib/utils'; interface FileViewerProps { stackName: string; selectedPath: string | null; canEdit: boolean; isDarkMode: boolean; onSaved?: () => void; } function getFilename(path: string): string { return path.split('/').pop() ?? path; } interface SpecialFilePanelProps { filename: string; size: number; label: string; stackName: string; relPath: string; } function SpecialFilePanel({ filename, size, label, stackName, relPath, }: SpecialFilePanelProps) { const [downloading, setDownloading] = useState(false); const handleDownload = async () => { setDownloading(true); try { const res = await downloadStackFile(stackName, relPath); if (!res.ok) { toast.error('Download failed.'); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 100); } catch (e) { toast.error(e instanceof Error ? e.message : 'Download failed.'); } finally { setDownloading(false); } }; return (

{filename}

{label} · {formatBytes(size)}

); } export function FileViewer({ stackName, selectedPath, canEdit, isDarkMode, onSaved, }: FileViewerProps) { const [content, setContent] = useState(''); const [originalContent, setOriginalContent] = useState(''); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [isBinary, setIsBinary] = useState(false); const [isOversized, setIsOversized] = useState(false); const [size, setSize] = useState(0); const readOnly = !canEdit; const editorOptions = useMemo( () => ({ readOnly, minimap: { enabled: false }, fontFamily: "'Geist Mono', monospace", fontSize: 13, padding: { top: 8 }, scrollBeyondLastLine: false, }), [readOnly], ); useEffect(() => { if (!selectedPath) { setContent(''); setOriginalContent(''); setIsBinary(false); setIsOversized(false); setError(null); return; } let cancelled = false; setLoading(true); setError(null); setIsBinary(false); setIsOversized(false); readStackFile(stackName, selectedPath) .then((result) => { if (cancelled) return; setSize(result.size); if (result.binary) { setIsBinary(true); } else if (result.oversized) { setIsOversized(true); } else { const text = result.content ?? ''; setContent(text); setOriginalContent(text); } }) .catch((e: unknown) => { if (cancelled) return; setError(e instanceof Error ? e.message : 'Failed to load file.'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [stackName, selectedPath]); const handleSave = async () => { if (!selectedPath) return; setSaving(true); const loadingId = toast.loading('Saving...'); try { await writeStackFile(stackName, selectedPath, content); setOriginalContent(content); toast.success('Saved.'); onSaved?.(); } catch (e) { toast.error(e instanceof Error ? e.message : 'Save failed.'); } finally { toast.dismiss(loadingId); setSaving(false); } }; if (!selectedPath) { return (
Select a file to view it
); } if (loading) { return (
); } if (error) { return (

{error}

); } const filename = getFilename(selectedPath); const language = extensionToLanguage(filename); if (isBinary) { return ( ); } if (isOversized) { return ( ); } const hasChanges = content !== originalContent; return (
{filename}
{readOnly && ( Read-only )} {!readOnly && ( )}
}> { if (!readOnly) setContent(val ?? ''); }} theme={isDarkMode ? 'vs-dark' : 'light'} options={editorOptions} />
); }