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 (
);
}
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}
/>
);
}