diff --git a/frontend/app/error.tsx b/frontend/app/error.tsx new file mode 100644 index 0000000..683665d --- /dev/null +++ b/frontend/app/error.tsx @@ -0,0 +1,37 @@ +"use client" + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { useEffect } from "react"; + +export default function Error({ + error, + reset, +}: { + error: string; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+
+ + Initialization Failed + +

{error}

+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 3abd9be..a3bc090 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -2,85 +2,67 @@ @tailwind components; @tailwind utilities; -body { - font-family: Arial, Helvetica, sans-serif; -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} - @layer base { :root { --background: 0 0% 100%; - --foreground: 224 71.4% 4.1%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; - --card-foreground: 224 71.4% 4.1%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; - --popover-foreground: 224 71.4% 4.1%; - --primary: 220.9 39.3% 11%; - --primary-foreground: 210 20% 98%; - --secondary: 220 14.3% 95.9%; - --secondary-foreground: 220.9 39.3% 11%; - --muted: 220 14.3% 95.9%; - --muted-foreground: 220 8.9% 46.1%; - --accent: 220 14.3% 95.9%; - --accent-foreground: 220.9 39.3% 11%; + --popover-foreground: 240 10% 3.9%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 20% 98%; - --border: 220 13% 91%; - --input: 220 13% 91%; - --ring: 224 71.4% 4.1%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; + --destructive-foreground: 0 0% 98%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; } + .dark { - --background: 224 71.4% 4.1%; - --foreground: 210 20% 98%; - --card: 224 71.4% 4.1%; - --card-foreground: 210 20% 98%; - --popover: 224 71.4% 4.1%; - --popover-foreground: 210 20% 98%; - --primary: 210 20% 98%; - --primary-foreground: 220.9 39.3% 11%; - --secondary: 215 27.9% 16.9%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 215 27.9% 16.9%; - --accent-foreground: 210 20% 98%; + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 20% 98%; - --border: 215 27.9% 16.9%; - --input: 215 27.9% 16.9%; - --ring: 216 12.2% 83.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --destructive-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; } } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index efae28d..46950bf 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,20 +1,6 @@ import type { Metadata } from "next"; -import localFont from "next/font/local"; import "./globals.css"; -import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" -import { AppSidebar } from "@/components/sidebar"; -import { CSSProperties } from 'react'; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); +import { LoadingProvider } from '../context/loading-provider'; export const metadata: Metadata = { title: "Cotomata", @@ -23,26 +9,16 @@ export const metadata: Metadata = { export default function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { +}) { return ( - - - - -
- {children} -
-
+ + + + {children} + ); -} +} \ No newline at end of file diff --git a/frontend/app/loading.tsx b/frontend/app/loading.tsx new file mode 100644 index 0000000..8ccff6a --- /dev/null +++ b/frontend/app/loading.tsx @@ -0,0 +1,15 @@ +import { Loader2 } from "lucide-react"; + +export default function Loading() { + return ( +
+
+ +

Initializing Cotomata...

+

+ This may take up to 5 minutes. Please wait while we set up your environment. +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 6672fa9..77708be 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,395 +1,184 @@ -/** - * app/page.tsx - * - * This file serves as the main entry point for the React application. It manages the overall - * application state, handles socket connections for real-time communication, and coordinates - * interactions between various components such as the code editor, terminal, chat interface, - * and file system. - * - * Key Features: - * - Establishes a WebSocket connection to the server for real-time updates. - * - Manages application state using React hooks, including messages, terminal output, - * and active panels. - * - Handles incoming messages from the server, including agent actions and chat messages. - * - Provides a user interface for code editing, browsing, and terminal commands. - * - * Components Used: - * - CodeEditor: For editing code files. - * - FileSystem: For displaying and managing files. - * - Terminal: For executing commands and displaying output. - * - ChatInterface: For user interaction and messaging. - * - Browser: For displaying web content. - * - Sidebar: For navigation between different application panels. - * - SceneContext: For displaying context messages from agents. - * - */ "use client" -import React, { useEffect, useState } from 'react'; -// import io from 'socket.io-client'; -// import { CodeEditor } from '@/components/CodeEditor/CodeEditor'; -// import { FileSystem } from '@/components/CodeEditor/FileSystem'; -// import { Terminal } from '@/components/Terminal/Terminal'; -// import { ChatInterface } from '@/components/ChatInterface/ChatInterface'; -// import { Browser } from '@/components/Browser/Browser'; -// import { Sidebar } from '@/components/Sidebar/Sidebar'; -// import { SceneContext } from '@/components/Sidebar/SceneContext'; -import { useFileSystem } from "@/hooks/useFileSystem"; -import { FileSystem } from "@/components/file-system"; -import { io, Socket } from 'socket.io-client'; -import { CodeEditor } from '@/components/code-editor'; -import { Browser } from '@/components/browser'; -import { SceneContext } from '@/components/scene-context'; -import { ChatInterface } from '@/components/chat-interface'; -import { Terminal } from '@/components/terminal'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; -import { SidebarTrigger } from '@/components/ui/sidebar'; - -type PanelOption = 'fileSystem' | 'sceneContext'; - -// need for run-dataflow post req - -// async function runPythonCommand() { -// const response = await fetch('/api/run-agent', { -// method: 'POST', -// headers: { 'Content-Type': 'application/json' }, -// body: JSON.stringify({ command: 'print("Hello, Python!")' }), -// }); -// const result = await response.json(); -// console.log(result); -// } - -export default function App() { - const { - fileSystem, - openFiles, - activeFile, - handleFileSelect, - handleFileClose, - handleFileChange, - addFile, - setActiveFile, - updateFileSystemFromList - } = useFileSystem(); - - const [socket, setSocket] = useState(null); - const [activePanel, setActivePanel] = useState('fileSystem'); - const [activeTab, setActiveTab] = useState<'editor' | 'browser'>('editor'); - const [browserUrl, setBrowserUrl] = useState('https://example.com'); - const [messages, setMessages] = useState>([]); - const [terminalMessages, setTerminalMessages] = useState([]); - const [sceneMessages, setSceneMessages] = useState<{ text: string, agentName: string }[]>([]); - - useEffect(() => { - - // Initialize socket connection to the server - const socketInstance = io('http://localhost:3000', { - transports: ['websocket'], - reconnection: true - }); - - // Log connection status - socketInstance.on('connect', () => { - console.log('Connected to server with ID:', socketInstance.id); - // runPythonCommand(); - }); - console.log('socket connect', socketInstance) - - // Log connection errors - socketInstance.on('connect_error', (error) => { - console.error('Connection error:', error); - }); - - socketInstance.emit('init_process'); - socketInstance.on('init_process_result', (result) => { - if (result.status === 'error') { - console.error('Process initialization failed:', result.error); - } else { - console.log('Process started successfully'); - } - }); - - - setSocket(socketInstance); - - // Cleanup on component unmount - return () => { - if (socketInstance) { - socketInstance.disconnect(); - } - }; - }, []); - - useEffect(() => { - // Function to handle new messages received from the socket - const handleNewMessage = (data: any) => { - try { - // Parse the incoming message data - const messageData = JSON.parse(data.message); - - // Log the entire messageData for debugging - console.log('Received message data:', messageData); - - // Check if messageData.data is defined - if (!messageData.data) { - console.error('messageData.data is undefined:', messageData); - return; // Exit if data is not present - } - - // Handle Scene context messages - if (data.channel.startsWith('Scene:')) { - if (messageData.data.data_type === "text") { - // Update scene messages and set active panel to scene context - setSceneMessages(prev => [...prev, { text: messageData.data.text, agentName: data.channel }]); - setActivePanel('sceneContext'); - } - return; - } - - // Check if it's an agent action - if (messageData.data.data_type === "agent_action") { - handleAgentAction(messageData); - } - // Check if it's a command output - else if (messageData.data.data_type === "text" && - messageData.data.text.includes("CmdOutputObservation") && - !messageData.data.text.includes("**FILE_SYSTEM_REFRESH**")) { - // Try to extract output from success case (exit code=0) - let parts = messageData.data.text.split("**CmdOutputObservation (source=None, exit code=0)**"); - - // If not found, try to extract from error case (exit code=1) - if (parts.length === 1) { - parts = messageData.data.text.split("**CmdOutputObservation (source=None, exit code=1)**"); - } - - // If we found output in either case, add it to terminal messages - if (parts.length > 1) { - const outputText = parts[1].trim(); - // Update terminal messages with the command output - setTerminalMessages(prev => [...prev, outputText]); - } - } - - // Handle file structure refresh response - if (messageData.data.data_type === "text" && - messageData.data.text.includes("CmdOutputObservation") && - messageData.data.text.includes("**FILE_SYSTEM_REFRESH**")) { - const parts = messageData.data.text.split("**CmdOutputObservation (source=None, exit code=0)**"); - if (parts.length > 1) { - const fileList = parts[1].trim().split('\n').filter(Boolean).slice(1); - updateFileSystemFromList(fileList); - } - } - - // Handle file content response - if (messageData.data.data_type === "text" && - messageData.data.text.includes("**FILE_CONTENT**")) { - // Split the response by new lines - const lines = messageData.data.text.split('\n').slice(1); - console.log('lines', lines); - - // Check if the response has at least 3 parts - if (lines.length >= 3) { - const filePath = lines[1].trim(); // The second line contains the file path - console.log('filePath', filePath); - const fileContent = lines.slice(2).join('\n').trim(); // The rest is the file content - console.log('fileContent', fileContent); - // Update the file content using the handleFileChange function - handleFileChange(filePath, fileContent); // Update the file content - } - } - - } catch (error) { - // Log any errors that occur during message parsing - console.error('Error parsing message:', error); - } - }; - - // Listen for new messages from the socket - socket?.on('new_message', handleNewMessage); - return () => { - // Clean up the listener on component unmount - socket?.off('new_message', handleNewMessage); - }; - }, [updateFileSystemFromList]); - - // Function to handle actions from agents - const handleAgentAction = (messageData: any) => { - // Check if messageData.data is defined - if (!messageData.data) { - console.error('messageData.data is undefined:', messageData); - return; // Exit if data is not present - } - - const actionType = messageData.data.action_type; // Get the action type from the message - const agentName = messageData.data.agent_name; // Get the agent's name - - console.log('Processing agent action:', actionType, 'from', agentName); - - switch (actionType) { - case "speak": - // Handle agent speaking - const newMessage = { - text: `${agentName}: ${messageData.data.argument}`, - type: 'message' as const - }; - // Update messages state with the new message - setMessages(prev => [...prev, newMessage]); - break; - - case "thought": - // Handle agent's thoughts - setMessages(prev => [...prev, { - text: `💭 ${agentName} is thinking: ${messageData.data.argument}`, - type: 'status' as const - }]); - break; - - case "write": - // Handle file writing - const filePath = messageData.data.path; // Get the file path - const fileContent = messageData.data.argument; // Get the file content - - // Check if file already exists in openFiles - const existingFileIndex = openFiles.findIndex(f => f.path === filePath); - - if (existingFileIndex !== -1) { - // Update existing file content - handleFileChange(filePath, fileContent); - } else { - // Add new file - addFile(filePath, fileContent); - } - - // Set the active file and update the UI - setActiveFile(filePath); - setActiveTab('editor'); - setActivePanel('fileSystem'); - setMessages(prev => [...prev, { - text: `${agentName} is writing code...`, - type: 'status' as const - }]); - break; - - case "read": - // Check if messageData.data.text is defined - setMessages(prev => [...prev, { - text: `${agentName} is reading file ${messageData.data.path}`, - type: 'status' as const - }]); - break; - - case "run": - // Check if messageData.data.text is defined - // Handle command execution - setTerminalMessages(prev => [...prev, `$ ${messageData.data.argument}`]); - setMessages(prev => [...prev, { - text: `${agentName} is executing a command...`, - type: 'status' as const - }]); - break; - - case "browse": - // Handle browsing action - const url = messageData.data.argument; // Get the URL to browse - setBrowserUrl(url); - setActiveTab('browser'); - setMessages(prev => [...prev, { - text: `${agentName} is browsing ${url}`, - type: 'status' as const - }]); - break; - - default: - // Log unknown action types for debugging - console.log('Unknown action type:', actionType); +import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; +import { useLoading } from "../context/loading-provider"; +import Loading from "./loading"; +import Error from "./error"; +import { useState } from "react"; +import { Sparkles, Code2 } from 'lucide-react'; +import { motion } from 'framer-motion'; + +export default function LandingPage() { + const router = useRouter(); + const { isReady, error, socket } = useLoading(); + const [isInitializing, setIsInitializing] = useState(false); + + const handleStartSession = () => { + console.log('Start Session button clicked'); + setIsInitializing(true); + if (socket) { + console.log('Emitting init_process event'); + socket.emit('init_process'); + } else { + console.error('Socket not available'); + setIsInitializing(false); } }; - // Listen for chat messages from the socket - socket?.on('chat_message', (message: string) => { - // Update messages state with the new chat message - setMessages(prev => [...prev, { - text: message, - type: 'message' as const - }]); - }); + if (error) { + return router.refresh()} />; + } + + if (isReady) { + router.push('/workspace'); + return null; + } return ( -
-
- {/* setActivePanel('fileSystem')}> - File System - - setActivePanel('sceneContext')}> - Scene Context - */} - {activePanel === 'fileSystem' ? ( - - ) : ( - - )} -
-
- + {/* Header */} +
+ - - - - Editor - Browser - - - - - - - - - - - - - - -
-
- { - // Update messages state with the user's message - setMessages(prev => [...prev, { - text: `User: ${text}`, - type: 'message' as const - }]); - }} + + + Cotomata + + + + {/* Main Content */} +
+ {/* Background Effects */} + -
+
+ + {/* Content */} +
+
+ +
+ + + {/* Split text into individual letters for animation */} + {"Cotomata".split("").map((letter, index) => ( + + {letter} + + ))} + +
+ + Research Prototype for Studying Cooperative Agents + +
+ + + {isInitializing ? ( +
+

+ Initializing your development environment... +

+ +
+ ) : ( + + + + )} +
+
+
+
); } \ No newline at end of file diff --git a/frontend/app/workspace/layout.tsx b/frontend/app/workspace/layout.tsx new file mode 100644 index 0000000..8745503 --- /dev/null +++ b/frontend/app/workspace/layout.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import localFont from "next/font/local"; +import "../globals.css"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" +import { AppSidebar } from "@/components/sidebar"; +import { CSSProperties } from 'react'; +import { LoadingProvider } from "../../context/loading-provider"; + +const geistSans = localFont({ + src: "../fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "../fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); + +export const metadata: Metadata = { + title: "Cotomata", + description: "Research Prototype for Studying Cooperative Agents", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + +
+ {children} +
+
+ + + ); +} diff --git a/frontend/app/workspace/page.tsx b/frontend/app/workspace/page.tsx new file mode 100644 index 0000000..3b015e8 --- /dev/null +++ b/frontend/app/workspace/page.tsx @@ -0,0 +1,402 @@ +/** + * app/page.tsx + * + * This file serves as the main entry point for the React application. It manages the overall + * application state, handles socket connections for real-time communication, and coordinates + * interactions between various components such as the code editor, terminal, chat interface, + * and file system. + * + * Key Features: + * - Establishes a WebSocket connection to the server for real-time updates. + * - Manages application state using React hooks, including messages, terminal output, + * and active panels. + * - Handles incoming messages from the server, including agent actions and chat messages. + * - Provides a user interface for code editing, browsing, and terminal commands. + * + * Components Used: + * - CodeEditor: For editing code files. + * - FileSystem: For displaying and managing files. + * - Terminal: For executing commands and displaying output. + * - ChatInterface: For user interaction and messaging. + * - Browser: For displaying web content. + * - Sidebar: For navigation between different application panels. + * - SceneContext: For displaying context messages from agents. + * + */ +"use client" + +import React, { useEffect, useState } from 'react'; +// import io from 'socket.io-client'; +// import { CodeEditor } from '@/components/CodeEditor/CodeEditor'; +// import { FileSystem } from '@/components/CodeEditor/FileSystem'; +// import { Terminal } from '@/components/Terminal/Terminal'; +// import { ChatInterface } from '@/components/ChatInterface/ChatInterface'; +// import { Browser } from '@/components/Browser/Browser'; +// import { Sidebar } from '@/components/Sidebar/Sidebar'; +// import { SceneContext } from '@/components/Sidebar/SceneContext'; +import { useFileSystem } from "@/hooks/useFileSystem"; +import { FileSystem } from "@/components/file-system"; +import { io, Socket } from 'socket.io-client'; +import { CodeEditor } from '@/components/code-editor'; +import { Browser } from '@/components/browser'; +import { SceneContext } from '@/components/scene-context'; +import { ChatInterface } from '@/components/chat-interface'; +import { Terminal } from '@/components/terminal'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; +import { SidebarTrigger } from '@/components/ui/sidebar'; +import { useLoading } from "../../context/loading-provider"; +import { redirect } from "next/navigation"; + +type PanelOption = 'fileSystem' | 'sceneContext'; + +// need for run-dataflow post req + +// async function runPythonCommand() { +// const response = await fetch('/api/run-agent', { +// method: 'POST', +// headers: { 'Content-Type': 'application/json' }, +// body: JSON.stringify({ command: 'print("Hello, Python!")' }), +// }); +// const result = await response.json(); +// console.log(result); +// } + +export default function App() { + const { + fileSystem, + openFiles, + activeFile, + handleFileSelect, + handleFileClose, + handleFileChange, + addFile, + setActiveFile, + updateFileSystemFromList + } = useFileSystem(); + +// const [socket, setSocket] = useState(null); + const [activePanel, setActivePanel] = useState('fileSystem'); + const [activeTab, setActiveTab] = useState<'editor' | 'browser'>('editor'); + const [browserUrl, setBrowserUrl] = useState('https://example.com'); + const [messages, setMessages] = useState>([]); + const [terminalMessages, setTerminalMessages] = useState([]); + const [sceneMessages, setSceneMessages] = useState<{ text: string, agentName: string }[]>([]); + const { isReady, socket } = useLoading(); + + if (!isReady) { + redirect('/'); + } + +// useEffect(() => { + +// // Initialize socket connection to the server +// const socketInstance = io('http://localhost:3000', { +// transports: ['websocket'], +// reconnection: true +// }); + +// // Log connection status +// socketInstance.on('connect', () => { +// console.log('Connected to server with ID:', socketInstance.id); +// // runPythonCommand(); +// }); +// console.log('socket connect', socketInstance) + +// // Log connection errors +// socketInstance.on('connect_error', (error) => { +// console.error('Connection error:', error); +// }); + +// socketInstance.emit('init_process'); +// socketInstance.on('init_process_result', (result) => { +// if (result.status === 'error') { +// console.error('Process initialization failed:', result.error); +// } else { +// console.log('Process started successfully'); +// } +// }); + + +// setSocket(socketInstance); + +// // Cleanup on component unmount +// return () => { +// if (socketInstance) { +// socketInstance.disconnect(); +// } +// }; +// }, []); + + useEffect(() => { + // Function to handle new messages received from the socket + const handleNewMessage = (data: any) => { + try { + // Parse the incoming message data + const messageData = JSON.parse(data.message); + + // Log the entire messageData for debugging + console.log('Received message data:', messageData); + + // Check if messageData.data is defined + if (!messageData.data) { + console.error('messageData.data is undefined:', messageData); + return; // Exit if data is not present + } + + // Handle Scene context messages + if (data.channel.startsWith('Scene:')) { + if (messageData.data.data_type === "text") { + // Update scene messages and set active panel to scene context + setSceneMessages(prev => [...prev, { text: messageData.data.text, agentName: data.channel }]); + setActivePanel('sceneContext'); + } + return; + } + + // Check if it's an agent action + if (messageData.data.data_type === "agent_action") { + handleAgentAction(messageData); + } + // Check if it's a command output + else if (messageData.data.data_type === "text" && + messageData.data.text.includes("CmdOutputObservation") && + !messageData.data.text.includes("**FILE_SYSTEM_REFRESH**")) { + // Try to extract output from success case (exit code=0) + let parts = messageData.data.text.split("**CmdOutputObservation (source=None, exit code=0)**"); + + // If not found, try to extract from error case (exit code=1) + if (parts.length === 1) { + parts = messageData.data.text.split("**CmdOutputObservation (source=None, exit code=1)**"); + } + + // If we found output in either case, add it to terminal messages + if (parts.length > 1) { + const outputText = parts[1].trim(); + // Update terminal messages with the command output + setTerminalMessages(prev => [...prev, outputText]); + } + } + + // Handle file structure refresh response + if (messageData.data.data_type === "text" && + messageData.data.text.includes("CmdOutputObservation") && + messageData.data.text.includes("**FILE_SYSTEM_REFRESH**")) { + const parts = messageData.data.text.split("**CmdOutputObservation (source=None, exit code=0)**"); + if (parts.length > 1) { + const fileList = parts[1].trim().split('\n').filter(Boolean).slice(1); + updateFileSystemFromList(fileList); + } + } + + // Handle file content response + if (messageData.data.data_type === "text" && + messageData.data.text.includes("**FILE_CONTENT**")) { + // Split the response by new lines + const lines = messageData.data.text.split('\n').slice(1); + console.log('lines', lines); + + // Check if the response has at least 3 parts + if (lines.length >= 3) { + const filePath = lines[1].trim(); // The second line contains the file path + console.log('filePath', filePath); + const fileContent = lines.slice(2).join('\n').trim(); // The rest is the file content + console.log('fileContent', fileContent); + // Update the file content using the handleFileChange function + handleFileChange(filePath, fileContent); // Update the file content + } + } + + } catch (error) { + // Log any errors that occur during message parsing + console.error('Error parsing message:', error); + } + }; + + // Listen for new messages from the socket + socket?.on('new_message', handleNewMessage); + return () => { + // Clean up the listener on component unmount + socket?.off('new_message', handleNewMessage); + }; + }, [updateFileSystemFromList]); + + // Function to handle actions from agents + const handleAgentAction = (messageData: any) => { + // Check if messageData.data is defined + if (!messageData.data) { + console.error('messageData.data is undefined:', messageData); + return; // Exit if data is not present + } + + const actionType = messageData.data.action_type; // Get the action type from the message + const agentName = messageData.data.agent_name; // Get the agent's name + + console.log('Processing agent action:', actionType, 'from', agentName); + + switch (actionType) { + case "speak": + // Handle agent speaking + const newMessage = { + text: `${agentName}: ${messageData.data.argument}`, + type: 'message' as const + }; + // Update messages state with the new message + setMessages(prev => [...prev, newMessage]); + break; + + case "thought": + // Handle agent's thoughts + setMessages(prev => [...prev, { + text: `💭 ${agentName} is thinking: ${messageData.data.argument}`, + type: 'status' as const + }]); + break; + + case "write": + // Handle file writing + const filePath = messageData.data.path; // Get the file path + const fileContent = messageData.data.argument; // Get the file content + + // Check if file already exists in openFiles + const existingFileIndex = openFiles.findIndex(f => f.path === filePath); + + if (existingFileIndex !== -1) { + // Update existing file content + handleFileChange(filePath, fileContent); + } else { + // Add new file + addFile(filePath, fileContent); + } + + // Set the active file and update the UI + setActiveFile(filePath); + setActiveTab('editor'); + setActivePanel('fileSystem'); + setMessages(prev => [...prev, { + text: `${agentName} is writing code...`, + type: 'status' as const + }]); + break; + + case "read": + // Check if messageData.data.text is defined + setMessages(prev => [...prev, { + text: `${agentName} is reading file ${messageData.data.path}`, + type: 'status' as const + }]); + break; + + case "run": + // Check if messageData.data.text is defined + // Handle command execution + setTerminalMessages(prev => [...prev, `$ ${messageData.data.argument}`]); + setMessages(prev => [...prev, { + text: `${agentName} is executing a command...`, + type: 'status' as const + }]); + break; + + case "browse": + // Handle browsing action + const url = messageData.data.argument; // Get the URL to browse + setBrowserUrl(url); + setActiveTab('browser'); + setMessages(prev => [...prev, { + text: `${agentName} is browsing ${url}`, + type: 'status' as const + }]); + break; + + default: + // Log unknown action types for debugging + console.log('Unknown action type:', actionType); + } + }; + + // Listen for chat messages from the socket + socket?.on('chat_message', (message: string) => { + // Update messages state with the new chat message + setMessages(prev => [...prev, { + text: message, + type: 'message' as const + }]); + }); + + return ( +
+
+ {/* setActivePanel('fileSystem')}> + File System + + setActivePanel('sceneContext')}> + Scene Context + */} + {activePanel === 'fileSystem' ? ( + + ) : ( + + )} +
+
+ + + + + Editor + Browser + + + + + + + + + + + + + + +
+
+ { + // Update messages state with the user's message + setMessages(prev => [...prev, { + text: `User: ${text}`, + type: 'message' as const + }]); + }} + /> +
+
+ ); +} \ No newline at end of file diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 5ebbab9..fcc7f52 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/components/code-editor.tsx b/frontend/components/code-editor.tsx index 694ea79..a6ea79b 100644 --- a/frontend/components/code-editor.tsx +++ b/frontend/components/code-editor.tsx @@ -75,54 +75,57 @@ export const CodeEditor = ({ return ( - -
- {openFiles.map((file) => ( -
- - - -
- ))} + + + +
+ ))} +
+ + +
+
- -
-
diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/frontend/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx index cabfbfc..e876783 100644 --- a/frontend/components/ui/card.tsx +++ b/frontend/components/ui/card.tsx @@ -1,5 +1,4 @@ import * as React from "react" - import { cn } from "@/lib/utils" const Card = React.forwardRef< @@ -9,7 +8,7 @@ const Card = React.forwardRef<
+ HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
)) CardTitle.displayName = "CardTitle" const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes + HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
({ + isReady: false, + error: null, + socket: null, +}); + +export const useLoading = () => useContext(LoadingContext); + +export function LoadingProvider({ children }: { children: React.ReactNode }) { + const [isReady, setIsReady] = useState(false); + const [error, setError] = useState(null); + const [socket, setSocket] = useState(null); + + useEffect(() => { + let socketInstance: Socket; + + const initialize = async () => { + try { + console.log('Creating socket instance...'); + socketInstance = io('http://localhost:3000', { + transports: ['websocket'], + reconnection: true, + autoConnect: true + }); + + // Log all incoming events for debugging + socketInstance.onAny((eventName, ...args) => { + console.log(`[Socket Event] ${eventName}:`, args); + }); + + socketInstance.on('connect', () => { + console.log('Socket connected with ID:', socketInstance.id); + setSocket(socketInstance); + }); + + socketInstance.on('connect_error', (error) => { + console.error('Socket connection error:', error); + setError(`Connection failed: ${error.message}`); + }); + + socketInstance.on('init_process_result', (result) => { + console.log('Received init_process_result:', result); + if (result.success) { + setIsReady(true); + } else { + setError(result.error || 'Failed to initialize OpenHands'); + socketInstance.disconnect(); + } + }); + } catch (err) { + console.error('Socket initialization error:', err); + setError(err instanceof Error ? err.message : 'An unknown error occurred'); + } + }; + + initialize(); + + return () => { + console.log('Cleaning up socket connection...'); + socketInstance?.disconnect(); + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index bd0c391..d084cca 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from "clsx" +import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { diff --git a/frontend/package.json b/frontend/package.json index 65318c2..f32ba1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@codemirror/lang-python": "^6.1.6", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", @@ -24,12 +25,13 @@ "@uiw/react-codemirror": "^4.23.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^0.468.0", + "framer-motion": "^11.15.0", + "lucide-react": "^0.469.0", "next": "14.2.16", "react": "^18", "react-dom": "^18", - "react-resizable-panels": "^2.1.7", "react-markdown": "^9.0.1", + "react-resizable-panels": "^2.1.7", "redis": "^4.7.0", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", diff --git a/frontend/public/grid.svg b/frontend/public/grid.svg new file mode 100644 index 0000000..8e75e54 --- /dev/null +++ b/frontend/public/grid.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/server.js b/frontend/server.js index e0cef55..3d8a79d 100644 --- a/frontend/server.js +++ b/frontend/server.js @@ -108,20 +108,38 @@ app.prepare().then(async () => { // Handle process initialization socket.on('init_process', async () => { try { - const response = await fetch('http://localhost:5000/run-dataflow', { + const initParams = { + node_name: "openhands_node", + input_channels: ["Agent:Runtime"], + output_channels: ["Runtime:Agent"], + modal_session_id: "arpan" + }; + + const response = await fetch('http://localhost:5000/initialize', { method: 'POST', - headers: { 'Content-Type': 'application/json' } + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(initParams) }); if (!response.ok) { - throw new Error(`Failed to initialize process: ${response.statusText}`); + const errorData = await response.json(); + throw new Error(`Failed to initialize process: ${errorData.error || response.statusText}`); } const result = await response.json(); - socket.emit('init_process_result', result); - console.log('openhands connected') + + if (result.status === 'initialized') { + socket.emit('init_process_result', { success: true }); + console.log('OpenHands initialized successfully'); + } else { + throw new Error(`Unexpected initialization status: ${result.status}`); + } } catch (err) { console.error('Error initializing process:', err); + socket.emit('init_process_result', { + success: false, + error: err.message + }); } });