2ns / frontend /app.jsx
proti0070's picture
Create frontend/app.jsx
98f6aac verified
const { useState, useEffect, useRef, useCallback } = React;
const API = '';
// ─── Utils ────────────────────────────────────────────────────────────────────
const fileIcon = (name) => {
if (!name) return 'πŸ“„';
const ext = name.split('.').pop()?.toLowerCase();
const icons = {
js: '🟨', jsx: 'βš›οΈ', ts: 'πŸ”·', tsx: 'βš›οΈ',
py: '🐍', html: '🌐', css: '🎨', json: 'πŸ“‹',
md: 'πŸ“', txt: 'πŸ“„', sh: '⚑', env: 'πŸ”’',
png: 'πŸ–ΌοΈ', jpg: 'πŸ–ΌοΈ', svg: '🎭', gif: 'πŸ–ΌοΈ',
gitignore: 'πŸ”§', dockerfile: '🐳',
};
return icons[ext] || 'πŸ“„';
};
const timestamp = () => new Date().toLocaleTimeString('en', {hour12: false});
const parseMarkdown = (text) => {
if (!text) return '';
try { return marked.parse(text); } catch { return text; }
};
// ─── Monaco Editor ───────────────────────────────────────────────────────────
const MonacoEditor = ({ value, onChange, language = 'javascript', path }) => {
const ref = useRef(null);
const editorRef = useRef(null);
const modelRef = useRef({});
useEffect(() => {
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' } });
require(['vs/editor/editor.main'], () => {
if (!ref.current) return;
const editor = monaco.editor.create(ref.current, {
value: value || '',
language,
theme: 'vs-dark',
fontSize: 13,
fontFamily: "'JetBrains Mono', monospace",
fontLigatures: true,
minimap: { enabled: window.innerWidth > 1200 },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 10 },
cursorStyle: 'line',
wordWrap: 'on',
tabSize: 2,
});
editor.onDidChangeModelContent(() => onChange?.(editor.getValue()));
editorRef.current = editor;
});
return () => editorRef.current?.dispose();
}, []);
useEffect(() => {
if (!editorRef.current) return;
const current = editorRef.current.getValue();
if (current !== value) editorRef.current.setValue(value || '');
const lang = getLanguage(path);
monaco.editor.setModelLanguage(editorRef.current.getModel(), lang);
}, [value, path]);
return <div ref={ref} style={{ height: '100%', width: '100%' }} />;
};
const getLanguage = (path) => {
if (!path) return 'plaintext';
const ext = path.split('.').pop()?.toLowerCase();
const map = { js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', html: 'html', css: 'css', json: 'json', md: 'markdown', sh: 'shell', txt: 'plaintext', yaml: 'yaml', yml: 'yaml' };
return map[ext] || 'plaintext';
};
// ─── File Tree ───────────────────────────────────────────────────────────────
const FileTree = ({ files, activeFile, onOpen, onRefresh, project }) => {
const [ctx, setCtx] = useState(null);
const [renaming, setRenaming] = useState(null);
const [renameVal, setRenameVal] = useState('');
const buildTree = (files) => {
const tree = {};
files.forEach(f => {
const parts = f.path.split('/');
let cur = tree;
parts.forEach((part, i) => {
if (!cur[part]) cur[part] = { _type: i === parts.length - 1 ? f.type : 'dir', _path: parts.slice(0, i + 1).join('/'), _children: {} };
cur = cur[part]._children;
});
});
return tree;
};
const renderTree = (tree, depth = 0) => {
return Object.entries(tree).map(([name, node]) => (
<div key={node._path}>
<div
className={`tree-item ${activeFile === node._path ? 'active' : ''}`}
style={{ paddingLeft: `${10 + depth * 14}px` }}
onClick={() => node._type === 'file' && onOpen(node._path)}
onContextMenu={(e) => { e.preventDefault(); setCtx({ x: e.clientX, y: e.clientY, path: node._path, type: node._type }); }}
>
<span>{node._type === 'dir' ? 'πŸ“' : fileIcon(name)}</span>
{renaming === node._path
? <input autoFocus value={renameVal} onChange={e => setRenameVal(e.target.value)}
onBlur={() => setRenaming(null)}
onKeyDown={e => { if (e.key === 'Enter') { /* rename api call */ setRenaming(null); } }}
style={{ background: 'var(--bg-4)', border: '1px solid var(--accent)', color: 'var(--text-0)', padding: '1px 4px', borderRadius: '3px', fontSize: '12px', fontFamily: 'var(--font-mono)', width: '100px' }}
/>
: <span>{name}</span>
}
</div>
{node._type === 'dir' && renderTree(node._children, depth + 1)}
</div>
));
};
const handleCtxAction = async (action) => {
if (!ctx) return;
if (action === 'open') onOpen(ctx.path);
if (action === 'rename') { setRenaming(ctx.path); setRenameVal(ctx.path.split('/').pop()); }
if (action === 'delete') {
await fetch(`${API}/api/file/${project}/${ctx.path}`, { method: 'DELETE' });
onRefresh();
}
setCtx(null);
};
const tree = buildTree(files);
return (
<div className="file-tree" onClick={() => setCtx(null)}>
<div className="file-tree-header">
<span>Files</span>
<div className="file-tree-actions">
<div className="tree-action-btn" title="New File" onClick={async () => {
const name = prompt('File name:');
if (name) { await fetch(`${API}/api/file/${project}/${name}`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({content:''}) }); onRefresh(); }
}}>+</div>
<div className="tree-action-btn" title="Refresh" onClick={onRefresh}>β†Ί</div>
</div>
</div>
{files.length === 0
? <div style={{padding:'16px', color:'var(--text-2)', fontSize:'11px', textAlign:'center'}}>No files yet</div>
: renderTree(tree)
}
{ctx && (
<div className="context-menu" style={{ left: ctx.x, top: ctx.y }}>
{ctx.type === 'file' && <div className="context-item" onClick={() => handleCtxAction('open')}>πŸ“ Open</div>}
<div className="context-item" onClick={() => handleCtxAction('rename')}>✏️ Rename</div>
<div className="context-divider" />
<div className="context-item danger" onClick={() => handleCtxAction('delete')}>πŸ—‘οΈ Delete</div>
</div>
)}
</div>
);
};
// ─── Agent Panel ──────────────────────────────────────────────────────────────
const AgentPanel = ({ project, onFilesUpdate, onConsoleLog }) => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [running, setRunning] = useState(false);
const [history, setHistory] = useState([]);
const abortRef = useRef(null);
const outputRef = useRef(null);
const addMsg = (msg) => {
setMessages(prev => [...prev, { ...msg, id: Date.now() + Math.random() }]);
setTimeout(() => outputRef.current?.scrollTo(0, outputRef.current.scrollHeight), 50);
};
const stop = () => { abortRef.current?.abort(); setRunning(false); };
const send = async () => {
if (!input.trim() || running || !project) return;
const msg = input.trim();
setInput('');
setRunning(true);
addMsg({ type: 'user', content: msg });
abortRef.current = new AbortController();
try {
const res = await fetch(`${API}/api/agent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, message: msg, history }),
signal: abortRef.current.signal,
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let finalMsg = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).split('\n');
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const evt = JSON.parse(line.slice(6));
if (evt.type === 'thinking') addMsg({ type: 'thinking', model: evt.model, content: evt.content });
else if (evt.type === 'analysis') addMsg({ type: 'analysis', content: evt.content });
else if (evt.type === 'plan') addMsg({ type: 'plan', content: evt.content });
else if (evt.type === 'tool_start') {
addMsg({ type: 'tool-start', content: `${evt.tool}(${JSON.stringify(evt.args).slice(0, 80)}...)` });
onConsoleLog({ type: 'tool', text: `[agent] ${evt.tool}: ${JSON.stringify(evt.args).slice(0, 60)}` });
}
else if (evt.type === 'tool_result') {
const isErr = evt.result?.startsWith('❌');
addMsg({ type: 'tool-result', error: isErr, content: evt.result?.slice(0, 200) });
onConsoleLog({ type: isErr ? 'error' : 'success', text: `[result] ${evt.result?.slice(0, 80)}` });
}
else if (evt.type === 'debug') addMsg({ type: 'debug', content: evt.content });
else if (evt.type === 'content') addMsg({ type: 'content', content: evt.content });
else if (evt.type === 'final') {
finalMsg += evt.content;
setMessages(prev => {
const last = prev[prev.length - 1];
if (last?.type === 'final') return [...prev.slice(0, -1), { ...last, content: last.content + evt.content }];
return [...prev, { type: 'final', content: evt.content, id: Date.now() }];
});
}
else if (evt.type === 'files_update') onFilesUpdate(evt.files);
else if (evt.type === 'done') {
setRunning(false);
addMsg({ type: 'done', content: 'βœ… Task completed!' });
onConsoleLog({ type: 'success', text: '[agent] Task completed' });
setHistory(prev => [...prev, { role: 'user', content: msg }, { role: 'assistant', content: finalMsg }]);
}
} catch {}
}
}
} catch (e) {
if (e.name !== 'AbortError') addMsg({ type: 'error', content: String(e) });
setRunning(false);
}
};
const renderMsg = (msg) => {
const modelBadge = msg.model && <div className={`model-badge ${msg.model}`}>{msg.model === 'qwen' ? 'Qwen3 235B' : msg.model === 'oss' ? 'GPT-OSS 120B' : 'Llama 70B'}</div>;
switch (msg.type) {
case 'user': return <div className="agent-msg" style={{background:'var(--accent-dim)',color:'var(--text-0)'}}>{msg.content}</div>;
case 'thinking': return <div className="agent-msg thinking">{modelBadge}<div className="pulse" />{msg.content}</div>;
case 'analysis': return <div className="agent-msg analysis"><div className="model-badge qwen">Qwen3 235B</div><div className="markdown" dangerouslySetInnerHTML={{__html: parseMarkdown(msg.content)}} /></div>;
case 'plan': return <div className="agent-msg plan"><div className="model-badge oss">GPT-OSS 120B</div><div className="markdown" dangerouslySetInnerHTML={{__html: parseMarkdown(msg.content)}} /></div>;
case 'tool-start': return <div className="agent-msg tool-start">⚑ {msg.content}</div>;
case 'tool-result': return <div className={`agent-msg tool-result ${msg.error ? 'error' : ''}`}>{msg.content}</div>;
case 'debug': return <div className="agent-msg debug">πŸ”§ {msg.content}</div>;
case 'final': return <div className="agent-msg final"><div className="model-badge llama">Llama 70B</div><div className="markdown" dangerouslySetInnerHTML={{__html: parseMarkdown(msg.content)}} /></div>;
case 'done': return <div className="agent-msg done">{msg.content}</div>;
default: return <div className="agent-msg" style={{color:'var(--text-1)'}}>{msg.content}</div>;
}
};
return (
<div className="agent-panel">
<div className="agent-output" ref={outputRef}>
{messages.length === 0 && (
<div className="empty-state">
<div className="empty-icon">⚑</div>
<div>Describe what you want to build</div>
<div style={{fontSize:'11px',color:'var(--text-2)'}}>Powered by Qwen3 235B + GPT-OSS 120B</div>
</div>
)}
{messages.map(msg => <div key={msg.id}>{renderMsg(msg)}</div>)}
</div>
<div className="agent-input-wrap">
<div className="agent-input-box">
<textarea
className="agent-textarea"
placeholder={project ? "Describe your task..." : "Select a project first..."}
value={input}
disabled={!project}
onChange={e => { setInput(e.target.value); e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
rows={1}
/>
{running
? <button className="stop-btn" onClick={stop} title="Stop">β– </button>
: <button className="send-btn" onClick={send} disabled={!input.trim() || !project}>β–Ά</button>
}
</div>
</div>
</div>
);
};
// ─── Assistant Panel ──────────────────────────────────────────────────────────
const AssistantPanel = () => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [model, setModel] = useState('llama');
const [streaming, setStreaming] = useState(false);
const messagesRef = useRef(null);
const send = async () => {
if (!input.trim() || streaming) return;
const msg = input.trim();
setInput('');
setStreaming(true);
const history = messages.map(m => ({ role: m.role, content: m.content }));
setMessages(prev => [...prev, { role: 'user', content: msg, id: Date.now() }]);
try {
const res = await fetch(`${API}/api/assistant`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: msg, history, model }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let id = Date.now();
setMessages(prev => [...prev, { role: 'assistant', content: '', id }]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split('\n')) {
if (!line.startsWith('data: ')) continue;
try {
const evt = JSON.parse(line.slice(6));
if (evt.type === 'token') setMessages(prev => prev.map(m => m.id === id ? { ...m, content: m.content + evt.content } : m));
} catch {}
}
}
} catch (e) {
setMessages(prev => [...prev, { role: 'assistant', content: 'Error: ' + e.message, id: Date.now() }]);
}
setStreaming(false);
setTimeout(() => messagesRef.current?.scrollTo(0, messagesRef.current.scrollHeight), 50);
};
return (
<div className="assistant-panel">
<div className="assistant-model-select">
<span>Model:</span>
<select className="model-select" value={model} onChange={e => setModel(e.target.value)}>
<option value="llama">Llama 70B (Cloudflare)</option>
<option value="qwen">Qwen3 235B (Cerebras)</option>
<option value="oss">GPT-OSS 120B (Cerebras)</option>
</select>
</div>
<div className="assistant-messages" ref={messagesRef}>
{messages.length === 0 && (
<div className="empty-state">
<div className="empty-icon">πŸ’¬</div>
<div>Ask me anything</div>
</div>
)}
{messages.map(msg => (
<div key={msg.id} className={`chat-msg ${msg.role}`}>
<div className="chat-bubble">
<div className="markdown" dangerouslySetInnerHTML={{__html: parseMarkdown(msg.content)}} />
</div>
</div>
))}
</div>
<div className="agent-input-wrap">
<div className="agent-input-box">
<textarea
className="agent-textarea"
placeholder="Ask anything..."
value={input}
onChange={e => { setInput(e.target.value); e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
rows={1}
/>
<button className="send-btn" onClick={send} disabled={!input.trim() || streaming}>β–Ά</button>
</div>
</div>
</div>
);
};
// ─── Shell Panel ──────────────────────────────────────────────────────────────
const ShellPanel = ({ project }) => {
const [lines, setLines] = useState([{ type: 'info', text: `XRO IDE Shell β€” project: ${project || 'none'}` }]);
const [input, setInput] = useState('');
const [history, setHistory] = useState([]);
const [histIdx, setHistIdx] = useState(-1);
const outputRef = useRef(null);
const run = async () => {
if (!input.trim() || !project) return;
const cmd = input.trim();
setHistory(prev => [cmd, ...prev]);
setHistIdx(-1);
setInput('');
setLines(prev => [...prev, { type: 'cmd', text: `C:\\XRO-IDE\\${project}> ${cmd}` }]);
try {
const res = await fetch(`${API}/api/shell`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, command: cmd }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split('\n')) {
if (!line.startsWith('data: ')) continue;
try {
const evt = JSON.parse(line.slice(6));
if (evt.type === 'output') setLines(prev => [...prev, { type: 'info', text: evt.content.trimEnd() }]);
if (evt.type === 'error') setLines(prev => [...prev, { type: 'err', text: evt.content }]);
} catch {}
}
}
} catch (e) {
setLines(prev => [...prev, { type: 'err', text: String(e) }]);
}
setTimeout(() => outputRef.current?.scrollTo(0, outputRef.current.scrollHeight), 50);
};
return (
<div className="shell-panel">
<div className="shell-output" ref={outputRef}>
<div style={{color:'var(--text-2)',fontSize:'11px',marginBottom:'8px'}}>Microsoft Windows [Version 11.0] β€” XRO IDE Shell</div>
{lines.map((l, i) => <div key={i} className={`shell-line ${l.type}`}>{l.text}</div>)}
</div>
<div className="shell-input-row">
<span className="shell-prompt">C:\XRO-IDE\{project || '?'}{'>'}</span>
<input
className="shell-input"
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') run();
if (e.key === 'ArrowUp') { const idx = Math.min(histIdx + 1, history.length - 1); setHistIdx(idx); setInput(history[idx] || ''); }
if (e.key === 'ArrowDown') { const idx = Math.max(histIdx - 1, -1); setHistIdx(idx); setInput(idx === -1 ? '' : history[idx]); }
}}
placeholder={project ? 'type command...' : 'select a project first'}
disabled={!project}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
);
};
// ─── Console Panel ────────────────────────────────────────────────────────────
const ConsolePanel = ({ logs }) => {
const ref = useRef(null);
useEffect(() => { ref.current?.scrollTo(0, ref.current.scrollHeight); }, [logs]);
return (
<div className="console-panel" ref={ref}>
{logs.length === 0 && <div style={{color:'var(--text-2)'}}>Agent output will appear here...</div>}
{logs.map((l, i) => (
<div key={i} className={`console-line ${l.type}`}>
<span className="console-time">{l.time}</span>
<span>{l.text}</span>
</div>
))}
</div>
);
};
// ─── Preview Panel ────────────────────────────────────────────────────────────
const PreviewPanel = () => {
const [url, setUrl] = useState('http://localhost:3000');
const [src, setSrc] = useState('');
return (
<div className="preview-panel">
<div className="preview-bar">
<input className="preview-url" value={url} onChange={e => setUrl(e.target.value)} onKeyDown={e => e.key === 'Enter' && setSrc(url)} placeholder="Enter URL..." />
<button className="btn btn-primary" style={{padding:'4px 10px',fontSize:'12px'}} onClick={() => setSrc(url)}>Go</button>
</div>
{src ? <iframe className="preview-iframe" src={src} sandbox="allow-scripts allow-same-origin allow-forms" /> : <div className="empty-state"><div className="empty-icon">🌐</div><div>Enter URL and press Go</div></div>}
</div>
);
};
// ─── Files Panel ──────────────────────────────────────────────────────────────
const FilesPanel = ({ project, files, onRefresh }) => {
const [openFiles, setOpenFiles] = useState([]);
const [activeFile, setActiveFile] = useState(null);
const [fileContents, setFileContents] = useState({});
const [modified, setModified] = useState({});
const openFile = async (path) => {
if (!openFiles.includes(path)) setOpenFiles(prev => [...prev, path]);
setActiveFile(path);
if (!fileContents[path]) {
const res = await fetch(`${API}/api/file/${project}/${path}`);
const data = await res.json();
setFileContents(prev => ({ ...prev, [path]: data.content || '' }));
}
};
const closeFile = (path, e) => {
e.stopPropagation();
const idx = openFiles.indexOf(path);
const next = openFiles.filter(f => f !== path);
setOpenFiles(next);
if (activeFile === path) setActiveFile(next[Math.max(0, idx - 1)] || null);
};
const saveFile = async () => {
if (!activeFile) return;
await fetch(`${API}/api/file/${project}/${activeFile}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: fileContents[activeFile] })
});
setModified(prev => ({ ...prev, [activeFile]: false }));
};
useEffect(() => {
const handler = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveFile(); } };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [activeFile, fileContents]);
return (
<div className="files-panel">
<FileTree files={files} activeFile={activeFile} onOpen={openFile} onRefresh={onRefresh} project={project} />
<div className="file-editor-area">
{openFiles.length > 0 && (
<div className="editor-file-tabs">
{openFiles.map(f => (
<div key={f} className={`editor-tab ${activeFile === f ? 'active' : ''}`} onClick={() => openFile(f)}>
{modified[f] && <span style={{color:'var(--yellow)',fontSize:'8px'}}>●</span>}
<span>{fileIcon(f)}</span>
<span>{f.split('/').pop()}</span>
<span className="tab-close" onClick={e => closeFile(f, e)}>Γ—</span>
</div>
))}
</div>
)}
<div className="editor-container">
{activeFile
? <MonacoEditor
value={fileContents[activeFile] || ''}
path={activeFile}
language={getLanguage(activeFile)}
onChange={val => { setFileContents(prev => ({ ...prev, [activeFile]: val })); setModified(prev => ({ ...prev, [activeFile]: true })); }}
/>
: <div className="empty-state"><div className="empty-icon">πŸ“</div><div>Click a file to open it</div></div>
}
</div>
</div>
</div>
);
};
// ─── New Project Modal ────────────────────────────────────────────────────────
const NewProjectModal = ({ onClose, onCreate }) => {
const [name, setName] = useState('');
const [template, setTemplate] = useState('empty');
const templates = [
{ id: 'empty', label: '⬜ Empty' },
{ id: 'react', label: 'βš›οΈ React' },
{ id: 'node', label: '🟩 Node.js' },
{ id: 'python', label: '🐍 Python' },
{ id: 'html', label: '🌐 HTML/CSS' },
];
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-title">+ New Project</div>
<div className="modal-field">
<label className="modal-label">Project Name</label>
<input className="modal-input" value={name} onChange={e => setName(e.target.value.replace(/\s+/g, '-').toLowerCase())} placeholder="my-project" autoFocus />
</div>
<div className="modal-field">
<label className="modal-label">Template</label>
<div className="template-grid">
{templates.map(t => (
<button key={t.id} className={`template-btn ${template === t.id ? 'selected' : ''}`} onClick={() => setTemplate(t.id)}>{t.label}</button>
))}
</div>
</div>
<div className="modal-actions">
<button className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button className="btn btn-primary" disabled={!name} onClick={() => { onCreate(name, template); onClose(); }}>Create β–Ά</button>
</div>
</div>
</div>
);
};
// ─── Main App ─────────────────────────────────────────────────────────────────
const App = () => {
const [projects, setProjects] = useState([]);
const [activeProject, setActiveProject] = useState(null);
const [files, setFiles] = useState([]);
const [showProjectMenu, setShowProjectMenu] = useState(false);
const [showNewProject, setShowNewProject] = useState(false);
const [consoleLogs, setConsoleLogs] = useState([]);
// Left tabs: fixed agent + assistant
const [leftTab, setLeftTab] = useState('agent');
// Right tabs: fixed 3 + extras
const [rightTabs, setRightTabs] = useState([
{ id: 'editor', label: 'πŸ“ Editor', fixed: true },
{ id: 'files', label: 'πŸ“ Files', fixed: true },
{ id: 'console', label: '⬛ Console', fixed: true },
]);
const [activeRightTab, setActiveRightTab] = useState('editor');
const [showTabPopup, setShowTabPopup] = useState(false);
const [showLeftPopup, setShowLeftPopup] = useState(false);
// Mobile nav
const [mobileTab, setMobileTab] = useState('agent');
useEffect(() => { loadProjects(); }, []);
useEffect(() => { if (activeProject) loadFiles(); }, [activeProject]);
useEffect(() => {
const saved = localStorage.getItem('xro-active-project');
if (saved) setActiveProject(saved);
}, [projects]);
const loadProjects = async () => {
const res = await fetch(`${API}/api/projects`);
const data = await res.json();
setProjects(data);
};
const loadFiles = async () => {
const res = await fetch(`${API}/api/files/${activeProject}`);
const data = await res.json();
setFiles(data);
};
const createProject = async (name, template) => {
await fetch(`${API}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, template }),
});
await loadProjects();
setActiveProject(name);
localStorage.setItem('xro-active-project', name);
};
const selectProject = (name) => {
setActiveProject(name);
localStorage.setItem('xro-active-project', name);
setShowProjectMenu(false);
};
const addConsoleLog = (log) => {
setConsoleLogs(prev => [...prev, { ...log, time: timestamp() }]);
};
const addRightTab = (type) => {
const id = `${type}-${Date.now()}`;
const labels = { shell: '>_ Shell', preview: '🌐 Preview' };
setRightTabs(prev => [...prev, { id, type, label: labels[type] || type }]);
setActiveRightTab(id);
setShowTabPopup(false);
};
const closeRightTab = (id, e) => {
e.stopPropagation();
setRightTabs(prev => prev.filter(t => t.id !== id));
if (activeRightTab === id) setActiveRightTab('editor');
};
const renderRightContent = () => {
const tab = rightTabs.find(t => t.id === activeRightTab);
if (!tab) return null;
if (tab.id === 'editor') return <div className="tab-content"><MonacoEditor value="" onChange={() => {}} /></div>;
if (tab.id === 'files') return <div className="tab-content"><FilesPanel project={activeProject} files={files} onRefresh={loadFiles} /></div>;
if (tab.id === 'console') return <div className="tab-content"><ConsolePanel logs={consoleLogs} /></div>;
if (tab.type === 'shell') return <div className="tab-content"><ShellPanel project={activeProject} /></div>;
if (tab.type === 'preview') return <div className="tab-content"><PreviewPanel /></div>;
return null;
};
// Mobile active panels
const mobileLeftActive = ['agent', 'assistant'].includes(mobileTab);
const mobileRightActive = ['editor', 'files', 'console'].includes(mobileTab);
if (mobileLeftActive && mobileTab !== leftTab) {
// sync
}
return (
<div className="ide" onClick={() => { setShowProjectMenu(false); setShowTabPopup(false); setShowLeftPopup(false); }}>
{/* Header */}
<div className="header">
<div className="header-logo">
<div className="header-logo-dot" />
XRO IDE
</div>
{/* Project switcher */}
<div className="project-switcher" onClick={e => e.stopPropagation()}>
<button className="project-btn" onClick={() => setShowProjectMenu(!showProjectMenu)}>
πŸ“‚ {activeProject || 'Select Project'} β–Ύ
</button>
{showProjectMenu && (
<div className="project-dropdown">
{projects.map(p => (
<div key={p.name} className={`project-dropdown-item ${activeProject === p.name ? 'active' : ''}`} onClick={() => selectProject(p.name)}>
{activeProject === p.name ? '●' : 'β—‹'} {p.name}
</div>
))}
{projects.length > 0 && <div className="project-dropdown-divider" />}
<div className="new-project-btn" onClick={() => { setShowNewProject(true); setShowProjectMenu(false); }}>
+ New Project
</div>
</div>
)}
</div>
<div className="header-spacer" />
<button className="settings-btn">βš™</button>
</div>
{/* Main */}
<div className="main">
{/* Left Panel */}
<div className={`left-panel ${mobileLeftActive ? 'mobile-active' : ''}`}>
<div className="left-tabs" onClick={e => e.stopPropagation()}>
<div className={`tab ${leftTab === 'agent' ? 'active' : ''}`} onClick={() => setLeftTab('agent')}>⚑ Agent</div>
<div className={`tab ${leftTab === 'assistant' ? 'active' : ''}`} onClick={() => setLeftTab('assistant')}>πŸ’¬ Assistant</div>
<div className="header-spacer" />
<div className="tab-add" onClick={() => setShowLeftPopup(!showLeftPopup)}>
+
{showLeftPopup && (
<div className="tab-popup">
<div className="tab-popup-item" onClick={() => { setLeftTab('agent'); setShowLeftPopup(false); }}>⚑ New Agent Tab</div>
<div className="tab-popup-item" onClick={() => { setLeftTab('assistant'); setShowLeftPopup(false); }}>πŸ’¬ New Chat</div>
</div>
)}
</div>
</div>
<div className="tab-content">
{leftTab === 'agent'
? <AgentPanel project={activeProject} onFilesUpdate={(f) => setFiles(f)} onConsoleLog={addConsoleLog} />
: <AssistantPanel />
}
</div>
</div>
{/* Right Panel */}
<div className={`right-panel ${mobileRightActive ? 'mobile-active' : ''}`}>
<div className="right-tabs" onClick={e => e.stopPropagation()}>
{rightTabs.map(tab => (
<div key={tab.id} className={`tab ${activeRightTab === tab.id ? 'active' : ''}`} onClick={() => setActiveRightTab(tab.id)}>
{tab.label}
{!tab.fixed && <span className="tab-close" onClick={e => closeRightTab(tab.id, e)}>Γ—</span>}
</div>
))}
<div className="tab-add" style={{position:'relative'}} onClick={() => setShowTabPopup(!showTabPopup)}>
+
{showTabPopup && (
<div className="tab-popup">
<div className="tab-popup-item" onClick={() => addRightTab('shell')}>{'>'} New Shell</div>
<div className="tab-popup-item" onClick={() => addRightTab('preview')}>🌐 New Preview</div>
</div>
)}
</div>
</div>
{renderRightContent()}
</div>
</div>
{/* Mobile Nav */}
<div className="mobile-nav">
{[
{ id: 'agent', icon: '⚑', label: 'Agent' },
{ id: 'assistant', icon: 'πŸ’¬', label: 'Chat' },
{ id: 'editor', icon: 'πŸ“', label: 'Code' },
{ id: 'files', icon: 'πŸ“', label: 'Files' },
{ id: 'console', icon: '⬛', label: 'Console' },
].map(item => (
<div key={item.id} className={`mobile-nav-item ${mobileTab === item.id ? 'active' : ''}`}
onClick={() => {
setMobileTab(item.id);
if (['agent','assistant'].includes(item.id)) setLeftTab(item.id);
else setActiveRightTab(item.id);
}}>
<span className="mobile-nav-item-icon">{item.icon}</span>
<span>{item.label}</span>
</div>
))}
</div>
{/* Modals */}
{showNewProject && <NewProjectModal onClose={() => setShowNewProject(false)} onCreate={createProject} />}
</div>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<App />);