diff --git a/README.md b/README.md index 90fb40f..8cac223 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ English | [ไธญๆ–‡](README_zh.md) Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* ๐Ÿ›ซ! -Our team -members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building! +Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building! It's a simple implementation, so we welcome any suggestions, contributions, and feedback! diff --git a/app.py b/app.py index 2e1959a..0f7425b 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,5 @@ import asyncio -import threading import uuid -import webbrowser from datetime import datetime from json import dumps @@ -175,7 +173,8 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n" + status_data = {"type": "status", "status": task.status, "steps": task.steps} + yield f"event: status\ndata: {dumps(status_data)}\n\n" while True: try: @@ -193,7 +192,12 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n" + status_data = { + "type": "status", + "status": task.status, + "steps": task.steps, + } + yield f"event: status\ndata: {dumps(status_data)}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" @@ -244,12 +248,7 @@ async def generic_exception_handler(request: Request, exc: Exception): ) -def open_local_browser(): - webbrowser.open_new_tab("http://localhost:5172") - - if __name__ == "__main__": import uvicorn - threading.Timer(3, open_local_browser).start() uvicorn.run(app, host="localhost", port=5172) diff --git a/openmanus_server/README.md b/openmanus_server/README.md new file mode 100644 index 0000000..6992f21 --- /dev/null +++ b/openmanus_server/README.md @@ -0,0 +1,130 @@ +# OpenManus-server ๐Ÿค– + +This project provides a server based on [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) that exposes **OpenManus** tool functionalities as standardized APIs. + +## โœจ Features + +This MCP server provides access to the following OpenManus tools: + +1. **Browser Automation** ๐ŸŒ - Navigate webpages, click elements, input text, and more +2. **Google Search** ๐Ÿ” - Execute searches and retrieve result links +3. **Python Code Execution** ๐Ÿ - Run Python code in a secure environment +4. **File Saving** ๐Ÿ’พ - Save content to local files +5. **Termination Control** ๐Ÿ›‘ - Control program execution flow + +## ๐Ÿš€ Installation + +### Prerequisites + +- Python 3.10+ +- OpenManus project dependencies + +### Installation Steps + +1. First, install the OpenManus project: + +```bash +git clone https://github.com/mannaandpoem/OpenManus.git +cd OpenManus +``` + +2. Install dependencies: + +```bash +# Using uv (recommended) +curl -LsSf https://astral.sh/uv/install.sh | sh +uv venv +source .venv/bin/activate # Unix/macOS +# or .venv\Scripts\activate # Windows +uv pip install -r requirements.txt +``` + +3. Install MCP dependencies: + +```bash +uv pip install -r openmanus_server/mcp_requirements.txt +``` + +## Demo display + + + + +## ๐Ÿ“– Usage + +### 1. Testing your server with Claude for Desktop ๐Ÿ–ฅ๏ธ + +> โš ๏ธ **Note**: Claude for Desktop is not yet available on Linux. Linux users can build an MCP client that connects to the server we just built. + +#### Step 1: Installation Check โœ… +First, make sure you have Claude for Desktop installed. [You can install the latest version here](https://claude.ai/download). If you already have Claude for Desktop, **make sure it's updated to the latest version**. + +#### Step 2: Configuration Setup โš™๏ธ +We'll need to configure Claude for Desktop for this server you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist. + +```bash +vim ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +#### Step 3: Server Configuration ๐Ÿ”ง +You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured. + +In this case, we'll add our single Openmanus server like so: +```json +{ + "mcpServers": { + "openmanus": { + "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/uv", + "args": [ + "--directory", + "/ABSOLUTE/PATH/TO/OpenManus/openmanus_server", + "run", + "openmanus_server.py" + ] + } + } +} +``` + +> ๐Ÿ’ก **Tip**: You may need to put the full path to the uv executable in the command field. You can get this by running: +> - MacOS/Linux: `which uv` +> - Windows: `where uv` + +#### Step 4: Understanding the Configuration ๐Ÿ“ +This tells Claude for Desktop: +1. There's an MCP server named "openmanus" ๐Ÿ”Œ +2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/OpenManus/openmanus_server run openmanus_server.py` ๐Ÿš€ + +#### Step 5: Activation ๐Ÿ”„ +Save the file, and restart Claude for Desktop. + +#### Step 6: Verification โœจ +Let's make sure Claude for Desktop is picking up the six tools we've exposed in our `openmanus` server. You can do this by looking for the hammer icon ![hammer icon](./assets/claude-desktop-mcp-hammer-icon.svg) +![tools_in_claude](./assets/1.jpg) + +After clicking on the hammer icon, you should see tools listed: +![alvaliable_tools_list](./assets/2.png) + +#### Ready to Test! ๐ŸŽ‰ +**Now, you can test the openmanus server in Claude for Desktop**: +* ๐Ÿ” Try to find the recent news about Manus AI agent, and write a post for me! + + + +### ๐Ÿ’ป 2. Testing with simple Client Example + +Check out `openmanus_client_example.py` to test the openmanus server using the MCP client. + +``` +uv run openmanus_server/openmanus_client_example.py openmanus_server/openmanus_server.py +``` + + +## ๐Ÿ”’ Security Considerations + +- When using in production, ensure proper authentication and authorization mechanisms are in place +- The Python execution tool has timeout limits to prevent long-running code + +## ๐Ÿ“„ License + +Same license as the OpenManus project diff --git a/openmanus_server/assets/1.jpg b/openmanus_server/assets/1.jpg new file mode 100644 index 0000000..9ea13a7 Binary files /dev/null and b/openmanus_server/assets/1.jpg differ diff --git a/openmanus_server/assets/2.png b/openmanus_server/assets/2.png new file mode 100644 index 0000000..c9005c9 Binary files /dev/null and b/openmanus_server/assets/2.png differ diff --git a/openmanus_server/assets/claude-desktop-mcp-hammer-icon.svg b/openmanus_server/assets/claude-desktop-mcp-hammer-icon.svg new file mode 100644 index 0000000..d8e4f80 --- /dev/null +++ b/openmanus_server/assets/claude-desktop-mcp-hammer-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmanus_server/assets/demo.mp4 b/openmanus_server/assets/demo.mp4 new file mode 100644 index 0000000..a1c9a89 Binary files /dev/null and b/openmanus_server/assets/demo.mp4 differ diff --git a/openmanus_server/mcp_requirements.txt b/openmanus_server/mcp_requirements.txt new file mode 100644 index 0000000..cba6c36 --- /dev/null +++ b/openmanus_server/mcp_requirements.txt @@ -0,0 +1,3 @@ +# Core dependencies +mcp +httpx>=0.27.0 diff --git a/openmanus_server/openmanus_client_example.py b/openmanus_server/openmanus_client_example.py new file mode 100644 index 0000000..3b8e8aa --- /dev/null +++ b/openmanus_server/openmanus_client_example.py @@ -0,0 +1,181 @@ +import asyncio +import os +from contextlib import AsyncExitStack +from typing import Optional + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + + +class OpenManusClient: + def __init__(self): + # Initialize session and client objects + self.session: Optional[ClientSession] = None + self.exit_stack = AsyncExitStack() + self.stdio = None + self.write = None + + async def connect_to_server(self, server_script_path: str): + """Connect to an MCP server via stdio + + Args: + server_script_path: Path to the server script + """ + if not server_script_path.endswith(".py"): + raise ValueError("Server script must be a .py file") + + # Get the current directory to add to PYTHONPATH + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(current_dir) # Get parent directory + + # Prepare environment variables + env = os.environ.copy() # Copy current environment + + # Add current directory and project root to PYTHONPATH + path_separator = ( + ";" if os.name == "nt" else ":" + ) # Use ; for Windows, : for Unix + if "PYTHONPATH" in env: + env[ + "PYTHONPATH" + ] = f"{current_dir}{path_separator}{project_root}{path_separator}{env['PYTHONPATH']}" + else: + env["PYTHONPATH"] = f"{current_dir}{path_separator}{project_root}" + + server_params = StdioServerParameters( + command="python", args=[server_script_path], env=env + ) + + stdio_transport = await self.exit_stack.enter_async_context( + stdio_client(server_params) + ) + self.stdio, self.write = stdio_transport + self.session = await self.exit_stack.enter_async_context( + ClientSession(self.stdio, self.write) + ) + await self.session.initialize() + + # List available tools + response = await self.session.list_tools() + tools = response.tools + print("\nConnected to server with tools:", [tool.name for tool in tools]) + return tools + + async def run_examples(self): + """Run example tool calls to demonstrate functionality""" + try: + print("\nExample 1: Google Search") + search_result = await self.session.call_tool( + "google_search", {"query": "Model Context Protocol", "num_results": 5} + ) + print(f"Search results: {search_result.content}") + + print("\nExample 2: Python Code Execution") + code = """ +import math +result = 0 +for i in range(1, 10): + result += math.sqrt(i) +print(f"Calculation result: {result}") +""" + python_result = await self.session.call_tool( + "python_execute", {"code": code, "timeout": 3} + ) + print(f"Python execution result: {python_result.content}") + + print("\nExample 3: File Saving") + file_result = await self.session.call_tool( + "file_saver", + { + "content": "This is a test file content saved through MCP", + "file_path": "mcp_test_file.txt", + }, + ) + print(f"File save result: {file_result.content}") + + print("\nExample 4: Browser Usage") + # Navigate to webpage + browser_result = await self.session.call_tool( + "browser_use", {"action": "navigate", "url": "https://www.example.com"} + ) + print(f"Browser navigation result: {browser_result.content}") + + # Get browser state + state_result = await self.session.call_tool("get_browser_state", {}) + print(f"Browser state: {state_result.content}") + + except Exception as e: + print(f"\nError during example execution: {str(e)}") + + async def chat_loop(self): + """Run an interactive chat loop for testing tools""" + print("\nOpenManus MCP Client Started!") + print("Type your commands or 'quit' to exit.") + print( + "Available commands: google_search, python_execute, file_saver, browser_use, get_browser_state" + ) + + while True: + try: + command = input("\nCommand: ").strip() + + if command.lower() == "quit": + break + + # Parse command and parameters + parts = command.split(maxsplit=1) + if len(parts) == 0: + continue + + tool_name = parts[0] + tool_args = {} + if len(parts) > 1: + try: + tool_args = eval(parts[1]) # Convert string to dict + except: + print( + "Invalid arguments format. Please provide a valid Python dictionary." + ) + continue + + result = await self.session.call_tool(tool_name, tool_args) + print("\nResult:", result.content) + + except Exception as e: + print(f"\nError: {str(e)}") + + async def cleanup(self): + """Clean up resources""" + if self.session: + await self.session.close() + await self.exit_stack.aclose() + print("\nClosed MCP client connection") + + +async def main(): + """Main entry point""" + import sys + + if len(sys.argv) < 2: + print("Usage: python openmanus_client_example.py ") + print("Example: python openmanus_client_example.py ../mcp_server.py") + sys.exit(1) + + client = OpenManusClient() + try: + await client.connect_to_server(server_script_path=sys.argv[1]) + + # Run examples first + await client.run_examples() + + # Then start interactive chat loop + await client.chat_loop() + + except Exception as e: + print(f"Error: {str(e)}") + finally: + await client.cleanup() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/openmanus_server/openmanus_server.py b/openmanus_server/openmanus_server.py new file mode 100644 index 0000000..d73add4 --- /dev/null +++ b/openmanus_server/openmanus_server.py @@ -0,0 +1,196 @@ +import argparse +import asyncio +import json +import logging +import os +import sys +from typing import Optional + +from mcp.server.fastmcp import FastMCP + + +# Add current directory to Python path +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) +sys.path.insert(0, current_dir) + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("mcp-server") + +# Import OpenManus tools +from app.tool.browser_use_tool import BrowserUseTool +from app.tool.file_saver import FileSaver +from app.tool.google_search import GoogleSearch +from app.tool.python_execute import PythonExecute +from app.tool.terminate import Terminate + + +# Initialize FastMCP server +openmanus = FastMCP("openmanus") + +# Initialize tool instances +browser_tool = BrowserUseTool() +google_search_tool = GoogleSearch() +python_execute_tool = PythonExecute() +file_saver_tool = FileSaver() +terminate_tool = Terminate() + + +# Browser tool +@openmanus.tool() +async def browser_use( + action: str, + url: Optional[str] = None, + index: Optional[int] = None, + text: Optional[str] = None, + script: Optional[str] = None, + scroll_amount: Optional[int] = None, + tab_id: Optional[int] = None, +) -> str: + """Execute various browser operations. + + Args: + action: The browser operation to execute, possible values include: + - navigate: Navigate to specified URL + - click: Click on an element on the page + - input_text: Input text into a text field + - screenshot: Take a screenshot of the current page + - get_html: Get HTML of the current page + - get_text: Get text content of the current page + - execute_js: Execute JavaScript code + - scroll: Scroll the page + - switch_tab: Switch to specified tab + - new_tab: Open new tab + - close_tab: Close current tab + - refresh: Refresh current page + url: URL for 'navigate' or 'new_tab' operations + index: Element index for 'click' or 'input_text' operations + text: Text for 'input_text' operation + script: JavaScript code for 'execute_js' operation + scroll_amount: Scroll pixels for 'scroll' operation (positive for down, negative for up) + tab_id: Tab ID for 'switch_tab' operation + """ + logger.info(f"Executing browser operation: {action}") + result = await browser_tool.execute( + action=action, + url=url, + index=index, + text=text, + script=script, + scroll_amount=scroll_amount, + tab_id=tab_id, + ) + return json.dumps(result.model_dump()) + + +@openmanus.tool() +async def get_browser_state() -> str: + """Get current browser state, including URL, title, tabs and interactive elements.""" + logger.info("Getting browser state") + result = await browser_tool.get_current_state() + return json.dumps(result.model_dump()) + + +# Google search tool +@openmanus.tool() +async def google_search(query: str, num_results: int = 10) -> str: + """Execute Google search and return list of relevant links. + + Args: + query: Search query + num_results: Number of results to return (default is 10) + """ + logger.info(f"Executing Google search: {query}") + results = await google_search_tool.execute(query=query, num_results=num_results) + return json.dumps(results) + + +# Python execution tool +@openmanus.tool() +async def python_execute(code: str, timeout: int = 5) -> str: + """Execute Python code and return results. + + Args: + code: Python code to execute + timeout: Execution timeout in seconds + """ + logger.info("Executing Python code") + result = await python_execute_tool.execute(code=code, timeout=timeout) + return json.dumps(result) + + +# File saver tool +@openmanus.tool() +async def file_saver(content: str, file_path: str, mode: str = "w") -> str: + """Save content to local file. + + Args: + content: Content to save + file_path: File path + mode: File open mode (default is 'w') + """ + logger.info(f"Saving file: {file_path}") + result = await file_saver_tool.execute( + content=content, file_path=file_path, mode=mode + ) + return result + + +# Terminate tool +@openmanus.tool() +async def terminate(status: str) -> str: + """Terminate program execution. + + Args: + status: Termination status, can be 'success' or 'failure' + """ + logger.info(f"Terminating execution: {status}") + result = await terminate_tool.execute(status=status) + return result + + +# Clean up resources +async def cleanup(): + """Clean up all tool resources""" + logger.info("Cleaning up resources") + await browser_tool.cleanup() + + +# Register cleanup function +import atexit + + +atexit.register(lambda: asyncio.run(cleanup())) + + +def parse_args(): + """Parse command line arguments""" + parser = argparse.ArgumentParser(description="OpenManus MCP Server") + parser.add_argument( + "--transport", + choices=["stdio", "http"], + default="stdio", + help="Communication method: stdio or http (default: stdio)", + ) + parser.add_argument( + "--host", default="127.0.0.1", help="HTTP server host (default: 127.0.0.1)" + ) + parser.add_argument( + "--port", type=int, default=8000, help="HTTP server port (default: 8000)" + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + if args.transport == "stdio": + logger.info("Starting OpenManus server (stdio mode)") + openmanus.run(transport="stdio") + else: + logger.info(f"Starting OpenManus server (HTTP mode) at {args.host}:{args.port}") + openmanus.run(transport="http", host=args.host, port=args.port) diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 4bbeada..0000000 --- a/pytest.ini +++ /dev/null @@ -1,14 +0,0 @@ -[pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* - -# Log settings -log_cli = true -log_cli_level = INFO -log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) -log_cli_date_format = %Y-%m-%d %H:%M:%S - -# Make sure asyncio works properly -asyncio_mode = auto diff --git a/run.bat b/run.bat deleted file mode 100644 index 4c48f2f..0000000 --- a/run.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -venv\Scripts\python.exe app.py -pause diff --git a/static/main.js b/static/main.js deleted file mode 100644 index c219d20..0000000 --- a/static/main.js +++ /dev/null @@ -1,307 +0,0 @@ -let currentEventSource = null; - -function createTask() { - const promptInput = document.getElementById('prompt-input'); - const prompt = promptInput.value.trim(); - - if (!prompt) { - alert("Please enter a valid prompt"); - promptInput.focus(); - return; - } - - if (currentEventSource) { - currentEventSource.close(); - currentEventSource = null; - } - - const container = document.getElementById('task-container'); - container.innerHTML = '
Initializing task...
'; - document.getElementById('input-container').classList.add('bottom'); - - fetch('/tasks', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ prompt }) - }) - .then(response => { - if (!response.ok) { - return response.json().then(err => { throw new Error(err.detail || 'Request failed') }); - } - return response.json(); - }) - .then(data => { - if (!data.task_id) { - throw new Error('Invalid task ID'); - } - setupSSE(data.task_id); - loadHistory(); - }) - .catch(error => { - container.innerHTML = `
Error: ${error.message}
`; - console.error('Failed to create task:', error); - }); -} - -function setupSSE(taskId) { - let retryCount = 0; - const maxRetries = 3; - const retryDelay = 2000; - - const container = document.getElementById('task-container'); - - function connect() { - const eventSource = new EventSource(`/tasks/${taskId}/events`); - currentEventSource = eventSource; - - let heartbeatTimer = setInterval(() => { - container.innerHTML += '
ยท
'; - }, 5000); - - const pollInterval = setInterval(() => { - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Polling failed:', error); - }); - }, 10000); - - const handleEvent = (event, type) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - container.classList.add('active'); - - const stepContainer = ensureStepContainer(container); - const { formattedContent, timestamp } = formatStepContent(data, type); - const step = createStepElement(type, formattedContent, timestamp); - - stepContainer.appendChild(step); - autoScroll(stepContainer); - - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error(`Error handling ${type} event:`, e); - } - }; - - const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']; - eventTypes.forEach(type => { - eventSource.addEventListener(type, (event) => handleEvent(event, type)); - }); - - eventSource.addEventListener('complete', (event) => { - clearInterval(heartbeatTimer); - clearInterval(pollInterval); - container.innerHTML += ` -
-
โœ… Task completed
-
${lastResultContent}
-
- `; - eventSource.close(); - currentEventSource = null; - }); - - eventSource.addEventListener('error', (event) => { - clearInterval(heartbeatTimer); - clearInterval(pollInterval); - try { - const data = JSON.parse(event.data); - container.innerHTML += ` -
- โŒ Error: ${data.message} -
- `; - eventSource.close(); - currentEventSource = null; - } catch (e) { - console.error('Error handling failed:', e); - } - }); - - eventSource.onerror = (err) => { - if (eventSource.readyState === EventSource.CLOSED) return; - - console.error('SSE connection error:', err); - clearInterval(heartbeatTimer); - clearInterval(pollInterval); - eventSource.close(); - - if (retryCount < maxRetries) { - retryCount++; - container.innerHTML += ` -
- โš  Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})... -
- `; - setTimeout(connect, retryDelay); - } else { - container.innerHTML += ` -
- โš  Connection lost, please try refreshing the page -
- `; - } - }; - } - - connect(); -} - -function loadHistory() { - fetch('/tasks') - .then(response => { - if (!response.ok) { - return response.text().then(text => { - throw new Error(`่ฏทๆฑ‚ๅคฑ่ดฅ: ${response.status} - ${text.substring(0, 100)}`); - }); - } - return response.json(); - }) - .then(tasks => { - const listContainer = document.getElementById('task-list'); - listContainer.innerHTML = tasks.map(task => ` -
-
${task.prompt}
-
- ${new Date(task.created_at).toLocaleString()} - - - ${task.status || 'ๆœช็Ÿฅ็Šถๆ€'} - -
-
- `).join(''); - }) - .catch(error => { - console.error('ๅŠ ่ฝฝๅކๅฒ่ฎฐๅฝ•ๅคฑ่ดฅ:', error); - const listContainer = document.getElementById('task-list'); - listContainer.innerHTML = `
ๅŠ ่ฝฝๅคฑ่ดฅ: ${error.message}
`; - }); -} - - -function ensureStepContainer(container) { - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - return stepContainer; -} - -function formatStepContent(data, eventType) { - return { - formattedContent: data.result, - timestamp: new Date().toLocaleTimeString() - }; -} - -function createStepElement(type, content, timestamp) { - const step = document.createElement('div'); - step.className = `step-item ${type}`; - step.innerHTML = ` -
- ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: -
${content}
-
- `; - return step; -} - -function autoScroll(element) { - requestAnimationFrame(() => { - element.scrollTo({ - top: element.scrollHeight, - behavior: 'smooth' - }); - }); - setTimeout(() => { - element.scrollTop = element.scrollHeight; - }, 100); -} - - -function getEventIcon(eventType) { - const icons = { - 'think': '๐Ÿค”', - 'tool': '๐Ÿ› ๏ธ', - 'act': '๐Ÿš€', - 'result': '๐Ÿ', - 'error': 'โŒ', - 'complete': 'โœ…', - 'log': '๐Ÿ“', - 'run': 'โš™๏ธ' - }; - return icons[eventType] || 'โ„น๏ธ'; -} - -function getEventLabel(eventType) { - const labels = { - 'think': 'Thinking', - 'tool': 'Using Tool', - 'act': 'Action', - 'result': 'Result', - 'error': 'Error', - 'complete': 'Complete', - 'log': 'Log', - 'run': 'Running' - }; - return labels[eventType] || 'Info'; -} - -function updateTaskStatus(task) { - const statusBar = document.getElementById('status-bar'); - if (!statusBar) return; - - if (task.status === 'completed') { - statusBar.innerHTML = `โœ… Task completed`; - } else if (task.status === 'failed') { - statusBar.innerHTML = `โŒ Task failed: ${task.error || 'Unknown error'}`; - } else { - statusBar.innerHTML = `โš™๏ธ Task running: ${task.status}`; - } -} - -document.addEventListener('DOMContentLoaded', () => { - loadHistory(); - - document.getElementById('prompt-input').addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - createTask(); - } - }); - - const historyToggle = document.getElementById('history-toggle'); - if (historyToggle) { - historyToggle.addEventListener('click', () => { - const historyPanel = document.getElementById('history-panel'); - if (historyPanel) { - historyPanel.classList.toggle('open'); - historyToggle.classList.toggle('active'); - } - }); - } - - const clearButton = document.getElementById('clear-btn'); - if (clearButton) { - clearButton.addEventListener('click', () => { - document.getElementById('prompt-input').value = ''; - document.getElementById('prompt-input').focus(); - }); - } -}); diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 5aaec28..0000000 --- a/static/style.css +++ /dev/null @@ -1,353 +0,0 @@ -:root { - --primary-color: #007bff; - --primary-hover: #0056b3; - --success-color: #28a745; - --error-color: #dc3545; - --warning-color: #ff9800; - --info-color: #2196f3; - --text-color: #333; - --text-light: #666; - --bg-color: #f8f9fa; - --border-color: #ddd; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; - margin: 0; - padding: 0; - background-color: var(--bg-color); - color: var(--text-color); -} - -.container { - display: flex; - min-height: 100vh; - min-width: 0; - width: 90%; - margin: 0 auto; - padding: 20px; - gap: 20px; -} - -.card { - background-color: white; - border-radius: 8px; - padding: 20px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -.history-panel { - width: 300px; -} - -.main-panel { - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; -} - -.task-list { - margin-top: 10px; - max-height: calc(100vh - 160px); - overflow-y: auto; - overflow-x: hidden; -} - -.task-container { - @extend .card; - width: 100%; - max-width: 100%; - position: relative; - min-height: 300px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - overflow: auto; - height: 100%; -} - -.welcome-message { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - color: var(--text-light); - background: white; - z-index: 1; - padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - width: 100%; - height: 100%; -} - -.welcome-message h1 { - font-size: 2rem; - margin-bottom: 10px; - color: var(--text-color); -} - -.input-container { - @extend .card; - display: flex; - gap: 10px; -} - -#prompt-input { - flex: 1; - padding: 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; -} - -button { - padding: 12px 24px; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.2s; -} - -button:hover { - background-color: var(--primary-hover); -} - -.task-item { - padding: 10px; - margin-bottom: 10px; - background-color: #f8f9fa; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s; -} - -.task-item:hover { - background-color: #e9ecef; -} - -.task-item.active { - background-color: var(--primary-color); - color: white; -} - -#input-container.bottom { - margin-top: auto; -} - -.task-card { - background: #fff; - padding: 15px; - margin-bottom: 10px; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s; -} - -.task-card:hover { - transform: translateX(5px); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -.status-pending { - color: var(--text-light); -} - -.status-running { - color: var(--primary-color); -} - -.status-completed { - color: var(--success-color); -} - -.status-failed { - color: var(--error-color); -} - -.step-container { - display: flex; - flex-direction: column; - gap: 10px; - padding: 15px; - width: 100%; - max-height: calc(100vh - 200px); - overflow-y: auto; - max-width: 100%; - overflow-x: hidden; -} - -.step-item { - padding: 15px; - background: white; - border-radius: 8px; - width: 100%; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); - margin-bottom: 10px; - opacity: 1; - transform: none; -} - -.step-item .log-line:not(.result) { - opacity: 0.7; - color: #666; - font-size: 0.9em; -} - -.step-item .log-line.result { - opacity: 1; - color: #333; - font-size: 1em; - background: #e8f5e9; - border-left: 4px solid #4caf50; - padding: 10px; - border-radius: 4px; -} - -.step-item.show { - opacity: 1; - transform: none; -} - -.log-line { - padding: 10px; - border-radius: 4px; - margin-bottom: 10px; - display: flex; - flex-direction: column; - gap: 4px; -} - -.log-line.think, -.step-item pre.think { - background: var(--info-color-light); - border-left: 4px solid var(--info-color); -} - -.log-line.tool, -.step-item pre.tool { - background: var(--warning-color-light); - border-left: 4px solid var(--warning-color); -} - -.log-line.result, -.step-item pre.result { - background: var(--success-color-light); - border-left: 4px solid var(--success-color); -} - -.log-line.error, -.step-item pre.error { - background: var(--error-color-light); - border-left: 4px solid var(--error-color); -} - -.log-line.info, -.step-item pre.info { - background: var(--bg-color); - border-left: 4px solid var(--text-light); -} - -.log-prefix { - font-weight: bold; - white-space: nowrap; - margin-bottom: 5px; - color: #666; -} - -.step-item pre { - padding: 10px; - border-radius: 4px; - margin: 10px 0; - overflow-x: hidden; - font-family: 'Courier New', monospace; - font-size: 0.9em; - line-height: 1.4; - white-space: pre-wrap; - word-wrap: break-word; - word-break: break-all; - max-width: 100%; - color: var(--text-color); - background: var(--bg-color); - - &.log { - background: var(--bg-color); - border-left: 4px solid var(--text-light); - } - &.think { - background: var(--info-color-light); - border-left: 4px solid var(--info-color); - } - &.tool { - background: var(--warning-color-light); - border-left: 4px solid var(--warning-color); - } - &.result { - background: var(--success-color-light); - border-left: 4px solid var(--success-color); - } -} - -.step-item strong { - display: block; - margin-bottom: 8px; - color: #007bff; - font-size: 0.9em; -} - -.step-item div { - color: #444; - line-height: 1.6; -} - -.loading { - padding: 15px; - color: #666; - text-align: center; -} - -.ping { - color: #ccc; - text-align: center; - margin: 5px 0; -} - -.error { - color: #dc3545; - padding: 10px; - background: #ffe6e6; - border-radius: 4px; - margin: 10px 0; -} - -.complete { - color: #28a745; - padding: 10px; - background: #e6ffe6; - border-radius: 4px; - margin: 10px 0; -} - -pre { - margin: 0; - white-space: pre-wrap; - word-break: break-word; -} - -.complete pre { - max-width: 100%; - white-space: pre-wrap; - word-break: break-word; -} diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index d3245d9..0000000 --- a/templates/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - OpenManus Local Version - - - -
-
-

History Tasks

-
-
- -
-
-
-

Welcome to OpenManus Local Version

-

Please enter a task prompt to start a new task

-
-
-
- -
- - -
-
-
- - - -