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}
window.location.reload()} className="mt-8 px-4 py-2 bg-red-600 text-white rounded w-fit">Reload Page
);
}
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 */}
e.currentTarget.style.display = 'none'} />
File
Edit
Selection
View
Go
handleRun()}>Run
Help
DataVision IDE Orchestrator
setShowCloseModal(true)} />
{/* Orchestrator Hub (Center) */}
e.currentTarget.style.display = 'none'} />
DataVision IDE Orchestrator
DataVision is now a Local Project Orchestrator .
Here is how it works:
1. The DataVision Agent on the right writes the code and plans your architecture.
2. You click "Open in [IDE]" below to view or edit the code in your preferred local environment.
3. You click "RUN PROJECT" below to automatically detect frameworks, install dependencies, and start the local server.
Prefer your own terminal? Click Copy Path below and run the project manually in your local laptop terminal!
Open in VS Code
Open in Cursor
Open in Antigravity
{
navigator.clipboard.writeText(workspacePath);
alert('Workspace path copied to clipboard!');
}}
className="flex items-center gap-2 px-6 py-3 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-800 dark:text-white rounded-lg font-semibold shadow-lg transition-colors"
>
Copy Path
{/* Console & Execution Area */}
Local Server Orchestration
{!isRunning ? (
handleRun()} className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded font-bold flex items-center gap-2 shadow transition-colors text-sm">
RUN PROJECT
) : (
<>
{runningUrl && (
OPEN APP
)}
STOP
>
)}
{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
setLlm(e.target.value)}
className="bg-transparent text-[10px] text-slate-500 dark:text-[#858585] outline-none cursor-pointer hover:text-slate-800 dark:hover:text-[#ccc]"
title="Select AI Model"
>
Llama 3.3 70B
DeepSeek V4
Nemotron Ultra
GLM 5.1
Kimi 2.6
setShowChatHistoryPanel(!showChatHistoryPanel)} className="text-slate-500 hover:text-blue-500 dark:text-[#858585] dark:hover:text-[#007acc] transition-colors" title="Chat History">
setRightPanelOpen(false)}/>
{showChatHistoryPanel ? (
{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' && (
setChatInput(msg.content)} className="ml-2 p-0.5 hover:bg-slate-200 dark:hover:bg-[#444] rounded text-blue-500 transition-colors" title="Edit Query">
)}
{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 && (
handleChat(lastPrompt, 'execute')} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-[12px] font-semibold rounded shadow-sm flex items-center gap-1.5 transition-colors">
Build It
)}
))}
)}
{/* Close Modal */}
{showCloseModal && (
Session Management
How would you like to handle your current workspace?
{isRunning && <>Note: You have a server process currently running.>}
{
setShowCloseModal(false);
if (onClose) onClose();
}}
className="w-full px-4 py-2.5 text-sm font-semibold bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors text-left flex items-center justify-between"
>
Keep Session Running in Background
Recommended
{
setShowCloseModal(false);
await handleStop();
if (onClose) onClose();
}}
className="w-full px-4 py-2.5 text-sm font-semibold bg-red-600 hover:bg-red-700 text-white rounded transition-colors text-left flex items-center justify-between"
>
End Session & Close IDE
Kills Process
setShowCloseModal(false)}
className="w-full px-4 py-2 text-sm text-slate-600 dark:text-[#cccccc] hover:bg-slate-100 dark:hover:bg-[#333333] rounded transition-colors mt-2"
>
Cancel
)}
);
if (typeof document !== 'undefined') {
return createPortal(
{modalContent}
,
document.body
);
}
return null;
};