import React, { useState, useEffect, useRef, Component, ErrorInfo, ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; class WebIDEErrorBoundary extends Component<{children: ReactNode}, {hasError: boolean, error: Error | null}> { constructor(props: {children: ReactNode}) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error("WebIDE Error:", error, errorInfo); } render() { if (this.state.hasError) { return (

WebIDE Crashed

{this.state.error?.toString()}
{this.state.error?.stack}
); } return this.props.children; } } import Editor from '@monaco-editor/react'; import { Play, Square, MessageSquare, Terminal as TerminalIcon, Loader2, Send, FileCode2, Folder, FolderOpen, ExternalLink, Files, Search, GitBranch, PlaySquare, Blocks, Settings, UserCircle, ChevronDown, ChevronRight, Plus, Trash2, X, MoreHorizontal, Check, Download, Edit3, FilePlus, ImagePlus, Mic } from 'lucide-react'; import { api } from '../services/api'; import { getUserIdSync } from '../utils/userId'; // Memoize editor options to prevent re-renders const EDITOR_OPTIONS = { minimap: { enabled: true, scale: 2 }, fontSize: 14, fontFamily: "'Consolas', 'Courier New', monospace", scrollBeyondLastLine: false, padding: { top: 16 }, wordWrap: 'on' as const, bracketPairColorization: { enabled: true } }; interface WebIDEProps { initialFiles: Record; onClose?: () => void; } const VSCodeIcon = () => ( ); const CursorIcon = () => ( ); const AntigravityIcon = () => ( ); export const WebIDE: React.FC = ({ initialFiles, onClose }) => { const [files, setFiles] = useState>(initialFiles); const [activeFile, setActiveFile] = useState(Object.keys(initialFiles)[0] || ''); const [pid, setPid] = useState(null); const [workspacePath, setWorkspacePath] = useState(''); const [logs, setLogs] = useState(''); const [isRunning, setIsRunning] = useState(false); const [runningUrl, setRunningUrl] = useState(null); const hasOpenedUrlRef = useRef(false); // Chat state const [chatMessages, setChatMessages] = useState<{role: 'user'|'ai', content: string, is_plan?: boolean, isStreaming?: boolean, thoughts?: string[], status?: string}[]>([ { role: 'ai', content: 'Hello! I am your Silicon Valley AI Architect. I have access to your full codebase. How would you like to upgrade your app today?' } ]); const [chatHistory, setChatHistory] = useState<{id: string, title: string, messages: {role: 'user'|'ai', content: string, is_plan?: boolean, isStreaming?: boolean, thoughts?: string[], status?: string}[]}[]>([ { id: '1', title: 'Current Session', messages: [ { role: 'ai', content: 'Hello! I am your Silicon Valley AI Architect. I have access to your full codebase. How would you like to upgrade your app today?' } ]} ]); const [activeChatId, setActiveChatId] = useState('1'); const [chatSearchQuery, setChatSearchQuery] = useState(''); const [showChatHistoryPanel, setShowChatHistoryPanel] = useState(false); const [chatInput, setChatInput] = useState(''); const [lastPrompt, setLastPrompt] = useState(''); const [isPlanningMode, setIsPlanningMode] = useState(true); const [isChatting, setIsChatting] = useState(false); const [llm, setLlm] = useState('llama'); // LLM Selection state const [modifiedFiles, setModifiedFiles] = useState>(new Set()); const [newFileInput, setNewFileInput] = useState(null); const [collapsedFolders, setCollapsedFolders] = useState>(new Set()); // Multimodal state const [chatImages, setChatImages] = useState([]); const [isRecording, setIsRecording] = useState(false); // UI state const [activeSidebar, setActiveSidebar] = useState<'explorer' | 'search' | 'git' | 'run' | 'extensions' | null>('explorer'); const [rightPanelOpen, setRightPanelOpen] = useState(true); const [terminalOpen, setTerminalOpen] = useState(true); const [activeTerminalTab, setActiveTerminalTab] = useState<'problems' | 'output' | 'debug' | 'terminal' | 'ports'>('terminal'); const [activeTerminalType, setActiveTerminalType] = useState<'powershell' | 'cmd' | 'python' | null>('powershell'); const [terminals, setTerminals] = useState>([]); const [activeTerminalId, setActiveTerminalId] = useState(null); const [terminalHeight, setTerminalHeight] = useState(300); const [isDraggingTerminal, setIsDraggingTerminal] = useState(false); const [showTerminalDropdown, setShowTerminalDropdown] = useState(false); const [isDarkMode, setIsDarkMode] = useState(true); const [showCloseModal, setShowCloseModal] = useState(false); const logsEndRef = useRef(null); const chatEndRef = useRef(null); const terminalContainerRef = useRef(null); const xtermRef = useRef(null); const fitAddonRef = useRef(null); // Refs for terminal state closure const activeTerminalIdRef = useRef(activeTerminalId); const activeTerminalTypeRef = useRef(activeTerminalType); const pidRef = useRef(pid); const inputBufferRef = useRef(''); useEffect(() => { activeTerminalIdRef.current = activeTerminalId; }, [activeTerminalId]); useEffect(() => { activeTerminalTypeRef.current = activeTerminalType; }, [activeTerminalType]); useEffect(() => { pidRef.current = pid; }, [pid]); // Initialize xterm useEffect(() => { if (!terminalContainerRef.current || !terminalOpen || activeTerminalTab !== 'terminal') return; if (!xtermRef.current) { const term = new Terminal({ theme: isDarkMode ? { background: '#1e1e1e', foreground: '#cccccc' } : { background: '#ffffff', foreground: '#333333' }, fontFamily: "'Consolas', 'Courier New', monospace", fontSize: 13, cursorBlink: true, disableStdin: false }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(terminalContainerRef.current); fitAddon.fit(); xtermRef.current = term; fitAddonRef.current = fitAddon; // Handle user input in terminal (Line buffered for Windows Pipe compatibility) term.onData(async (data) => { const targetPid = (activeTerminalTypeRef.current === 'python') ? pidRef.current : activeTerminalIdRef.current; if (!targetPid) return; if (data === '\r') { // Enter key pressed: send the buffered line try { term.write('\r\n'); const lineToSend = inputBufferRef.current + '\n'; inputBufferRef.current = ''; await api.post(`/api/v1/ide/terminal/input/${targetPid}`, { input: lineToSend }); } catch (e) { console.error("Failed to send terminal input", e); } } else if (data === '\x7f' || data === '\b') { // Backspace pressed if (inputBufferRef.current.length > 0) { inputBufferRef.current = inputBufferRef.current.slice(0, -1); term.write('\b \b'); // Erase character visually } } else if (data === '\u0003') { // Ctrl+C pressed inputBufferRef.current = ''; term.write('^C\r\n'); try { await api.post(`/api/v1/ide/terminal/input/${targetPid}`, { input: '\x03' }); } catch (e) { console.error("Failed to send Ctrl+C", e); } } else { // Normal character typed inputBufferRef.current += data; term.write(data); } }); // Initial write if (logs) { term.write(logs.replace(/\n/g, '\r\n')); } else { term.write('Welcome to DataVision Terminal.\r\nType "python api_server.py" to start manually.\r\n\r\n> '); } } const handleResize = () => { if (fitAddonRef.current) fitAddonRef.current.fit(); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); if (xtermRef.current) { xtermRef.current.dispose(); xtermRef.current = null; } }; }, [terminalOpen, activeTerminalTab, isDarkMode]); // Terminal Dragging useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isDraggingTerminal) return; // Calculate new height (window height - mouse Y - status bar approx height) const newHeight = window.innerHeight - e.clientY - 24; setTerminalHeight(Math.max(100, Math.min(newHeight, window.innerHeight - 200))); // Trigger xterm fit on drag if (fitAddonRef.current) fitAddonRef.current.fit(); }; const handleMouseUp = () => setIsDraggingTerminal(false); if (isDraggingTerminal) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); // Disable text selection while dragging document.body.style.userSelect = 'none'; } else { document.body.style.userSelect = ''; } return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [isDraggingTerminal]); // Theme observer useEffect(() => { const checkDarkMode = () => setIsDarkMode(document.documentElement.classList.contains('dark')); checkDarkMode(); const observer = new MutationObserver(checkDarkMode); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); return () => observer.disconnect(); }, []); useEffect(() => { if (logsEndRef.current) logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); }, [logs, activeTerminalTab]); useEffect(() => { if (chatEndRef.current) chatEndRef.current.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages]); useEffect(() => { const fetchPath = async () => { try { const res = await api.get(`/api/v1/ide/workspace-path/${getUserIdSync()}`); if (res.data.success) { setWorkspacePath(res.data.path); } } catch (e) {} }; fetchPath(); }, []); // Load persisted files and check for running process on mount useEffect(() => { const userId = getUserIdSync(); // 1. Load persisted files from localStorage const stored = localStorage.getItem(`ide_files_${userId}`); if (stored) { try { const parsed = JSON.parse(stored); if (Object.keys(parsed).length > 0) { setFiles(prev => ({ ...prev, ...parsed })); } } catch (e) {} } // Removed checkStatus to prevent reconnecting to old powershell terminals }, []); // Persist files to localStorage when they change useEffect(() => { if (Object.keys(files).length > 0) { localStorage.setItem(`ide_files_${getUserIdSync()}`, JSON.stringify(files)); } }, [files]); const spawnTerminal = async (type: 'powershell' | 'cmd') => { try { const res = await api.post('/api/v1/ide/terminal/spawn', { user_id: getUserIdSync(), shell: type }); if (res.data.success) { const newTerminal = { id: res.data.pid, type, logsLen: 0 }; setTerminals(prev => [...prev, newTerminal]); setActiveTerminalId(newTerminal.id); setActiveTerminalType(type); if (xtermRef.current) xtermRef.current.reset(); } } catch (e) { console.error("Failed to spawn terminal", e); } }; const deleteTerminal = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); try { await api.post(`/api/v1/ide/stop/${id}`); setTerminals(prev => { const next = prev.filter(t => t.id !== id); if (activeTerminalId === id) { if (next.length > 0) { setActiveTerminalId(next[next.length - 1].id); setActiveTerminalType(next[next.length - 1].type); } else { setActiveTerminalId(null); // fallback to python if running, else cmd setActiveTerminalType(isRunning ? 'python' : null); } } return next; }); } catch (err) { console.error("Failed to delete terminal", err); } }; // Auto-spawn raw terminal when tab opens and no terminals exist useEffect(() => { if (terminalOpen && activeTerminalTab === 'terminal' && terminals.length === 0 && (activeTerminalType === 'powershell' || activeTerminalType === 'cmd' || activeTerminalType === null)) { spawnTerminal('powershell'); } }, [terminalOpen, activeTerminalTab, terminals.length, activeTerminalType]); // Handle switching between existing terminals useEffect(() => { if (activeTerminalTab === 'terminal') { if (xtermRef.current) xtermRef.current.reset(); if (activeTerminalType === 'python' && pid) { // we will fetch logs in the python polling effect } else if (activeTerminalId) { // Reset length so it fetches full history of switched terminal setTerminals(prev => prev.map(t => t.id === activeTerminalId ? { ...t, logsLen: 0 } : t)); } } }, [activeTerminalId, activeTerminalType, activeTerminalTab]); useEffect(() => { let interval: NodeJS.Timeout; if (pid && isRunning) { interval = setInterval(async () => { try { const res = await api.get(`/api/v1/ide/logs/${pid}`); if (res.data.success) { if (xtermRef.current && res.data.logs && activeTerminalType === 'python') { const newLogs = res.data.logs.slice(logs.length); if (newLogs) xtermRef.current.write(newLogs.replace(/\n/g, '\r\n')); } setLogs(res.data.logs); if (res.data.url) { setRunningUrl(res.data.url); if (!hasOpenedUrlRef.current) { hasOpenedUrlRef.current = true; window.open(res.data.url, '_blank'); } } if (res.data.status !== 'running') { setIsRunning(false); } } } catch (err) {} }, 1000); } return () => clearInterval(interval); }, [pid, isRunning, logs, activeTerminalType]); // Polling for Standalone Terminal useEffect(() => { let interval: NodeJS.Timeout; if (activeTerminalId && (activeTerminalType === 'powershell' || activeTerminalType === 'cmd')) { interval = setInterval(async () => { try { const res = await api.get(`/api/v1/ide/logs/${activeTerminalId}`); if (res.data.success && res.data.logs) { const allLogs = res.data.logs; setTerminals(prev => { const activeTerm = prev.find(t => t.id === activeTerminalId); if (activeTerm && allLogs.length > activeTerm.logsLen) { const newChunk = allLogs.slice(activeTerm.logsLen); if (xtermRef.current) xtermRef.current.write(newChunk.replace(/\n/g, '\r\n')); return prev.map(t => t.id === activeTerminalId ? { ...t, logsLen: allLogs.length } : t); } return prev; }); } } catch (err) {} }, 200); // Poll faster for responsiveness } return () => clearInterval(interval); }, [activeTerminalId, activeTerminalType]); const handleRun = async () => { try { setIsRunning(true); setRunningUrl(null); hasOpenedUrlRef.current = false; setLogs('[System] Instructing backend to auto-detect and run project...\\n'); const res = await api.post(`/api/v1/ide/run`, { user_id: getUserIdSync(), command: "python api_server.py", // Or allow auto-detect by passing empty/null if backend supports it files: files }); if (res.data.success) { setPid(res.data.pid); } } catch (err: any) { setLogs(prev => prev + ` [System Error]: ${err.message}`); setIsRunning(false); } }; const handleStop = async () => { if (!pid) { setLogs(''); setTerminalOpen(false); return; } try { await api.post(`/api/v1/ide/stop/${pid}`); setLogs(prev => prev + '\n[Process stopped by user.]\n'); } catch (err) { console.error("Failed to stop cleanly", err); } finally { setIsRunning(false); setPid(null); } }; const handleDownload = async () => { try { const res = await api.post('/api/v1/ide/download', { user_id: getUserIdSync(), files }, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([res.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', 'workspace.zip'); document.body.appendChild(link); link.click(); link.remove(); } catch (err) { console.error("Failed to download workspace", err); } }; const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { const base64String = reader.result as string; setChatImages(prev => [...prev, base64String]); }; reader.readAsDataURL(file); } }; const handleVoiceRecord = () => { // Use browser SpeechRecognition API const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (!SpeechRecognition) { alert("Voice recording is not supported in your browser."); return; } if (isRecording) return; // Prevent multiple instances const recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = false; recognition.onstart = () => { setIsRecording(true); }; recognition.onresult = (event: any) => { const transcript = event.results[0][0].transcript; setChatInput(prev => prev ? prev + ' ' + transcript : transcript); setIsRecording(false); }; recognition.onerror = (event: any) => { console.error("Speech recognition error", event.error); setIsRecording(false); }; recognition.onend = () => { setIsRecording(false); }; recognition.start(); }; const handleChat = async (overrideMsg?: string, forceMode?: 'plan' | 'execute') => { const userMsg = overrideMsg || chatInput; if (!userMsg.trim() || isChatting) return; if (!overrideMsg) { setChatInput(''); setLastPrompt(userMsg); setChatMessages(prev => [...prev, { role: 'user', content: userMsg }]); } setIsChatting(true); // Add a placeholder AI message that we will stream into setChatMessages(prev => [...prev, { role: 'ai', content: '', isStreaming: true, thoughts: [] }]); try { const currentMode = forceMode || (isPlanningMode ? 'plan' : 'execute'); // Build the body for fetch const body = { user_id: getUserIdSync(), files: files, prompt: userMsg, model: llm, chat_history: chatMessages.map(msg => ({ role: msg.role, content: msg.content })), images: chatImages.length > 0 ? chatImages : undefined, mode: currentMode }; // Clear attached images after sending setChatImages([]); const token = localStorage.getItem('token'); const response = await fetch('/api/v1/ide/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, body: JSON.stringify(body) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) throw new Error("No reader available"); const decoder = new TextDecoder("utf-8"); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith('data: ')) { const dataStr = line.slice(6); try { const data = JSON.parse(dataStr); setChatMessages(prev => { const newMsgs = [...prev]; if (newMsgs.length === 0) return prev; const lastIndex = newMsgs.length - 1; if (newMsgs[lastIndex].role !== 'ai') return prev; // Must clone the last message object to avoid mutating state const lastMsg = { ...newMsgs[lastIndex] }; if (data.type === 'status') { lastMsg.status = data.content; } else if (data.type === 'thought') { lastMsg.thoughts = lastMsg.thoughts ? [...lastMsg.thoughts, data.content] : [data.content]; } else if (data.type === 'tool_call') { lastMsg.content = (lastMsg.content || "") + `\n> 🛠️ Running Tool: ${data.tool}\n\`\`\`json\n${JSON.stringify(data.args, null, 2)}\n\`\`\`\n`; } else if (data.type === 'tool_result') { lastMsg.content = (lastMsg.content || "") + `\n*✅ Tool Complete*\n`; } else if (data.type === 'message') { lastMsg.content = (lastMsg.content ? lastMsg.content + "\n\n" : "") + data.content; } else if (data.type === 'done') { lastMsg.isStreaming = false; } else if (data.type === 'error') { lastMsg.content = (lastMsg.content || "") + `\n\n**Error:** ${data.content}`; lastMsg.isStreaming = false; } newMsgs[lastIndex] = lastMsg; return newMsgs; }); if (data.type === 'sync_files' && data.files) { setFiles(prev => ({ ...prev, ...data.files })); setModifiedFiles(prev => { const next = new Set(prev); Object.keys(data.files).forEach(f => next.add(f)); return next; }); } } catch (e) { console.error("Failed to parse SSE JSON", e, dataStr); } } } } } catch (error: any) { setChatMessages(prev => [...prev, { role: 'ai', content: `Error: ${error.message}` }]); } finally { setIsChatting(false); setChatMessages(prev => { const newMsgs = [...prev]; const lastMsg = newMsgs[newMsgs.length - 1]; if (lastMsg.role === 'ai') lastMsg.isStreaming = false; return newMsgs; }); } }; // Sync chat messages to history whenever they change useEffect(() => { setChatHistory(prev => prev.map(chat => chat.id === activeChatId ? { ...chat, messages: chatMessages } : chat )); }, [chatMessages, activeChatId]); const createNewChat = () => { const newId = Date.now().toString(); const initialMsg = { role: 'ai' as const, content: 'Hello! I am your Silicon Valley AI Architect. I have access to your full codebase. How would you like to upgrade your app today?' }; setChatHistory(prev => [{ id: newId, title: 'New Session', messages: [initialMsg] }, ...prev]); setActiveChatId(newId); setChatMessages([initialMsg]); setShowChatHistoryPanel(false); }; const switchChat = (id: string) => { const chat = chatHistory.find(c => c.id === id); if (chat) { setActiveChatId(id); setChatMessages(chat.messages); setShowChatHistoryPanel(false); } }; const fileList = Object.keys(files).sort((a, b) => { const aDepth = a.split('/').length; const bDepth = b.split('/').length; if (aDepth !== bDepth) return aDepth - bDepth; return a.localeCompare(b); }); // Build folder tree structure const getFolders = (): string[] => { const folders = new Set(); fileList.forEach(f => { const parts = f.split('/'); if (parts.length > 1) { for (let i = 1; i < parts.length; i++) { folders.add(parts.slice(0, i).join('/')); } } }); return Array.from(folders).sort(); }; const folders = getFolders(); const toggleFolder = (folder: string) => { setCollapsedFolders(prev => { const next = new Set(prev); if (next.has(folder)) next.delete(folder); else next.add(folder); return next; }); }; const isFileVisible = (filename: string): boolean => { const parts = filename.split('/'); if (parts.length <= 1) return true; for (let i = 1; i < parts.length; i++) { const parentFolder = parts.slice(0, i).join('/'); if (collapsedFolders.has(parentFolder)) return false; } return true; }; const handleCreateFile = (filename: string) => { if (!filename.trim()) return; setFiles(prev => ({ ...prev, [filename.trim()]: '' })); setActiveFile(filename.trim()); setNewFileInput(null); }; const handleDeleteFile = (filename: string) => { setFiles(prev => { const next = { ...prev }; delete next[filename]; return next; }); if (activeFile === filename) { const remaining = Object.keys(files).filter(f => f !== filename); setActiveFile(remaining[0] || ''); } }; const getFileIcon = (filename: string) => { if (filename.endsWith('.py')) return 'text-blue-500 dark:text-[#4fc1ff]'; if (filename.endsWith('.html')) return 'text-orange-500 dark:text-[#e37933]'; if (filename.endsWith('.css')) return 'text-purple-500 dark:text-[#a855f7]'; if (filename.endsWith('.js') || filename.endsWith('.jsx')) return 'text-yellow-500 dark:text-[#cbcb41]'; if (filename.endsWith('.ts') || filename.endsWith('.tsx')) return 'text-blue-600 dark:text-[#3178c6]'; if (filename.endsWith('.json')) return 'text-yellow-600 dark:text-[#cbcb41]'; if (filename.endsWith('.md')) return 'text-blue-400 dark:text-[#519aba]'; if (filename.endsWith('.csv')) return 'text-green-600 dark:text-[#89d185]'; if (filename.endsWith('.txt') || filename.endsWith('.yml') || filename.endsWith('.yaml')) return 'text-slate-500 dark:text-[#cccccc]'; return 'text-slate-600 dark:text-[#cccccc]'; }; const getMonacoLanguage = (filename: string) => { if (filename.endsWith('.py')) return 'python'; if (filename.endsWith('.js') || filename.endsWith('.jsx')) return 'javascript'; if (filename.endsWith('.ts') || filename.endsWith('.tsx')) return 'typescript'; if (filename.endsWith('.html')) return 'html'; if (filename.endsWith('.css')) return 'css'; if (filename.endsWith('.json')) return 'json'; if (filename.endsWith('.md')) return 'markdown'; if (filename.endsWith('.yml') || filename.endsWith('.yaml')) return 'yaml'; if (filename.endsWith('.sh') || filename.endsWith('.bash')) return 'shell'; if (filename.endsWith('.dockerfile') || filename === 'Dockerfile') return 'dockerfile'; return 'plaintext'; }; // Simple markdown-like renderer for chat messages const renderChatContent = (content: string) => { const parts = content.split(/(```[\s\S]*?```|`[^`]+`|\*\*[^*]+\*\*|\n)/g); return parts.map((part, i) => { if (part.startsWith('```') && part.endsWith('```')) { const code = part.slice(3, -3).replace(/^\w+\n/, ''); return
{code}
; } if (part.startsWith('`') && part.endsWith('`')) { return {part.slice(1, -1)}; } if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)}; } if (part === '\n') return
; return {part}; }); }; const modalContent = (
{/* VSCode Custom Titlebar */}
Logo e.currentTarget.style.display = 'none'} />
File Edit Selection View Go handleRun()}>Run Help
DataVision IDE Orchestrator
{/* Console & Execution Area */}
Local Server Orchestration
{!isRunning ? ( ) : ( <> {runningUrl && ( OPEN APP )} )}
{logs || "[Standby] Ready to orchestrate local project...\\nClick 'RUN PROJECT' to automatically detect frameworks and start the server."}
{/* Right Panel: AI Architect (Unchanged) */}
DataVision Agent
setRightPanelOpen(false)}/>
{showChatHistoryPanel ? (
setChatSearchQuery(e.target.value)} className="w-full bg-slate-100 dark:bg-[#2d2d2d] text-[12px] text-slate-800 dark:text-[#ccc] pl-8 pr-3 py-1.5 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" />
{chatHistory.filter(c => c.title.toLowerCase().includes(chatSearchQuery.toLowerCase())).map(chat => (
switchChat(chat.id)} className={`p-2.5 rounded cursor-pointer text-[12px] transition-colors ${activeChatId === chat.id ? 'bg-blue-50 dark:bg-[#2d2d30] text-blue-600 dark:text-[#fff]' : 'text-slate-600 dark:text-[#ccc] hover:bg-slate-50 dark:hover:bg-[#2d2d2d]'}`} >
{chat.title}
{chat.messages.length > 0 ? chat.messages[chat.messages.length - 1].content : 'Empty session'}
))}
) : (
{chatMessages.map((msg, i) => (
{msg.role === 'user' ? :
} {msg.role === 'user' ? 'You' : 'DataVision Agent'} {msg.role === 'user' && ( )}
{msg.role === 'ai' && msg.thoughts && msg.thoughts.length > 0 && (
Agent Thoughts ({msg.thoughts.length})
{msg.thoughts.map((t, idx) =>
{t}
)}
)} {msg.role === 'ai' && msg.status && (
{msg.status}
)} {msg.role === 'ai' ? renderChatContent(msg.content) : msg.content} {msg.role === 'ai' && msg.is_plan && (
)}
))}
)}
{ e.preventDefault(); handleChat(); }} className="relative flex flex-col border border-slate-300 dark:border-[#3c3c3c] bg-white dark:bg-[#3c3c3c] rounded focus-within:border-blue-600 dark:focus-within:border-[#007acc] transition-colors shadow-sm dark:shadow-none"> {chatImages.length > 0 && (
{chatImages.map((img, i) => (
{`attachment-${i}`}
))}
)}