import { useEffect, useRef, useState, type KeyboardEvent } from 'react'; import type { ShellEntry } from '../client/useAgent'; import { iconFor, shortPath } from '../lib/format'; import { makeZip } from '../lib/zip'; interface Props { files: Record; shell: ShellEntry[]; busy: string | null; onSave: (path: string, content: string) => Promise; onRun: (command: string) => Promise; onClearShell: () => void; onError: (message: string) => void; } const MAX_BYTES = 1024 ** 2; export function WorkspacePanel({ files, shell, busy, onSave, onRun, onClearShell, onError }: Props) { const paths = Object.keys(files).sort(); const [selected, setSelected] = useState(null); const [text, setText] = useState(''); const [dirty, setDirty] = useState(false); const [adding, setAdding] = useState(false); const [newName, setNewName] = useState(''); const [command, setCommand] = useState(''); const history = useRef([]); const cursor = useRef(-1); const outRef = useRef(null); // Chọn file đầu tiên khi có dữ liệu, hoặc khi file đang chọn bị xoá. useEffect(() => { if (paths.length === 0) return; if (selected === null || !(selected in files)) { setSelected(paths.find((p) => p.endsWith('README.md')) ?? paths[0]); setDirty(false); } }, [paths.join('|')]); // eslint-disable-line react-hooks/exhaustive-deps // Đồng bộ nội dung editor khi agent/shell đổi file (không ghi đè nếu bạn đang sửa dở). useEffect(() => { if (selected !== null && !dirty) setText(files[selected] ?? ''); }, [selected, files, dirty]); useEffect(() => { const el = outRef.current; if (el) el.scrollTop = el.scrollHeight; }, [shell]); const save = async () => { if (selected === null) return; if (await onSave(selected, text)) setDirty(false); }; const choose = (path: string) => { if (dirty && !window.confirm('Discard unsaved changes to this file?')) return; setSelected(path); setDirty(false); }; const create = async (e: { preventDefault(): void }) => { e.preventDefault(); const name = newName.trim().replace(/^\/+/, ''); if (!name) return; const path = '/workspace/' + name; if (await onSave(path, '')) { setSelected(path); setDirty(false); setAdding(false); setNewName(''); } }; const importFiles = async (list: FileList | null) => { for (const f of Array.from(list ?? [])) { if (f.size > MAX_BYTES) { onError(`${f.name} is larger than 1 MiB.`); continue; } await onSave('/workspace/' + f.name, await f.text()); } }; const exportZip = () => { const url = URL.createObjectURL(makeZip(files)); const a = document.createElement('a'); a.href = url; a.download = 'pi-workspace.zip'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }; const run = async () => { const cmd = command.trim(); if (!cmd) return; history.current.push(cmd); cursor.current = history.current.length; setCommand(''); await onRun(cmd); }; const onShellKey = (e: KeyboardEvent) => { if (e.nativeEvent.isComposing) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void run(); } else if (e.key === 'ArrowUp' && !command.includes('\n') && cursor.current > 0) { e.preventDefault(); cursor.current -= 1; setCommand(history.current[cursor.current]); } else if (e.key === 'ArrowDown' && !command.includes('\n') && cursor.current >= 0) { e.preventDefault(); cursor.current += 1; setCommand(history.current[cursor.current] ?? ''); } else if (e.key === 'l' && e.ctrlKey) { e.preventDefault(); onClearShell(); } }; return (