From b9312ac5b6210b1541b0c0b93fec785661986de3 Mon Sep 17 00:00:00 2001 From: vinci Date: Fri, 16 Jan 2026 15:32:30 +0800 Subject: [PATCH 1/2] simple webui --- webui/README.md | 176 + webui/backend/agent_runner.py | 431 ++ webui/backend/api.py | 335 ++ webui/backend/config_manager.py | 157 + webui/backend/main.py | 86 + webui/backend/project_discovery.py | 151 + webui/backend/session_manager.py | 129 + webui/backend/shared.py | 24 + webui/backend/websocket_handler.py | 304 ++ webui/config/mcp_servers.json | 3 + webui/config/settings.json | 12 + webui/frontend/package-lock.json | 3969 +++++++++++++++++ webui/frontend/package.json | 30 + webui/frontend/public/favicon.svg | 10 + webui/frontend/src/App.tsx | 55 + .../src/components/ConversationView.tsx | 1013 +++++ .../frontend/src/components/FileProgress.tsx | 58 + webui/frontend/src/components/Layout.tsx | 237 + webui/frontend/src/components/LogViewer.tsx | 219 + .../src/components/MessageContent.tsx | 190 + webui/frontend/src/components/SearchView.tsx | 359 ++ .../src/components/SettingsDialog.tsx | 438 ++ .../src/components/WorkflowProgress.tsx | 86 + webui/frontend/src/context/SessionContext.tsx | 414 ++ webui/frontend/src/context/ThemeContext.tsx | 281 ++ webui/frontend/src/main.tsx | 27 + webui/frontend/tsconfig.json | 21 + webui/frontend/tsconfig.node.json | 11 + webui/frontend/vite.config.ts | 23 + webui/requirements.txt | 3 + webui/start.sh | 159 + 31 files changed, 9411 insertions(+) create mode 100644 webui/README.md create mode 100644 webui/backend/agent_runner.py create mode 100644 webui/backend/api.py create mode 100644 webui/backend/config_manager.py create mode 100644 webui/backend/main.py create mode 100644 webui/backend/project_discovery.py create mode 100644 webui/backend/session_manager.py create mode 100644 webui/backend/shared.py create mode 100644 webui/backend/websocket_handler.py create mode 100644 webui/config/mcp_servers.json create mode 100644 webui/config/settings.json create mode 100644 webui/frontend/package-lock.json create mode 100644 webui/frontend/package.json create mode 100644 webui/frontend/public/favicon.svg create mode 100644 webui/frontend/src/App.tsx create mode 100644 webui/frontend/src/components/ConversationView.tsx create mode 100644 webui/frontend/src/components/FileProgress.tsx create mode 100644 webui/frontend/src/components/Layout.tsx create mode 100644 webui/frontend/src/components/LogViewer.tsx create mode 100644 webui/frontend/src/components/MessageContent.tsx create mode 100644 webui/frontend/src/components/SearchView.tsx create mode 100644 webui/frontend/src/components/SettingsDialog.tsx create mode 100644 webui/frontend/src/components/WorkflowProgress.tsx create mode 100644 webui/frontend/src/context/SessionContext.tsx create mode 100644 webui/frontend/src/context/ThemeContext.tsx create mode 100644 webui/frontend/src/main.tsx create mode 100644 webui/frontend/tsconfig.json create mode 100644 webui/frontend/tsconfig.node.json create mode 100644 webui/frontend/vite.config.ts create mode 100644 webui/requirements.txt create mode 100755 webui/start.sh diff --git a/webui/README.md b/webui/README.md new file mode 100644 index 000000000..9f8dccead --- /dev/null +++ b/webui/README.md @@ -0,0 +1,176 @@ +# MS-Agent WebUI Backend - 开发者指南 + +## 概述 + +MS-Agent WebUI后端负责管理前端与ms-agent框架之间的通信,通过WebSocket实现实时交互,支持多种项目类型的运行和监控。 + +## 架构设计 + +### 核心组件 + +``` +┌─────────────┐ WebSocket ┌──────────────────┐ +│ Frontend │ ◄────────────────────────► │ WebSocket Handler│ +└─────────────┘ └────────┬─────────┘ + │ + │ + ▼ +┌──────────────────┐ ┌──────────────────────┐ +│Project Discovery │ │ Agent Runner │ +└──────────────────┘ └──────────┬───────────┘ + │ + ▼ +┌──────────────────┐ ┌──────────────────────┐ +│ Session Manager │ │ ms-agent Process │ +└──────────────────┘ └──────────────────────┘ +``` + +### 文件职责 + +| 文件 | 职责 | +|------|--------| +| `main.py` | FastAPI应用入口,路由配置 | +| `api.py` | REST API端点定义 | +| `websocket_handler.py` | WebSocket连接管理和消息处理 | +| `agent_runner.py` | ms-agent进程管理和输出解析 | +| `project_discovery.py` | 项目发现和类型识别 | +| `session_manager.py` | 会话状态管理 | +| `config_manager.py` | 配置文件管理 | +| `shared.py` | 共享实例初始化 | + +## 请求处理流程 + +### 1. 项目发现阶段 + +**文件**: `project_discovery.py` + +系统启动时扫描项目目录,根据配置文件识别项目类型: + +```python +def _analyze_project(self, name: str, path: str): + # 检查配置文件确定项目类型 + workflow_file = os.path.join(path, 'workflow.yaml') + agent_file = os.path.join(path, 'agent.yaml') + run_file = os.path.join(path, 'run.py') + + if os.path.exists(workflow_file): + project_type = 'workflow' # 工作流项目 + config_file = workflow_file + elif os.path.exists(agent_file): + project_type = 'agent' # 代理项目 + config_file = agent_file + elif os.path.exists(run_file): + project_type = 'script' # 脚本项目 + config_file = run_file +``` + +**项目类型说明**: +- **workflow**: 使用`workflow.yaml`配置,通过ms-agent CLI运行 +- **agent**: 使用`agent.yaml`配置,通过ms-agent CLI运行 +- **script**: 使用`run.py`脚本,直接Python执行 + +### 2. WebSocket连接阶段 + +**文件**: `websocket_handler.py` + +前端通过WebSocket连接到后端: + +```python +@router.websocket("/session/{session_id}") +async def websocket_session(websocket: WebSocket, session_id: str): + await connection_manager.connect(websocket, session_id) + + try: + while True: + data = await websocket.receive_json() + await handle_session_message(session_id, data, websocket) + except WebSocketDisconnect: + connection_manager.disconnect(websocket, session_id) +``` + +**支持的消息类型**: +- `start`: 启动代理 +- `stop`: 停止代理 +- `send_input`: 向运行中的代理发送输入 +- `get_status`: 获取当前状态 + +### 3. 代理启动阶段 + +**文件**: `websocket_handler.py` + +处理启动请求,创建AgentRunner实例: + +```python +async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocket): + # 1. 获取会话信息 + session = session_manager.get_session(session_id) + + # 2. 获取项目信息 + project = project_discovery.get_project(session['project_id']) + + # 3. 创建AgentRunner + runner = AgentRunner( + session_id=session_id, + project=project, # 包含项目类型和配置文件路径 + config_manager=config_manager, + on_output=lambda msg: asyncio.create_task(on_agent_output(session_id, msg)), + on_log=lambda log: asyncio.create_task(on_agent_log(session_id, log)), + on_progress=lambda prog: asyncio.create_task(on_agent_progress(session_id, prog)), + on_complete=lambda result: asyncio.create_task(on_agent_complete(session_id, result)), + on_error=lambda err: asyncio.create_task(on_agent_error(session_id, err)) + ) + + # 4. 启动代理 + task = asyncio.create_task(runner.start(data.get('query', ''))) +``` + +### 4. 命令构建阶段 + +**文件**: `agent_runner.py` + +根据项目类型构建对应的ms-agent命令: + +```python +def _build_command(self, query: str) -> list: + project_type = self.project.get('type') + config_file = self.project.get('config_file', '') + + if project_type == 'workflow' or project_type == 'agent': + # workflow/agent类型:使用ms-agent CLI + cmd = [ + 'ms-agent', 'run', + '--config', config_file, # workflow.yaml 或 agent.yaml + '--trust_remote_code', 'true' + ] + + if query: + cmd.extend(['--query', query]) + + # 添加MCP服务器配置 + mcp_file = self.config_manager.get_mcp_file_path() + if os.path.exists(mcp_file): + cmd.extend(['--mcp_server_file', mcp_file]) + + # 添加LLM配置 + llm_config = self.config_manager.get_llm_config() + if llm_config.get('api_key'): + provider = llm_config.get('provider', 'modelscope') + if provider == 'modelscope': + cmd.extend(['--modelscope_api_key', llm_config['api_key']]) + elif provider == 'openai': + cmd.extend(['--openai_api_key', llm_config['api_key']]) + + elif project_type == 'script': + # script类型:直接运行Python脚本 + cmd = [python, self.project['config_file']] # run.py + + return cmd +``` + +## 不同项目类型的命令对应 + +| 项目类型 | 配置文件 | ms-agent命令 | +|---------|----------|--------------| +| **workflow** | `workflow.yaml` | `ms-agent run --config workflow.yaml --trust_remote_code true --query "xxx" --mcp_server_file xxx.json --modelscope_api_key xxx` | +| **agent** | `agent.yaml` | `ms-agent run --config agent.yaml --trust_remote_code true --query "xxx" --mcp_server_file xxx.json --modelscope_api_key xxx` | +| **script** | `run.py` | `python run.py` | \ No newline at end of file diff --git a/webui/backend/agent_runner.py b/webui/backend/agent_runner.py new file mode 100644 index 000000000..44c872f96 --- /dev/null +++ b/webui/backend/agent_runner.py @@ -0,0 +1,431 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +Agent runner for MS-Agent Web UI +Manages the execution of ms-agent through subprocess with log streaming. +""" +import os +import sys +import re +import asyncio +import subprocess +import signal +from typing import Dict, Any, Callable, Optional +from datetime import datetime + + +class AgentRunner: + """Runs ms-agent as a subprocess with output streaming""" + + def __init__( + self, + session_id: str, + project: Dict[str, Any], + config_manager, + on_output: Callable[[Dict[str, Any]], None] = None, + on_log: Callable[[Dict[str, Any]], None] = None, + on_progress: Callable[[Dict[str, Any]], None] = None, + on_complete: Callable[[Dict[str, Any]], None] = None, + on_error: Callable[[Dict[str, Any]], None] = None + ): + self.session_id = session_id + self.project = project + self.config_manager = config_manager + self.on_output = on_output + self.on_log = on_log + self.on_progress = on_progress + self.on_complete = on_complete + self.on_error = on_error + + self.process: Optional[asyncio.subprocess.Process] = None + self.is_running = False + self._accumulated_output = "" + self._current_step = None + self._workflow_steps = [] + self._stop_requested = False + + async def start(self, query: str): + """Start the agent""" + try: + self._stop_requested = False + self.is_running = True + + # Build command based on project type + cmd = self._build_command(query) + env = self._build_env() + + print(f"[Runner] Starting agent with command:") + print(f"[Runner] {' '.join(cmd)}") + print(f"[Runner] Working directory: {self.project['path']}") + + # Log the command + if self.on_log: + self.on_log({ + 'level': 'info', + 'message': f'Starting agent: {" ".join(cmd[:5])}...', + 'timestamp': datetime.now().isoformat() + }) + + # Start subprocess + self.process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + stdin=asyncio.subprocess.PIPE, + env=env, + cwd=self.project['path'], + start_new_session=True + ) + + print(f"[Runner] Process started with PID: {self.process.pid}") + + # Start output reader + await self._read_output() + + except Exception as e: + print(f"[Runner] ERROR: {e}") + import traceback + traceback.print_exc() + if self.on_error: + self.on_error({ + 'message': str(e), + 'type': 'startup_error' + }) + + async def stop(self): + """Stop the agent""" + self._stop_requested = True + self.is_running = False + if not self.process: + return + + try: + # If already exited, nothing to do + if self.process.returncode is not None: + return + + # Prefer terminating the whole process group to stop child processes too + try: + os.killpg(self.process.pid, signal.SIGTERM) + except Exception: + # Fallback to terminating only the parent + try: + self.process.terminate() + except Exception: + pass + + try: + await asyncio.wait_for(self.process.wait(), timeout=5.0) + except asyncio.TimeoutError: + try: + os.killpg(self.process.pid, signal.SIGKILL) + except Exception: + try: + self.process.kill() + except Exception: + pass + except Exception: + pass + + async def send_input(self, text: str): + """Send input to the agent""" + if self.process and self.process.stdin: + self.process.stdin.write((text + '\n').encode()) + await self.process.stdin.drain() + + def _build_command(self, query: str) -> list: + """Build the command to run the agent""" + project_type = self.project.get('type') + project_path = self.project['path'] + config_file = self.project.get('config_file', '') + + # Get python executable + python = sys.executable + + # Get MCP config file path + mcp_file = self.config_manager.get_mcp_file_path() + + if project_type == 'workflow' or project_type == 'agent': + # Use ms-agent CLI command (installed via entry point) + cmd = [ + 'ms-agent', 'run', + '--config', config_file, + '--trust_remote_code', 'true' + ] + + if query: + cmd.extend(['--query', query]) + + if os.path.exists(mcp_file): + cmd.extend(['--mcp_server_file', mcp_file]) + + # Add LLM config + llm_config = self.config_manager.get_llm_config() + if llm_config.get('api_key'): + provider = llm_config.get('provider', 'modelscope') + if provider == 'modelscope': + cmd.extend(['--modelscope_api_key', llm_config['api_key']]) + elif provider == 'openai': + cmd.extend(['--openai_api_key', llm_config['api_key']]) + + elif project_type == 'script': + # Run the script directly + cmd = [python, self.project['config_file']] + else: + cmd = [python, '-m', 'ms_agent', 'run', '--config', project_path] + + return cmd + + def _build_env(self) -> Dict[str, str]: + """Build environment variables""" + env = os.environ.copy() + + # Add config env vars + env.update(self.config_manager.get_env_vars()) + + # Set PYTHONUNBUFFERED for real-time output + env['PYTHONUNBUFFERED'] = '1' + + return env + + async def _read_output(self): + """Read and process output from the subprocess""" + print(f"[Runner] Starting to read output...") + try: + while self.is_running and self.process and self.process.stdout: + line = await self.process.stdout.readline() + if not line: + print(f"[Runner] No more output, breaking...") + break + + text = line.decode('utf-8', errors='replace').rstrip() + print(f"[Runner] Output: {text[:200]}" if len(text) > 200 else f"[Runner] Output: {text}") + await self._process_line(text) + + # Wait for process to complete + if self.process: + return_code = await self.process.wait() + print(f"[Runner] Process exited with code: {return_code}") + + # If stop was requested, do not report as completion/error + if self._stop_requested: + if self.on_log: + self.on_log({ + 'level': 'info', + 'message': 'Agent stopped by user', + 'timestamp': datetime.now().isoformat() + }) + return + + if return_code == 0: + if self.on_complete: + self.on_complete({ + 'status': 'success', + 'message': 'Agent completed successfully' + }) + else: + if self.on_error: + self.on_error({ + 'message': f'Agent exited with code {return_code}', + 'type': 'exit_error', + 'code': return_code + }) + + except Exception as e: + print(f"[Runner] Read error: {e}") + import traceback + traceback.print_exc() + if not self._stop_requested and self.on_error: + self.on_error({ + 'message': str(e), + 'type': 'read_error' + }) + finally: + self.is_running = False + print(f"[Runner] Finished reading output") + + async def _process_line(self, line: str): + """Process a line of output""" + # Log the line + if self.on_log: + log_level = self._detect_log_level(line) + await self.on_log({ + 'level': log_level, + 'message': line, + 'timestamp': datetime.now().isoformat() + }) + + # Parse for special patterns + await self._detect_patterns(line) + + def _detect_log_level(self, line: str) -> str: + """Detect log level from line""" + line_lower = line.lower() + if '[error' in line_lower or 'error:' in line_lower: + return 'error' + elif '[warn' in line_lower or 'warning:' in line_lower: + return 'warning' + elif '[debug' in line_lower: + return 'debug' + return 'info' + + async def _detect_patterns(self, line: str): + """Detect special patterns in output""" + # Detect workflow step beginning: "[tag] Agent tag task beginning." + begin_match = re.search(r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+beginning', line) + if begin_match: + step_name = begin_match.group(1) + + # Skip sub-steps (contain -r0-, -diversity-, etc.) + if '-r' in step_name and '-' in step_name.split('-r')[-1]: + print(f"[Runner] Skipping sub-step: {step_name}") + return + + print(f"[Runner] Detected step beginning: {step_name}") + + # If there's a previous step running, mark it as completed first + if self._current_step and self._current_step != step_name: + prev_step = self._current_step + print(f"[Runner] Auto-completing previous step: {prev_step}") + if self.on_output: + self.on_output({ + 'type': 'step_complete', + 'content': prev_step, + 'role': 'assistant', + 'metadata': {'step': prev_step, 'status': 'completed'} + }) + + self._current_step = step_name + if step_name not in self._workflow_steps: + self._workflow_steps.append(step_name) + + # Build step status - all previous steps completed, current running + step_status = {} + for i, s in enumerate(self._workflow_steps): + if s == step_name: + step_status[s] = 'running' + elif i < self._workflow_steps.index(step_name): + step_status[s] = 'completed' + else: + step_status[s] = 'pending' + + if self.on_progress: + self.on_progress({ + 'type': 'workflow', + 'current_step': step_name, + 'steps': self._workflow_steps.copy(), + 'step_status': step_status + }) + + # Send step start message + if self.on_output: + self.on_output({ + 'type': 'step_start', + 'content': step_name, + 'role': 'assistant', + 'metadata': {'step': step_name, 'status': 'running'} + }) + return + + # Detect workflow step finished: "[tag] Agent tag task finished." + end_match = re.search(r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+finished', line) + if end_match: + step_name = end_match.group(1) + + # Skip sub-steps + if '-r' in step_name and '-' in step_name.split('-r')[-1]: + return + + print(f"[Runner] Detected step finished: {step_name}") + + # Build step status dict - all steps up to current are completed + step_status = {} + for s in self._workflow_steps: + step_status[s] = 'completed' if self._workflow_steps.index(s) <= self._workflow_steps.index(step_name) else 'pending' + + if self.on_progress: + self.on_progress({ + 'type': 'workflow', + 'current_step': step_name, + 'steps': self._workflow_steps.copy(), + 'step_status': step_status + }) + + # Send step complete message + if self.on_output: + self.on_output({ + 'type': 'step_complete', + 'content': step_name, + 'role': 'assistant', + 'metadata': {'step': step_name, 'status': 'completed'} + }) + return + + # Detect assistant output: "[tag] [assistant]:" + if '[assistant]:' in line: + self._accumulated_output = "" + return + + # Detect tool calls: "[tag] [tool_calling]:" + if '[tool_calling]:' in line: + if self.on_output: + self.on_output({ + 'type': 'tool_call', + 'content': 'Calling tool...', + 'role': 'assistant' + }) + return + + # Detect file writing + file_match = re.search(r'writing file:?\s*["\']?([^\s"\']+)["\']?', line.lower()) + if not file_match: + file_match = re.search(r'creating file:?\s*["\']?([^\s"\']+)["\']?', line.lower()) + if file_match and self.on_progress: + filename = file_match.group(1) + self.on_progress({ + 'type': 'file', + 'file': filename, + 'status': 'writing' + }) + return + + # Detect file written/created/saved - multiple patterns + file_keywords = ['file created', 'file written', 'file saved', 'saved to:', 'wrote to', 'generated:', 'output:'] + if any(keyword in line.lower() for keyword in file_keywords): + # Try to extract filename with extension + file_match = re.search(r'["\']?([^\s"\'\[\]]+\.[a-zA-Z0-9]+)["\']?', line) + if file_match and self.on_progress: + filename = file_match.group(1) + print(f"[Runner] Detected file output: {filename}") + # Send as output file + if self.on_output: + self.on_output({ + 'type': 'file_output', + 'content': filename, + 'role': 'assistant', + 'metadata': {'filename': filename} + }) + self.on_progress({ + 'type': 'file', + 'file': filename, + 'status': 'completed' + }) + return + + # Detect output file paths (e.g., "output/user_story.txt" standalone) + output_path_match = re.search(r'(?:^|\s)((?:output|projects)/[^\s]+\.[a-zA-Z0-9]+)(?:\s|$)', line) + if output_path_match and self.on_progress: + filename = output_path_match.group(1) + print(f"[Runner] Detected output path: {filename}") + if self.on_output: + self.on_output({ + 'type': 'file_output', + 'content': filename, + 'role': 'assistant', + 'metadata': {'filename': filename} + }) + self.on_progress({ + 'type': 'file', + 'file': filename, + 'status': 'completed' + }) + return diff --git a/webui/backend/api.py b/webui/backend/api.py new file mode 100644 index 000000000..d8ee59164 --- /dev/null +++ b/webui/backend/api.py @@ -0,0 +1,335 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +API endpoints for the MS-Agent Web UI +""" +import os +import uuid +from typing import Dict, List, Optional, Any +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +# Import shared instances +from shared import project_discovery, config_manager, session_manager + +router = APIRouter() + + +# Request/Response Models +class ProjectInfo(BaseModel): + id: str + name: str + display_name: str + description: str + type: str # 'workflow' or 'agent' + path: str + has_readme: bool + + +class SessionCreate(BaseModel): + project_id: str + query: Optional[str] = None + + +class SessionInfo(BaseModel): + id: str + project_id: str + project_name: str + status: str + created_at: str + + +class LLMConfig(BaseModel): + provider: str = "openai" + model: str = "qwen3-coder-plus" + api_key: Optional[str] = None + base_url: Optional[str] = None + temperature: float = 0.7 + max_tokens: int = 4096 + + +class MCPServer(BaseModel): + name: str + type: str # 'stdio' or 'sse' + command: Optional[str] = None + args: Optional[List[str]] = None + url: Optional[str] = None + env: Optional[Dict[str, str]] = None + + +class GlobalConfig(BaseModel): + llm: LLMConfig + mcp_servers: Dict[str, Any] + theme: str = "dark" + output_dir: str = "./output" + + +# Project Endpoints +@router.get("/projects", response_model=List[ProjectInfo]) +async def list_projects(): + """List all available projects""" + return project_discovery.discover_projects() + + +@router.get("/projects/{project_id}") +async def get_project(project_id: str): + """Get detailed information about a specific project""" + project = project_discovery.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return project + + +@router.get("/projects/{project_id}/readme") +async def get_project_readme(project_id: str): + """Get the README content for a project""" + readme = project_discovery.get_project_readme(project_id) + if readme is None: + raise HTTPException(status_code=404, detail="README not found") + return {"content": readme} + + +# Session Endpoints +@router.post("/sessions", response_model=SessionInfo) +async def create_session(session_data: SessionCreate): + """Create a new session for a project""" + project = project_discovery.get_project(session_data.project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + session = session_manager.create_session( + project_id=session_data.project_id, + project_name=project['name'] + ) + return session + + +@router.get("/sessions", response_model=List[SessionInfo]) +async def list_sessions(): + """List all active sessions""" + return session_manager.list_sessions() + + +@router.get("/sessions/{session_id}") +async def get_session(session_id: str): + """Get session details""" + session = session_manager.get_session(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + return session + + +@router.delete("/sessions/{session_id}") +async def delete_session(session_id: str): + """Delete a session""" + success = session_manager.delete_session(session_id) + if not success: + raise HTTPException(status_code=404, detail="Session not found") + return {"status": "deleted"} + + +@router.get("/sessions/{session_id}/messages") +async def get_session_messages(session_id: str): + """Get all messages for a session""" + messages = session_manager.get_messages(session_id) + if messages is None: + raise HTTPException(status_code=404, detail="Session not found") + return {"messages": messages} + + +# Configuration Endpoints +@router.get("/config") +async def get_config(): + """Get global configuration""" + return config_manager.get_config() + + +@router.put("/config") +async def update_config(config: GlobalConfig): + """Update global configuration""" + config_manager.update_config(config.model_dump()) + return {"status": "updated"} + + +@router.get("/config/llm") +async def get_llm_config(): + """Get LLM configuration""" + return config_manager.get_llm_config() + + +@router.put("/config/llm") +async def update_llm_config(config: LLMConfig): + """Update LLM configuration""" + config_manager.update_llm_config(config.model_dump()) + return {"status": "updated"} + + +@router.get("/config/mcp") +async def get_mcp_config(): + """Get MCP servers configuration""" + return config_manager.get_mcp_config() + + +@router.put("/config/mcp") +async def update_mcp_config(servers: Dict[str, Any]): + """Update MCP servers configuration""" + config_manager.update_mcp_config(servers) + return {"status": "updated"} + + +@router.post("/config/mcp/servers") +async def add_mcp_server(server: MCPServer): + """Add a new MCP server""" + config_manager.add_mcp_server(server.name, server.model_dump(exclude={'name'})) + return {"status": "added"} + + +@router.delete("/config/mcp/servers/{server_name}") +async def remove_mcp_server(server_name: str): + """Remove an MCP server""" + success = config_manager.remove_mcp_server(server_name) + if not success: + raise HTTPException(status_code=404, detail="Server not found") + return {"status": "removed"} + + +# Available models endpoint +@router.get("/models") +async def list_available_models(): + """List available LLM models""" + return { + "models": [ + {"provider": "modelscope", "model": "Qwen/Qwen3-235B-A22B-Instruct-2507", "display_name": "Qwen3-235B (Recommended)"}, + {"provider": "modelscope", "model": "Qwen/Qwen2.5-72B-Instruct", "display_name": "Qwen2.5-72B"}, + {"provider": "modelscope", "model": "Qwen/Qwen2.5-32B-Instruct", "display_name": "Qwen2.5-32B"}, + {"provider": "modelscope", "model": "deepseek-ai/DeepSeek-V3", "display_name": "DeepSeek-V3"}, + {"provider": "openai", "model": "gpt-4o", "display_name": "GPT-4o"}, + {"provider": "openai", "model": "gpt-4o-mini", "display_name": "GPT-4o Mini"}, + {"provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "display_name": "Claude 3.5 Sonnet"}, + ] + } + + +# File content endpoint +class FileReadRequest(BaseModel): + path: str + session_id: Optional[str] = None + + +@router.get("/files/list") +async def list_output_files(): + """List all files in the output directory as a tree structure""" + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + output_dir = os.path.join(base_dir, 'ms-agent', 'output') + + # Folders to exclude + exclude_dirs = {'node_modules', '__pycache__', '.git', '.venv', 'venv', 'dist', 'build'} + + def build_tree(dir_path: str) -> dict: + """Recursively build a tree structure""" + result = {'folders': {}, 'files': []} + + if not os.path.exists(dir_path): + return result + + try: + items = os.listdir(dir_path) + except PermissionError: + return result + + for item in sorted(items): + # Skip hidden files/folders and excluded directories + if item.startswith('.') or item in exclude_dirs: + continue + + full_path = os.path.join(dir_path, item) + + if os.path.isdir(full_path): + # Recursively build subtree + subtree = build_tree(full_path) + # Only include folder if it has content + if subtree['folders'] or subtree['files']: + result['folders'][item] = subtree + else: + result['files'].append({ + 'name': item, + 'path': full_path, + 'size': os.path.getsize(full_path), + 'modified': os.path.getmtime(full_path) + }) + + # Sort files by modification time (newest first) + result['files'].sort(key=lambda x: x['modified'], reverse=True) + + return result + + tree = build_tree(output_dir) + return {'tree': tree, 'output_dir': output_dir} + + +@router.post("/files/read") +async def read_file_content(request: FileReadRequest): + """Read content of a generated file""" + file_path = request.path + + # Get base directories for security check + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + output_dir = os.path.join(base_dir, 'ms-agent', 'output') + projects_dir = os.path.join(base_dir, 'ms-agent', 'projects') + + # Resolve the file path + if not os.path.isabs(file_path): + # Try output dir first + full_path = os.path.join(output_dir, file_path) + if not os.path.exists(full_path): + # Try projects dir + full_path = os.path.join(projects_dir, file_path) + else: + full_path = file_path + + # Normalize path + full_path = os.path.normpath(full_path) + + # Security check: ensure file is within allowed directories + allowed_dirs = [output_dir, projects_dir] + is_allowed = any(full_path.startswith(os.path.normpath(d)) for d in allowed_dirs) + + if not is_allowed: + raise HTTPException(status_code=403, detail="Access denied: file outside allowed directories") + + if not os.path.exists(full_path): + raise HTTPException(status_code=404, detail=f"File not found: {file_path}") + + if not os.path.isfile(full_path): + raise HTTPException(status_code=400, detail="Path is not a file") + + # Check file size (limit to 1MB) + file_size = os.path.getsize(full_path) + if file_size > 1024 * 1024: + raise HTTPException(status_code=400, detail="File too large (max 1MB)") + + try: + with open(full_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Detect language from extension + ext = os.path.splitext(full_path)[1].lower() + lang_map = { + '.py': 'python', '.js': 'javascript', '.ts': 'typescript', + '.tsx': 'typescript', '.jsx': 'javascript', '.json': 'json', + '.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown', + '.html': 'html', '.css': 'css', '.txt': 'text', + '.sh': 'bash', '.java': 'java', '.go': 'go', '.rs': 'rust', + } + language = lang_map.get(ext, 'text') + + return { + "content": content, + "path": full_path, + "filename": os.path.basename(full_path), + "language": language, + "size": file_size + } + except UnicodeDecodeError: + raise HTTPException(status_code=400, detail="File is not a text file") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}") diff --git a/webui/backend/config_manager.py b/webui/backend/config_manager.py new file mode 100644 index 000000000..1e9a7718a --- /dev/null +++ b/webui/backend/config_manager.py @@ -0,0 +1,157 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +Configuration management for MS-Agent Web UI +Handles global settings, LLM configuration, and MCP server configuration. +""" +import os +import json +from typing import Dict, Any, Optional +from threading import Lock + + +class ConfigManager: + """Manages global configuration for the Web UI""" + + DEFAULT_CONFIG = { + "llm": { + "provider": "modelscope", + "model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "api_key": "", + "base_url": "https://api-inference.modelscope.cn/v1/", + "temperature": 0.7, + "max_tokens": 4096 + }, + "mcp_servers": {}, + "theme": "dark", + "output_dir": "./output" + } + + def __init__(self, config_dir: str): + self.config_dir = config_dir + self.config_file = os.path.join(config_dir, 'settings.json') + self.mcp_file = os.path.join(config_dir, 'mcp_servers.json') + self._lock = Lock() + self._config: Optional[Dict[str, Any]] = None + self._ensure_config_dir() + + def _ensure_config_dir(self): + """Ensure config directory exists""" + os.makedirs(self.config_dir, exist_ok=True) + + def _load_config(self) -> Dict[str, Any]: + """Load configuration from file""" + if self._config is not None: + return self._config + + if os.path.exists(self.config_file): + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + self._config = json.load(f) + except Exception: + self._config = self.DEFAULT_CONFIG.copy() + else: + self._config = self.DEFAULT_CONFIG.copy() + + # Load MCP servers from separate file if exists + if os.path.exists(self.mcp_file): + try: + with open(self.mcp_file, 'r', encoding='utf-8') as f: + mcp_data = json.load(f) + if 'mcpServers' in mcp_data: + self._config['mcp_servers'] = mcp_data['mcpServers'] + else: + self._config['mcp_servers'] = mcp_data + except Exception: + pass + + return self._config + + def _save_config(self): + """Save configuration to file""" + with self._lock: + # Save main config (without mcp_servers) + config_to_save = {k: v for k, v in self._config.items() if k != 'mcp_servers'} + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config_to_save, f, indent=2) + + # Save MCP servers to separate file (compatible with ms-agent format) + mcp_data = {"mcpServers": self._config.get('mcp_servers', {})} + with open(self.mcp_file, 'w', encoding='utf-8') as f: + json.dump(mcp_data, f, indent=2) + + def get_config(self) -> Dict[str, Any]: + """Get the full configuration""" + return self._load_config().copy() + + def update_config(self, config: Dict[str, Any]): + """Update the full configuration""" + self._load_config() + self._config.update(config) + self._save_config() + + def get_llm_config(self) -> Dict[str, Any]: + """Get LLM configuration""" + config = self._load_config() + return config.get('llm', self.DEFAULT_CONFIG['llm']) + + def update_llm_config(self, llm_config: Dict[str, Any]): + """Update LLM configuration""" + self._load_config() + self._config['llm'] = llm_config + self._save_config() + + def get_mcp_config(self) -> Dict[str, Any]: + """Get MCP servers configuration""" + config = self._load_config() + return {"mcpServers": config.get('mcp_servers', {})} + + def update_mcp_config(self, mcp_config: Dict[str, Any]): + """Update MCP servers configuration""" + self._load_config() + if 'mcpServers' in mcp_config: + self._config['mcp_servers'] = mcp_config['mcpServers'] + else: + self._config['mcp_servers'] = mcp_config + self._save_config() + + def add_mcp_server(self, name: str, server_config: Dict[str, Any]): + """Add a new MCP server""" + self._load_config() + if 'mcp_servers' not in self._config: + self._config['mcp_servers'] = {} + self._config['mcp_servers'][name] = server_config + self._save_config() + + def remove_mcp_server(self, name: str) -> bool: + """Remove an MCP server""" + self._load_config() + if name in self._config.get('mcp_servers', {}): + del self._config['mcp_servers'][name] + self._save_config() + return True + return False + + def get_mcp_file_path(self) -> str: + """Get the path to the MCP servers file""" + return self.mcp_file + + def get_env_vars(self) -> Dict[str, str]: + """Get environment variables for running agents""" + config = self._load_config() + llm = config.get('llm', {}) + + env_vars = {} + + if llm.get('api_key'): + provider = llm.get('provider', 'modelscope') + if provider == 'modelscope': + env_vars['MODELSCOPE_API_KEY'] = llm['api_key'] + elif provider == 'openai': + env_vars['OPENAI_API_KEY'] = llm['api_key'] + elif provider == 'anthropic': + env_vars['ANTHROPIC_API_KEY'] = llm['api_key'] + + if llm.get('base_url'): + env_vars['OPENAI_BASE_URL'] = llm['base_url'] + + return env_vars diff --git a/webui/backend/main.py b/webui/backend/main.py new file mode 100644 index 000000000..ab3bdec2d --- /dev/null +++ b/webui/backend/main.py @@ -0,0 +1,86 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +MS-Agent Web UI Backend Server +Provides REST API and WebSocket endpoints for the ms-agent framework. +""" +import os +import sys +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse + +from api import router as api_router +from websocket_handler import router as ws_router + +# Add ms-agent to path +MS_AGENT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'ms-agent')) +if MS_AGENT_PATH not in sys.path: + sys.path.insert(0, MS_AGENT_PATH) + +app = FastAPI( + title="MS-Agent Web UI", + description="Web interface for the MS-Agent framework", + version="1.0.0" +) + +# CORS configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include API and WebSocket routers +app.include_router(api_router, prefix="/api") +app.include_router(ws_router, prefix="/ws") + +# Serve static files in production +STATIC_DIR = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'dist') +if os.path.exists(STATIC_DIR): + app.mount("/assets", StaticFiles(directory=os.path.join(STATIC_DIR, "assets")), name="assets") + + @app.get("/{full_path:path}") + async def serve_spa(full_path: str): + """Serve the SPA for all non-API routes""" + file_path = os.path.join(STATIC_DIR, full_path) + if os.path.exists(file_path) and os.path.isfile(file_path): + return FileResponse(file_path) + return FileResponse(os.path.join(STATIC_DIR, "index.html")) + + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy", "service": "ms-agent-webui"} + + +def main(): + """Start the server""" + import argparse + parser = argparse.ArgumentParser(description="MS-Agent Web UI Server") + parser.add_argument("--host", default="0.0.0.0", help="Host to bind") + parser.add_argument("--port", type=int, default=7860, help="Port to bind") + parser.add_argument("--reload", action="store_true", help="Enable auto-reload") + args = parser.parse_args() + + print(f"\n{'='*60}") + print(" MS-Agent Web UI Server") + print(f"{'='*60}") + print(f" Server running at: http://{args.host}:{args.port}") + print(f" API documentation: http://{args.host}:{args.port}/docs") + print(f"{'='*60}\n") + + uvicorn.run( + "main:app", + host=args.host, + port=args.port, + reload=args.reload + ) + + +if __name__ == "__main__": + main() diff --git a/webui/backend/project_discovery.py b/webui/backend/project_discovery.py new file mode 100644 index 000000000..24d923906 --- /dev/null +++ b/webui/backend/project_discovery.py @@ -0,0 +1,151 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +Project discovery module for MS-Agent Web UI +Discovers and manages available projects from the ms-agent/projects directory. +""" +import os +import re +from typing import Dict, List, Optional, Any + + +class ProjectDiscovery: + """Discovers and manages projects from the ms-agent projects directory""" + + def __init__(self, projects_dir: str): + self.projects_dir = projects_dir + self._projects_cache: Optional[List[Dict[str, Any]]] = None + + def discover_projects(self, force_refresh: bool = False) -> List[Dict[str, Any]]: + """Discover all available projects""" + if self._projects_cache is not None and not force_refresh: + return self._projects_cache + + projects = [] + + if not os.path.exists(self.projects_dir): + return projects + + for item in os.listdir(self.projects_dir): + item_path = os.path.join(self.projects_dir, item) + if os.path.isdir(item_path) and not item.startswith('.'): + project_info = self._analyze_project(item, item_path) + if project_info: + projects.append(project_info) + + # Sort by display name + projects.sort(key=lambda x: x['display_name']) + self._projects_cache = projects + return projects + + def _analyze_project(self, name: str, path: str) -> Optional[Dict[str, Any]]: + """Analyze a project directory and extract its information""" + # Check for workflow.yaml or agent.yaml + workflow_file = os.path.join(path, 'workflow.yaml') + agent_file = os.path.join(path, 'agent.yaml') + run_file = os.path.join(path, 'run.py') + readme_file = os.path.join(path, 'README.md') + + # Determine project type + if os.path.exists(workflow_file): + project_type = 'workflow' + config_file = workflow_file + elif os.path.exists(agent_file): + project_type = 'agent' + config_file = agent_file + elif os.path.exists(run_file): + project_type = 'script' + config_file = run_file + else: + # Skip directories without valid config + return None + + # Generate display name from directory name + display_name = self._format_display_name(name) + + # Extract description from README if available + description = self._extract_description(readme_file) if os.path.exists(readme_file) else "" + + return { + 'id': name, + 'name': name, + 'display_name': display_name, + 'description': description, + 'type': project_type, + 'path': path, + 'has_readme': os.path.exists(readme_file), + 'config_file': config_file + } + + def _format_display_name(self, name: str) -> str: + """Convert directory name to display name""" + # Replace underscores with spaces and title case + display = name.replace('_', ' ').replace('-', ' ') + # Handle camelCase + display = re.sub(r'([a-z])([A-Z])', r'\1 \2', display) + return display.title() + + def _extract_description(self, readme_path: str) -> str: + """Extract first paragraph from README as description""" + try: + with open(readme_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Skip title and find first paragraph + lines = content.split('\n') + description_lines = [] + in_description = False + + for line in lines: + stripped = line.strip() + # Skip headers and empty lines at the beginning + if not in_description: + if stripped and not stripped.startswith('#') and not stripped.startswith('['): + in_description = True + description_lines.append(stripped) + else: + if stripped and not stripped.startswith('#'): + description_lines.append(stripped) + elif not stripped and description_lines: + break + + description = ' '.join(description_lines) + # Truncate if too long + if len(description) > 300: + description = description[:297] + '...' + return description + except Exception: + return "" + + def get_project(self, project_id: str) -> Optional[Dict[str, Any]]: + """Get a specific project by ID""" + projects = self.discover_projects() + for project in projects: + if project['id'] == project_id: + return project + return None + + def get_project_readme(self, project_id: str) -> Optional[str]: + """Get the README content for a project""" + project = self.get_project(project_id) + if not project or not project['has_readme']: + return None + + readme_path = os.path.join(project['path'], 'README.md') + try: + with open(readme_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception: + return None + + def get_project_config(self, project_id: str) -> Optional[Dict[str, Any]]: + """Get the configuration for a project""" + project = self.get_project(project_id) + if not project: + return None + + try: + import yaml + with open(project['config_file'], 'r', encoding='utf-8') as f: + return yaml.safe_load(f) + except Exception: + return None diff --git a/webui/backend/session_manager.py b/webui/backend/session_manager.py new file mode 100644 index 000000000..48ff4cc2a --- /dev/null +++ b/webui/backend/session_manager.py @@ -0,0 +1,129 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +Session management for MS-Agent Web UI +Handles session lifecycle and message history. +""" +import uuid +from datetime import datetime +from typing import Dict, List, Any, Optional +from threading import Lock + + +class SessionManager: + """Manages user sessions and their message history""" + + def __init__(self): + self._sessions: Dict[str, Dict[str, Any]] = {} + self._messages: Dict[str, List[Dict[str, Any]]] = {} + self._lock = Lock() + + def create_session(self, project_id: str, project_name: str) -> Dict[str, Any]: + """Create a new session""" + session_id = str(uuid.uuid4()) + session = { + 'id': session_id, + 'project_id': project_id, + 'project_name': project_name, + 'status': 'idle', # idle, running, completed, error + 'created_at': datetime.now().isoformat(), + 'workflow_progress': None, + 'file_progress': None, + 'current_step': None + } + + with self._lock: + self._sessions[session_id] = session + self._messages[session_id] = [] + + return session + + def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: + """Get session by ID""" + return self._sessions.get(session_id) + + def update_session(self, session_id: str, updates: Dict[str, Any]) -> bool: + """Update session data""" + if session_id not in self._sessions: + return False + + with self._lock: + self._sessions[session_id].update(updates) + return True + + def delete_session(self, session_id: str) -> bool: + """Delete a session""" + with self._lock: + if session_id in self._sessions: + del self._sessions[session_id] + if session_id in self._messages: + del self._messages[session_id] + return True + return False + + def list_sessions(self) -> List[Dict[str, Any]]: + """List all sessions""" + return list(self._sessions.values()) + + def add_message(self, session_id: str, role: str, content: str, + message_type: str = 'text', metadata: Dict[str, Any] = None) -> bool: + """Add a message to a session""" + if session_id not in self._sessions: + return False + + message = { + 'id': str(uuid.uuid4()), + 'role': role, # user, assistant, system, tool + 'content': content, + 'type': message_type, # text, tool_call, tool_result, error, log + 'timestamp': datetime.now().isoformat(), + 'metadata': metadata or {} + } + + with self._lock: + if session_id not in self._messages: + self._messages[session_id] = [] + self._messages[session_id].append(message) + + return True + + def get_messages(self, session_id: str) -> Optional[List[Dict[str, Any]]]: + """Get all messages for a session""" + if session_id not in self._sessions: + return None + return self._messages.get(session_id, []) + + def update_last_message(self, session_id: str, content: str) -> bool: + """Update the content of the last message (for streaming)""" + if session_id not in self._messages or not self._messages[session_id]: + return False + + with self._lock: + self._messages[session_id][-1]['content'] = content + return True + + def set_workflow_progress(self, session_id: str, progress: Dict[str, Any]) -> bool: + """Set workflow progress for a session""" + if session_id not in self._sessions: + return False + + with self._lock: + self._sessions[session_id]['workflow_progress'] = progress + return True + + def set_file_progress(self, session_id: str, progress: Dict[str, Any]) -> bool: + """Set file writing progress for a session""" + if session_id not in self._sessions: + return False + + with self._lock: + self._sessions[session_id]['file_progress'] = progress + return True + + def set_current_step(self, session_id: str, step: str) -> bool: + """Set the current workflow step""" + if session_id not in self._sessions: + return False + + with self._lock: + self._sessions[session_id]['current_step'] = step + return True diff --git a/webui/backend/shared.py b/webui/backend/shared.py new file mode 100644 index 000000000..cc109a544 --- /dev/null +++ b/webui/backend/shared.py @@ -0,0 +1,24 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +Shared instances for backend modules. +Ensures api.py and websocket_handler.py use the same manager instances. +""" +import os + +from session_manager import SessionManager +from project_discovery import ProjectDiscovery +from config_manager import ConfigManager + +# Initialize paths +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +PROJECTS_DIR = os.path.join(BASE_DIR, 'projects') +CONFIG_DIR = os.path.join(BASE_DIR, 'webui', 'config') + +# Shared instances +project_discovery = ProjectDiscovery(PROJECTS_DIR) +config_manager = ConfigManager(CONFIG_DIR) +session_manager = SessionManager() + +print(f"[Shared] Initialized managers") +print(f"[Shared] Projects dir: {PROJECTS_DIR}") +print(f"[Shared] Config dir: {CONFIG_DIR}") diff --git a/webui/backend/websocket_handler.py b/webui/backend/websocket_handler.py new file mode 100644 index 000000000..d363aa30e --- /dev/null +++ b/webui/backend/websocket_handler.py @@ -0,0 +1,304 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +""" +WebSocket handler for real-time communication +Handles agent execution, log streaming, and progress updates. +""" +import os +import json +import asyncio +from typing import Dict, Set, Any +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +from agent_runner import AgentRunner +# Import shared instances +from shared import project_discovery, config_manager, session_manager + +router = APIRouter() + + +class ConnectionManager: + """Manages WebSocket connections""" + + def __init__(self): + self.active_connections: Dict[str, Set[WebSocket]] = {} + self.log_connections: Set[WebSocket] = set() + + async def connect(self, websocket: WebSocket, session_id: str): + """Connect a client to a session""" + await websocket.accept() + if session_id not in self.active_connections: + self.active_connections[session_id] = set() + self.active_connections[session_id].add(websocket) + + async def connect_logs(self, websocket: WebSocket): + """Connect a client to log stream""" + await websocket.accept() + self.log_connections.add(websocket) + + def disconnect(self, websocket: WebSocket, session_id: str = None): + """Disconnect a client""" + if session_id and session_id in self.active_connections: + self.active_connections[session_id].discard(websocket) + if not self.active_connections[session_id]: + del self.active_connections[session_id] + self.log_connections.discard(websocket) + + async def send_to_session(self, session_id: str, message: Dict[str, Any]): + """Send message to all clients in a session""" + if session_id in self.active_connections: + disconnected = set() + for connection in self.active_connections[session_id]: + try: + await connection.send_json(message) + except Exception: + disconnected.add(connection) + for conn in disconnected: + self.active_connections[session_id].discard(conn) + + async def broadcast_log(self, log_entry: Dict[str, Any]): + """Broadcast log entry to all log connections""" + disconnected = set() + for connection in self.log_connections: + try: + await connection.send_json(log_entry) + except Exception: + disconnected.add(connection) + for conn in disconnected: + self.log_connections.discard(conn) + + +connection_manager = ConnectionManager() +agent_runners: Dict[str, AgentRunner] = {} +agent_tasks: Dict[str, asyncio.Task] = {} + + +@router.websocket("/session/{session_id}") +async def websocket_session(websocket: WebSocket, session_id: str): + """WebSocket endpoint for session communication""" + print(f"[WS] Client connecting to session: {session_id}") + await connection_manager.connect(websocket, session_id) + print(f"[WS] Client connected to session: {session_id}") + + try: + while True: + data = await websocket.receive_json() + print(f"[WS] Received message: {data}") + await handle_session_message(session_id, data, websocket) + except WebSocketDisconnect: + print(f"[WS] Client disconnected from session: {session_id}") + connection_manager.disconnect(websocket, session_id) + # Stop agent if running + if session_id in agent_runners: + await agent_runners[session_id].stop() + del agent_runners[session_id] + if session_id in agent_tasks: + agent_tasks[session_id].cancel() + del agent_tasks[session_id] + + +@router.websocket("/logs") +async def websocket_logs(websocket: WebSocket): + """WebSocket endpoint for log streaming""" + await connection_manager.connect_logs(websocket) + + try: + while True: + # Keep connection alive + await websocket.receive_text() + except WebSocketDisconnect: + connection_manager.disconnect(websocket) + + +async def handle_session_message(session_id: str, data: Dict[str, Any], websocket: WebSocket): + """Handle incoming WebSocket messages""" + action = data.get('action') + + if action == 'start': + await start_agent(session_id, data, websocket) + elif action == 'stop': + await stop_agent(session_id) + elif action == 'send_input': + await send_input(session_id, data) + elif action == 'get_status': + await send_status(session_id, websocket) + + +async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocket): + """Start an agent for a session""" + print(f"[Agent] Starting agent for session: {session_id}") + + session = session_manager.get_session(session_id) + if not session: + print(f"[Agent] ERROR: Session not found: {session_id}") + await websocket.send_json({ + 'type': 'error', + 'message': 'Session not found' + }) + return + + project = project_discovery.get_project(session['project_id']) + if not project: + print(f"[Agent] ERROR: Project not found: {session['project_id']}") + await websocket.send_json({ + 'type': 'error', + 'message': 'Project not found' + }) + return + + print(f"[Agent] Project: {project['id']}, type: {project['type']}, config: {project['config_file']}") + + query = data.get('query', '') + print(f"[Agent] Query: {query[:100]}..." if len(query) > 100 else f"[Agent] Query: {query}") + + # Add user message to session (but don't broadcast - frontend already has it) + session_manager.add_message(session_id, 'user', query, 'text') + + # Create agent runner + runner = AgentRunner( + session_id=session_id, + project=project, + config_manager=config_manager, + on_output=lambda msg: asyncio.create_task(on_agent_output(session_id, msg)), + on_log=lambda log: asyncio.create_task(on_agent_log(session_id, log)), + on_progress=lambda prog: asyncio.create_task(on_agent_progress(session_id, prog)), + on_complete=lambda result: asyncio.create_task(on_agent_complete(session_id, result)), + on_error=lambda err: asyncio.create_task(on_agent_error(session_id, err)) + ) + + agent_runners[session_id] = runner + session_manager.update_session(session_id, {'status': 'running'}) + + # Notify session started + await connection_manager.send_to_session(session_id, { + 'type': 'status', + 'status': 'running' + }) + + # Start agent in background so the WS loop can still receive stop/input messages + task = asyncio.create_task(runner.start(query)) + agent_tasks[session_id] = task + + def _cleanup(_task: asyncio.Task): + agent_tasks.pop(session_id, None) + + task.add_done_callback(_cleanup) + + +async def stop_agent(session_id: str): + """Stop a running agent""" + if session_id in agent_runners: + await agent_runners[session_id].stop() + del agent_runners[session_id] + if session_id in agent_tasks: + agent_tasks[session_id].cancel() + del agent_tasks[session_id] + + session_manager.update_session(session_id, {'status': 'stopped'}) + await connection_manager.send_to_session(session_id, { + 'type': 'status', + 'status': 'stopped' + }) + + +async def send_input(session_id: str, data: Dict[str, Any]): + """Send input to a running agent""" + if session_id in agent_runners: + await agent_runners[session_id].send_input(data.get('input', '')) + + +async def send_status(session_id: str, websocket: WebSocket): + """Send current status to a client""" + session = session_manager.get_session(session_id) + if session: + await websocket.send_json({ + 'type': 'status', + 'session': session, + 'messages': session_manager.get_messages(session_id) + }) + + +async def on_agent_output(session_id: str, message: Dict[str, Any]): + """Handle agent output""" + msg_type = message.get('type', 'text') + content = message.get('content', '') + role = message.get('role', 'assistant') + + if msg_type == 'stream': + # Streaming update + await connection_manager.send_to_session(session_id, { + 'type': 'stream', + 'content': content, + 'done': message.get('done', False) + }) + if message.get('done'): + session_manager.add_message(session_id, role, content, 'text') + else: + session_manager.add_message(session_id, role, content, msg_type, message.get('metadata')) + await connection_manager.send_to_session(session_id, { + 'type': 'message', + 'role': role, + 'content': content, + 'message_type': msg_type, + 'metadata': message.get('metadata') + }) + + +async def on_agent_log(session_id: str, log: Dict[str, Any]): + """Handle agent log""" + await connection_manager.send_to_session(session_id, { + 'type': 'log', + **log + }) + await connection_manager.broadcast_log({ + 'session_id': session_id, + **log + }) + + +async def on_agent_progress(session_id: str, progress: Dict[str, Any]): + """Handle progress update""" + progress_type = progress.get('type', 'workflow') + + if progress_type == 'workflow': + session_manager.set_workflow_progress(session_id, progress) + session_manager.set_current_step(session_id, progress.get('current_step')) + elif progress_type == 'file': + session_manager.set_file_progress(session_id, progress) + + await connection_manager.send_to_session(session_id, { + 'type': 'progress', + **progress + }) + + +async def on_agent_complete(session_id: str, result: Dict[str, Any]): + """Handle agent completion""" + session_manager.update_session(session_id, {'status': 'completed'}) + + if session_id in agent_runners: + del agent_runners[session_id] + if session_id in agent_tasks: + agent_tasks[session_id].cancel() + del agent_tasks[session_id] + + await connection_manager.send_to_session(session_id, { + 'type': 'complete', + 'result': result + }) + + +async def on_agent_error(session_id: str, error: Dict[str, Any]): + """Handle agent error""" + session_manager.update_session(session_id, {'status': 'error'}) + session_manager.add_message(session_id, 'system', error.get('message', 'Unknown error'), 'error') + + if session_id in agent_runners: + del agent_runners[session_id] + if session_id in agent_tasks: + agent_tasks[session_id].cancel() + del agent_tasks[session_id] + + await connection_manager.send_to_session(session_id, { + 'type': 'error', + **error + }) diff --git a/webui/config/mcp_servers.json b/webui/config/mcp_servers.json new file mode 100644 index 000000000..700113020 --- /dev/null +++ b/webui/config/mcp_servers.json @@ -0,0 +1,3 @@ +{ + "mcpServers": {} +} \ No newline at end of file diff --git a/webui/config/settings.json b/webui/config/settings.json new file mode 100644 index 000000000..ebd5155b5 --- /dev/null +++ b/webui/config/settings.json @@ -0,0 +1,12 @@ +{ + "llm": { + "provider": "openai", + "model": "qwen3-coder-plus", + "api_key": "", + "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "temperature": 0.7, + "max_tokens": 32768 + }, + "theme": "dark", + "output_dir": "./output" +} \ No newline at end of file diff --git a/webui/frontend/package-lock.json b/webui/frontend/package-lock.json new file mode 100644 index 000000000..14c664a38 --- /dev/null +++ b/webui/frontend/package-lock.json @@ -0,0 +1,3969 @@ +{ + "name": "ms-agent-webui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ms-agent-webui", + "version": "1.0.0", + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.6", + "@mui/material": "^5.15.6", + "framer-motion": "^11.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@types/react-syntax-highlighter": "^15.5.11", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmmirror.com/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmmirror.com/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmmirror.com/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.18.0", + "resolved": "https://registry.npmmirror.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz", + "integrity": "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.18.0", + "resolved": "https://registry.npmmirror.com/@mui/icons-material/-/icons-material-5.18.0.tgz", + "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.18.0", + "resolved": "https://registry.npmmirror.com/@mui/material/-/material-5.18.0.tgz", + "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.18.0", + "@mui/system": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmmirror.com/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.18.0", + "resolved": "https://registry.npmmirror.com/@mui/styled-engine/-/styled-engine-5.18.0.tgz", + "integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.18.0", + "resolved": "https://registry.npmmirror.com/@mui/system/-/system-5.18.0.tgz", + "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmmirror.com/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmmirror.com/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmmirror.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmmirror.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.13", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz", + "integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001763", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.6", + "resolved": "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmmirror.com/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/webui/frontend/package.json b/webui/frontend/package.json new file mode 100644 index 000000000..8a88cb6bb --- /dev/null +++ b/webui/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "ms-agent-webui", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.6", + "@mui/material": "^5.15.6", + "framer-motion": "^11.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@types/react-syntax-highlighter": "^15.5.11", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } +} diff --git a/webui/frontend/public/favicon.svg b/webui/frontend/public/favicon.svg new file mode 100644 index 000000000..be490a284 --- /dev/null +++ b/webui/frontend/public/favicon.svg @@ -0,0 +1,10 @@ + + + MS + + + + + + + diff --git a/webui/frontend/src/App.tsx b/webui/frontend/src/App.tsx new file mode 100644 index 000000000..6d57fde85 --- /dev/null +++ b/webui/frontend/src/App.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { Box } from '@mui/material'; +import { AnimatePresence } from 'framer-motion'; +import { useSession } from './context/SessionContext'; +import SearchView from './components/SearchView'; +import ConversationView from './components/ConversationView'; +import Layout from './components/Layout'; + +const App: React.FC = () => { + const { currentSession } = useSession(); + const [showSettings, setShowSettings] = useState(false); + const [showLogs, setShowLogs] = useState(false); + + return ( + setShowSettings(true)} + onToggleLogs={() => setShowLogs(!showLogs)} + showLogs={showLogs} + > + + + {!currentSession ? ( + + ) : ( + + )} + + + + {showSettings && ( + + setShowSettings(false)} + /> + + )} + + ); +}; + +// Lazy load settings dialog +const SettingsDialogLazy = React.lazy(() => import('./components/SettingsDialog')); + +export default App; diff --git a/webui/frontend/src/components/ConversationView.tsx b/webui/frontend/src/components/ConversationView.tsx new file mode 100644 index 000000000..6c75e8598 --- /dev/null +++ b/webui/frontend/src/components/ConversationView.tsx @@ -0,0 +1,1013 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { + Box, + TextField, + IconButton, + Typography, + Paper, + InputAdornment, + useTheme, + alpha, + Chip, + Divider, + Avatar, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + CircularProgress, +} from '@mui/material'; +import { + Send as SendIcon, + Stop as StopIcon, + Person as PersonIcon, + AutoAwesome as BotIcon, + PlayArrow as RunningIcon, + InsertDriveFile as FileIcon, + Code as CodeIcon, + Description as DocIcon, + Image as ImageIcon, + CheckCircle as CompleteIcon, + HourglassTop as StartIcon, + Close as CloseIcon, + ContentCopy as CopyIcon, + Folder as FolderIcon, + FolderOpen as FolderOpenIcon, + ChevronRight as ChevronRightIcon, + ExpandMore as ExpandMoreIcon, +} from '@mui/icons-material'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useSession, Message, Session } from '../context/SessionContext'; +import WorkflowProgress from './WorkflowProgress'; +import FileProgress from './FileProgress'; +import LogViewer from './LogViewer'; +import MessageContent from './MessageContent'; + +interface ConversationViewProps { + showLogs: boolean; +} + +const ConversationView: React.FC = ({ showLogs }) => { + const theme = useTheme(); + const { + currentSession, + messages, + streamingContent, + isStreaming, + isLoading, + sendMessage, + stopAgent, + logs, + } = useSession(); + + const completedSteps = React.useMemo(() => { + const set = new Set(); + for (const m of messages) { + if (m.type === 'step_complete' && m.content) set.add(m.content); + } + return set; + }, [messages]); + + const [input, setInput] = useState(''); + const [outputFilesOpen, setOutputFilesOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [outputTree, setOutputTree] = useState({folders: {}, files: []}); + const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [selectedFile, setSelectedFile] = useState(null); + const [fileContent, setFileContent] = useState(null); + const [fileLoading, setFileLoading] = useState(false); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // Auto-scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, streamingContent]); + + // Focus input on mount + useEffect(() => { + inputRef.current?.focus(); + }, []); + + const handleSend = useCallback(() => { + if (!input.trim() || isLoading) return; + sendMessage(input); + setInput(''); + }, [input, isLoading, sendMessage]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const loadOutputFiles = async () => { + try { + const response = await fetch('/api/files/list'); + if (response.ok) { + const data = await response.json(); + setOutputTree(data.tree || {folders: {}, files: []}); + // Expand root level by default + setExpandedFolders(new Set([''])); + } + } catch (err) { + console.error('Failed to load output files:', err); + } + }; + + const toggleFolder = (folder: string) => { + setExpandedFolders(prev => { + const next = new Set(prev); + if (next.has(folder)) { + next.delete(folder); + } else { + next.add(folder); + } + return next; + }); + }; + + const handleOpenOutputFiles = () => { + loadOutputFiles(); + setOutputFilesOpen(true); + setSelectedFile(null); + setFileContent(null); + }; + + const handleViewFile = async (path: string) => { + setSelectedFile(path); + setFileLoading(true); + try { + const response = await fetch('/api/files/read', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + if (response.ok) { + const data = await response.json(); + setFileContent(data.content); + } + } catch (err) { + console.error('Failed to load file:', err); + } finally { + setFileLoading(false); + } + }; + + return ( + + {/* Session Header */} + + + + : undefined} + label={currentSession?.status} + size="small" + color={ + currentSession?.status === 'running' ? 'info' : + currentSession?.status === 'completed' ? 'success' : + currentSession?.status === 'error' ? 'error' : 'default' + } + sx={{ + textTransform: 'capitalize', + borderRadius: '8px', + '& .MuiChip-icon': { ml: 0.5 }, + }} + /> + + + {/* Workflow Progress */} + {currentSession?.workflow_progress && ( + + + + )} + + {/* File Progress */} + {currentSession?.file_progress && ( + + )} + + {/* View Output Files Button */} + + } + label="Output Files" + size="small" + onClick={handleOpenOutputFiles} + sx={{ + backgroundColor: alpha(theme.palette.warning.main, 0.1), + color: theme.palette.warning.main, + cursor: 'pointer', + '&:hover': { + backgroundColor: alpha(theme.palette.warning.main, 0.2), + }, + }} + /> + + + + {/* Output Files Dialog */} + setOutputFilesOpen(false)} + maxWidth="md" + fullWidth + PaperProps={{ + sx: { backgroundColor: theme.palette.background.paper, minHeight: '60vh' } + }} + > + + + + Output Files + + setOutputFilesOpen(false)}> + + + + + {/* File Tree */} + + + + {/* File Content */} + + {fileLoading ? ( + + + + ) : fileContent ? ( + + {fileContent} + + ) : ( + + Select a file to view + + )} + + + + + {/* Main Content Area */} + + {/* Messages Area */} + + {/* Messages List */} + + + {messages.map((message) => ( + + ))} + + {/* Streaming Content */} + {isStreaming && streamingContent && ( + + + + )} + + {/* Loading Indicator */} + {isLoading && !isStreaming && messages.length > 0 && (() => { + // Find current running step + const runningSteps = messages.filter(m => m.type === 'step_start'); + const completedSteps = messages.filter(m => m.type === 'step_complete'); + const currentStep = runningSteps.length > completedSteps.length + ? runningSteps[runningSteps.length - 1]?.content?.replace(/_/g, ' ') + : null; + + return ( + + + + + + + + + {[0, 1, 2].map((i) => ( + + + + ))} + + + {currentStep ? ( + <> + + {currentStep} + + in progress... + + ) : 'Processing...'} + + + + + + ); + })()} + +
+ + + {/* Input Area */} + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: '12px', + backgroundColor: theme.palette.background.paper, + }, + }} + InputProps={{ + endAdornment: ( + + {isLoading ? ( + + + + ) : ( + + + + )} + + ), + }} + /> + + + + {/* Logs Panel */} + {showLogs && ( + <> + + + + )} + + + ); +}; + +interface MessageBubbleProps { + message: Message; + isStreaming?: boolean; + sessionStatus?: Session['status']; + completedSteps?: Set; +} + +const MessageBubble: React.FC = ({ message, isStreaming, sessionStatus, completedSteps }) => { + const theme = useTheme(); + const isUser = message.role === 'user'; + const isError = message.type === 'error'; + const isSystem = message.role === 'system'; + const isFileOutput = message.type === 'file_output'; + const isStepStart = message.type === 'step_start'; + const isStepComplete = message.type === 'step_complete'; + const isToolCall = message.type === 'tool_call'; + + // Skip empty messages + if (!message.content?.trim()) return null; + + // Skip old format system messages + if (isSystem && message.content.startsWith('Starting step:')) return null; + if (isSystem && message.content.startsWith('Completed step:')) return null; + + // Step start/complete display + if (isStepStart || isStepComplete) { + // If a step has a completion record, hide the earlier start record to avoid duplicates. + if (isStepStart && completedSteps?.has(message.content)) { + return null; + } + + const stepName = message.content.replace(/_/g, ' '); + const isComplete = isStepComplete || (isStepStart && !!completedSteps?.has(message.content)); + const isStopped = isStepStart && !isComplete && sessionStatus === 'stopped'; + const accentColor = isComplete + ? theme.palette.success.main + : isStopped + ? theme.palette.warning.main + : theme.palette.info.main; + + return ( + + + + {isComplete ? : } + + + + {stepName} + + + {isComplete ? 'Completed' : isStopped ? 'Stopped' : 'Running...'} + + + + + ); + } + + // Tool call - skip display (we show step progress instead) + if (isToolCall) { + return null; + } + + // File output display as compact chip + if (isFileOutput) { + return ; + } + + return ( + + + {/* Avatar */} + + + {isUser ? : } + + + + {/* Message Content */} + + + + {isStreaming && ( + + )} + + + + ); +}; + +export default ConversationView; + +// Recursive FileTreeView component +interface TreeNode { + folders: Record; + files: Array<{name: string; path: string; size: number; modified: number}>; +} + +interface FileTreeViewProps { + tree: TreeNode; + path: string; + expandedFolders: Set; + toggleFolder: (path: string) => void; + selectedFile: string | null; + onSelectFile: (path: string) => void; + depth?: number; +} + +const FileTreeView: React.FC = ({ + tree, path, expandedFolders, toggleFolder, selectedFile, onSelectFile, depth = 0 +}) => { + const theme = useTheme(); + const stripProgrammerPrefix = (name: string) => { + if (!name.startsWith('programmer-')) return name; + const stripped = name.slice('programmer-'.length); + return stripped.length > 0 ? stripped : name; + }; + const hasContent = Object.keys(tree.folders).length > 0 || tree.files.length > 0; + + if (!hasContent && depth === 0) { + return ( + + No files yet + + ); + } + + return ( + <> + {/* Folders */} + {Object.entries(tree.folders).map(([folderName, subtree]) => { + const folderPath = path ? `${path}/${folderName}` : folderName; + const isExpanded = expandedFolders.has(folderPath); + + return ( + + toggleFolder(folderPath)} + sx={{ + py: 0.5, + pl: depth * 2 + 1, + pr: 1, + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: 0.5, + '&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.05) }, + }} + > + {isExpanded ? ( + + ) : ( + + )} + {isExpanded ? ( + + ) : ( + + )} + + {folderName} + + + {isExpanded && ( + + )} + + ); + })} + + {/* Files */} + {tree.files.map((file) => ( + onSelectFile(file.path)} + sx={{ + py: 0.5, + pl: depth * 2 + 3.5, + pr: 1, + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: 0.5, + backgroundColor: selectedFile === file.path ? alpha(theme.palette.primary.main, 0.1) : 'transparent', + '&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.05) }, + }} + > + + + {stripProgrammerPrefix(file.name)} + + + ))} + + ); +}; + +// Separate component for file output with dialog +const FileOutputChip: React.FC<{ filename: string }> = ({ filename }) => { + const theme = useTheme(); + const shortName = filename.split('/').pop() || filename; + const displayName = shortName.startsWith('programmer-') ? shortName.slice('programmer-'.length) : shortName; + const [dialogOpen, setDialogOpen] = useState(false); + const [fileContent, setFileContent] = useState(null); + const [fileLoading, setFileLoading] = useState(false); + const [fileError, setFileError] = useState(null); + const [fileLang, setFileLang] = useState('text'); + + const getFileIcon = (fname: string) => { + const ext = fname.split('.').pop()?.toLowerCase(); + if (['js', 'ts', 'tsx', 'jsx', 'py', 'java', 'cpp', 'c', 'go', 'rs'].includes(ext || '')) { + return ; + } + if (['md', 'txt', 'json', 'yaml', 'yml', 'xml', 'html', 'css'].includes(ext || '')) { + return ; + } + if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext || '')) { + return ; + } + return ; + }; + + const handleViewFile = async () => { + setDialogOpen(true); + setFileLoading(true); + setFileError(null); + + try { + const response = await fetch('/api/files/read', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: filename }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Failed to load file'); + } + + const data = await response.json(); + setFileContent(data.content); + setFileLang(data.language || 'text'); + } catch (err) { + setFileError(err instanceof Error ? err.message : 'Failed to load file'); + } finally { + setFileLoading(false); + } + }; + + const handleCopy = () => { + if (fileContent) { + navigator.clipboard.writeText(fileContent); + } + }; + + return ( + <> + + + + + Click to view + + + + + {/* File Viewer Dialog */} + setDialogOpen(false)} + maxWidth="md" + fullWidth + PaperProps={{ + sx: { + backgroundColor: theme.palette.background.paper, + backgroundImage: 'none', + } + }} + > + + + {getFileIcon(filename)} + + {shortName} + + + + + + + + + + setDialogOpen(false)}> + + + + + + {fileLoading ? ( + + + + ) : fileError ? ( + + {fileError} + + ) : ( + + {fileContent} + + )} + + + + ); +}; diff --git a/webui/frontend/src/components/FileProgress.tsx b/webui/frontend/src/components/FileProgress.tsx new file mode 100644 index 000000000..fb70edf77 --- /dev/null +++ b/webui/frontend/src/components/FileProgress.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Box, Typography, Chip, useTheme, alpha, CircularProgress } from '@mui/material'; +import { CheckCircle as CheckIcon } from '@mui/icons-material'; +import { motion } from 'framer-motion'; +import { FileProgress as FileProgressType } from '../context/SessionContext'; + +interface FileProgressProps { + progress: FileProgressType; +} + +const FileProgress: React.FC = ({ progress }) => { + const theme = useTheme(); + const { file, status } = progress; + const isWriting = status === 'writing'; + + // Extract filename from path + const filename = file.split('/').pop() || file; + + return ( + + + + File: + + : } + label={filename} + sx={{ + height: 24, + fontSize: '0.7rem', + maxWidth: 200, + backgroundColor: isWriting + ? alpha(theme.palette.warning.main, 0.1) + : alpha(theme.palette.success.main, 0.1), + color: isWriting + ? theme.palette.warning.main + : theme.palette.success.main, + '& .MuiChip-icon': { + color: 'inherit', + }, + '& .MuiChip-label': { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + }} + /> + + + ); +}; + +export default FileProgress; diff --git a/webui/frontend/src/components/Layout.tsx b/webui/frontend/src/components/Layout.tsx new file mode 100644 index 000000000..d2370a32c --- /dev/null +++ b/webui/frontend/src/components/Layout.tsx @@ -0,0 +1,237 @@ +import React, { ReactNode } from 'react'; +import { + Box, + AppBar, + Toolbar, + Typography, + IconButton, + Tooltip, + useTheme, + alpha, +} from '@mui/material'; +import { + Settings as SettingsIcon, + DarkMode as DarkModeIcon, + LightMode as LightModeIcon, + Terminal as TerminalIcon, + GitHub as GitHubIcon, +} from '@mui/icons-material'; +import { motion } from 'framer-motion'; +import { useThemeContext } from '../context/ThemeContext'; + +interface LayoutProps { + children: ReactNode; + onOpenSettings: () => void; + onToggleLogs: () => void; + showLogs: boolean; +} + +const Layout: React.FC = ({ children, onOpenSettings, onToggleLogs, showLogs }) => { + const theme = useTheme(); + const { mode, toggleTheme } = useThemeContext(); + + return ( + + {/* Header */} + + + {/* Logo */} + + + + + MS + + + + + MS-Agent + + + Intelligent Platform + + + + + + {/* Actions */} + + + + + + + + + + + {mode === 'dark' ? : } + + + + + + + + + + + + + + + + + + + + {/* Main Content */} + + {children} + + + {/* Footer */} + + + Powered by ModelScope + + + + © 2024 Alibaba Inc. + + + + ); +}; + +export default Layout; diff --git a/webui/frontend/src/components/LogViewer.tsx b/webui/frontend/src/components/LogViewer.tsx new file mode 100644 index 000000000..d4746e3a3 --- /dev/null +++ b/webui/frontend/src/components/LogViewer.tsx @@ -0,0 +1,219 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { + Box, + Typography, + IconButton, + TextField, + InputAdornment, + useTheme, + alpha, + Chip, + Tooltip, +} from '@mui/material'; +import { + Clear as ClearIcon, + Search as SearchIcon, + Download as DownloadIcon, +} from '@mui/icons-material'; +import { motion, AnimatePresence } from 'framer-motion'; +import { LogEntry } from '../context/SessionContext'; + +interface LogViewerProps { + logs: LogEntry[]; + onClear?: () => void; +} + +const LogViewer: React.FC = ({ logs, onClear }) => { + const theme = useTheme(); + const logsEndRef = useRef(null); + const [filter, setFilter] = useState(''); + const [levelFilter, setLevelFilter] = useState(null); + + // Auto-scroll to bottom + useEffect(() => { + logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [logs]); + + const filteredLogs = logs.filter((log) => { + const matchesSearch = !filter || log.message.toLowerCase().includes(filter.toLowerCase()); + const matchesLevel = !levelFilter || log.level === levelFilter; + return matchesSearch && matchesLevel; + }); + + const getLevelColor = (level: LogEntry['level']) => { + switch (level) { + case 'error': return theme.palette.error.main; + case 'warning': return theme.palette.warning.main; + case 'debug': return theme.palette.info.main; + default: return theme.palette.text.secondary; + } + }; + + const handleDownload = () => { + const content = logs.map((log) => + `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}` + ).join('\n'); + + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `ms-agent-logs-${new Date().toISOString().slice(0, 10)}.txt`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( + + {/* Header */} + + + Logs + + + + + + + + + {onClear && ( + + + + + + )} + + + {/* Filters */} + + setFilter(e.target.value)} + sx={{ mb: 1 }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + {['info', 'warning', 'error', 'debug'].map((level) => ( + setLevelFilter(levelFilter === level ? null : level)} + sx={{ + height: 22, + fontSize: '0.65rem', + textTransform: 'uppercase', + backgroundColor: levelFilter === level + ? alpha(getLevelColor(level as LogEntry['level']), 0.2) + : 'transparent', + border: `1px solid ${alpha(getLevelColor(level as LogEntry['level']), 0.3)}`, + color: getLevelColor(level as LogEntry['level']), + }} + /> + ))} + + + + {/* Log List */} + + + {filteredLogs.map((log, index) => ( + + + + + [{log.level}] + + + {log.message} + + + + {new Date(log.timestamp).toLocaleTimeString()} + + + + ))} + +
+ + + ); +}; + +export default LogViewer; diff --git a/webui/frontend/src/components/MessageContent.tsx b/webui/frontend/src/components/MessageContent.tsx new file mode 100644 index 000000000..05ee5553f --- /dev/null +++ b/webui/frontend/src/components/MessageContent.tsx @@ -0,0 +1,190 @@ +import React, { useMemo } from 'react'; +import { Box, Typography, useTheme } from '@mui/material'; +import ReactMarkdown from 'react-markdown'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +interface MessageContentProps { + content: string; +} + +const MessageContent: React.FC = ({ content }) => { + const theme = useTheme(); + const isDark = theme.palette.mode === 'dark'; + + const components = useMemo(() => ({ + code({ node, inline, className, children, ...props }: any) { + const match = /language-(\w+)/.exec(className || ''); + const language = match ? match[1] : ''; + + if (!inline && language) { + return ( + + + + {language} + + + + {String(children).replace(/\n$/, '')} + + + ); + } + + return ( + + {children} + + ); + }, + p({ children }: any) { + return ( + + {children} + + ); + }, + ul({ children }: any) { + return ( + + {children} + + ); + }, + ol({ children }: any) { + return ( + + {children} + + ); + }, + h1({ children }: any) { + return ( + + {children} + + ); + }, + h2({ children }: any) { + return ( + + {children} + + ); + }, + h3({ children }: any) { + return ( + + {children} + + ); + }, + blockquote({ children }: any) { + return ( + + {children} + + ); + }, + a({ href, children }: any) { + return ( + + {children} + + ); + }, + }), [isDark, theme]); + + return ( + *:first-child': { mt: 0 }, + '& > *:last-child': { mb: 0 }, + }} + > + + {content} + + + ); +}; + +export default MessageContent; diff --git a/webui/frontend/src/components/SearchView.tsx b/webui/frontend/src/components/SearchView.tsx new file mode 100644 index 000000000..5f611cc53 --- /dev/null +++ b/webui/frontend/src/components/SearchView.tsx @@ -0,0 +1,359 @@ +import React, { useState, useCallback } from 'react'; +import { + Box, + TextField, + Typography, + Card, + CardContent, + Chip, + InputAdornment, + IconButton, + useTheme, + alpha, + Grid, + Tooltip, +} from '@mui/material'; +import { + Search as SearchIcon, + ArrowForward as ArrowForwardIcon, + Code as CodeIcon, + Psychology as PsychologyIcon, + Science as ScienceIcon, + Description as DescriptionIcon, + Movie as MovieIcon, + AccountTree as WorkflowIcon, +} from '@mui/icons-material'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useSession, Project } from '../context/SessionContext'; + +const projectIcons: Record = { + code_genesis: , + agent_skills: , + deep_research: , + doc_research: , + fin_research: , + singularity_cinema: , +}; + +const SearchView: React.FC = () => { + const theme = useTheme(); + const { projects, createSession, selectSession } = useSession(); + const [query, setQuery] = useState(''); + const [selectedProject, setSelectedProject] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleProjectSelect = (project: Project) => { + setSelectedProject(project); + }; + + const handleSubmit = useCallback(async () => { + if (!selectedProject || !query.trim()) return; + + console.log('[SearchView] Submitting with project:', selectedProject.id, 'query:', query); + setIsSubmitting(true); + try { + const session = await createSession(selectedProject.id); + console.log('[SearchView] Session created:', session); + if (session) { + // Pass the session object directly to avoid race condition + selectSession(session.id, query, session); + } + } catch (error) { + console.error('[SearchView] Error creating session:', error); + } finally { + setIsSubmitting(false); + } + }, [selectedProject, query, createSession, selectSession]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( + + {/* Hero Section */} + + + Intelligent Agent Platform + + + Harness the power of AI agents for research, coding, and creative tasks + + + + {/* Search Input */} + + + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + disabled={!selectedProject} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: '16px', + backgroundColor: alpha(theme.palette.background.paper, 0.8), + backdropFilter: 'blur(10px)', + border: `2px solid ${alpha(theme.palette.primary.main, selectedProject ? 0.3 : 0.1)}`, + transition: 'all 0.3s ease', + '&:hover': { + border: `2px solid ${alpha(theme.palette.primary.main, 0.5)}`, + }, + '&.Mui-focused': { + border: `2px solid ${theme.palette.primary.main}`, + boxShadow: `0 0 20px ${alpha(theme.palette.primary.main, 0.15)}`, + }, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + + + + + + + + + ), + }} + /> + + {/* Selected Project Badge */} + + {selectedProject && ( + + + } + label={selectedProject.display_name} + onDelete={() => setSelectedProject(null)} + sx={{ + backgroundColor: alpha(theme.palette.primary.main, 0.15), + borderColor: theme.palette.primary.main, + '& .MuiChip-icon': { + color: theme.palette.primary.main, + }, + }} + variant="outlined" + /> + + + )} + + + + + {/* Project Cards */} + + + Select a Project + + + + {projects.map((project, index) => ( + + + handleProjectSelect(project)} + sx={{ + cursor: 'pointer', + position: 'relative', + overflow: 'hidden', + border: selectedProject?.id === project.id + ? `2px solid ${theme.palette.primary.main}` + : `1px solid ${theme.palette.divider}`, + transition: 'all 0.3s ease', + '&:hover': { + transform: 'translateY(-4px)', + boxShadow: `0 12px 24px ${alpha(theme.palette.common.black, 0.15)}`, + border: `2px solid ${alpha(theme.palette.primary.main, 0.5)}`, + }, + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: 3, + background: selectedProject?.id === project.id + ? `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.primary.light})` + : 'transparent', + }, + }} + > + + + + {projectIcons[project.id] || } + + + + {project.display_name} + + + + + + + + {project.description || 'No description available'} + + + + + + + + ))} + + + + ); +}; + +export default SearchView; diff --git a/webui/frontend/src/components/SettingsDialog.tsx b/webui/frontend/src/components/SettingsDialog.tsx new file mode 100644 index 000000000..ad4a8b747 --- /dev/null +++ b/webui/frontend/src/components/SettingsDialog.tsx @@ -0,0 +1,438 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Box, + Typography, + Tabs, + Tab, + Select, + MenuItem, + FormControl, + InputLabel, + Slider, + IconButton, + Divider, + Paper, + useTheme, + Alert, + Chip, + Tooltip, +} from '@mui/material'; +import { + Close as CloseIcon, + Add as AddIcon, + Delete as DeleteIcon, + Save as SaveIcon, +} from '@mui/icons-material'; + +interface SettingsDialogProps { + open: boolean; + onClose: () => void; +} + +interface LLMConfig { + provider: string; + model: string; + api_key: string; + base_url: string; + temperature: number; + max_tokens: number; +} + +interface MCPServer { + type: 'stdio' | 'sse'; + command?: string; + args?: string[]; + url?: string; + env?: Record; +} + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +const TabPanel: React.FC = ({ children, value, index }) => ( + +); + +const SettingsDialog: React.FC = ({ open, onClose }) => { + const theme = useTheme(); + const [tabValue, setTabValue] = useState(0); + const [llmConfig, setLlmConfig] = useState({ + provider: 'modelscope', + model: 'Qwen/Qwen3-235B-A22B-Instruct-2507', + api_key: '', + base_url: 'https://api-inference.modelscope.cn/v1/', + temperature: 0.7, + max_tokens: 4096, + }); + const [mcpServers, setMcpServers] = useState>({}); + const [newServerName, setNewServerName] = useState(''); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); + + // Load config on mount + useEffect(() => { + if (open) { + loadConfig(); + } + }, [open]); + + const loadConfig = async () => { + try { + const [llmRes, mcpRes] = await Promise.all([ + fetch('/api/config/llm'), + fetch('/api/config/mcp'), + ]); + + if (llmRes.ok) { + const data = await llmRes.json(); + setLlmConfig(data); + } + + if (mcpRes.ok) { + const data = await mcpRes.json(); + setMcpServers(data.mcpServers || {}); + } + } catch (error) { + console.error('Failed to load config:', error); + } + }; + + const handleSave = async () => { + setSaveStatus('saving'); + try { + const llmRes = await fetch('/api/config/llm', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(llmConfig), + }); + + const mcpRes = await fetch('/api/config/mcp', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mcpServers: mcpServers }), + }); + + if (llmRes.ok && mcpRes.ok) { + setSaveStatus('saved'); + setTimeout(() => setSaveStatus('idle'), 2000); + } else { + setSaveStatus('error'); + } + } catch (error) { + setSaveStatus('error'); + } + }; + + const handleAddMCPServer = () => { + if (!newServerName.trim()) return; + + setMcpServers((prev) => ({ + ...prev, + [newServerName]: { type: 'sse', url: '' }, + })); + setNewServerName(''); + }; + + const handleRemoveMCPServer = (name: string) => { + setMcpServers((prev) => { + const newServers = { ...prev }; + delete newServers[name]; + return newServers; + }); + }; + + const handleMCPServerChange = (name: string, field: keyof MCPServer, value: any) => { + setMcpServers((prev) => ({ + ...prev, + [name]: { ...prev[name], [field]: value }, + })); + }; + + const providers = [ + { value: 'modelscope', label: 'ModelScope', baseUrl: 'https://api-inference.modelscope.cn/v1/' }, + { value: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1/' }, + { value: 'anthropic', label: 'Anthropic', baseUrl: 'https://api.anthropic.com/v1/' }, + { value: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1/' }, + { value: 'custom', label: 'Custom', baseUrl: '' }, + ]; + + const models: Record = { + modelscope: ['Qwen/Qwen3-235B-A22B-Instruct-2507', 'Qwen/Qwen2.5-72B-Instruct', 'Qwen/Qwen2.5-32B-Instruct'], + openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'], + anthropic: ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229'], + deepseek: ['deepseek-chat', 'deepseek-coder'], + custom: [], + }; + + return ( + + + Settings + + + + + + + + + setTabValue(v)} + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + + + + + {/* LLM Configuration Tab */} + + + + Provider + + + + + Model + + + + {llmConfig.provider === 'custom' && ( + setLlmConfig((prev) => ({ ...prev, model: e.target.value }))} + /> + )} + + setLlmConfig((prev) => ({ ...prev, api_key: e.target.value }))} + helperText={ + llmConfig.provider === 'modelscope' + ? 'Get your API key from https://modelscope.cn/my/myaccesstoken' + : undefined + } + /> + + setLlmConfig((prev) => ({ ...prev, base_url: e.target.value }))} + /> + + + Temperature: {llmConfig.temperature} + setLlmConfig((prev) => ({ ...prev, temperature: v as number }))} + min={0} + max={2} + step={0.1} + marks={[ + { value: 0, label: '0' }, + { value: 1, label: '1' }, + { value: 2, label: '2' }, + ]} + /> + + + setLlmConfig((prev) => ({ ...prev, max_tokens: parseInt(e.target.value) || 4096 }))} + /> + + + + {/* MCP Servers Tab */} + + + + Configure MCP (Model Context Protocol) servers to extend agent capabilities with additional tools. + + + {/* Add new server */} + + setNewServerName(e.target.value)} + sx={{ flex: 1 }} + /> + + + + {/* Server list */} + {Object.entries(mcpServers).map(([name, server]) => ( + + + + {name} + + + handleRemoveMCPServer(name)} + > + + + + + + + + Type + + + + {server.type === 'sse' ? ( + handleMCPServerChange(name, 'url', e.target.value)} + /> + ) : ( + <> + handleMCPServerChange(name, 'command', e.target.value)} + /> + handleMCPServerChange(name, 'args', e.target.value.split(',').map((s) => s.trim()))} + /> + + )} + + + ))} + + {Object.keys(mcpServers).length === 0 && ( + + No MCP servers configured + + Add a server above to get started + + + )} + + + + + + + + {saveStatus === 'saved' && ( + + )} + {saveStatus === 'error' && ( + + )} + + + + + + ); +}; + +export default SettingsDialog; diff --git a/webui/frontend/src/components/WorkflowProgress.tsx b/webui/frontend/src/components/WorkflowProgress.tsx new file mode 100644 index 000000000..84f705518 --- /dev/null +++ b/webui/frontend/src/components/WorkflowProgress.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Box, Typography, Chip, useTheme, alpha, LinearProgress } from '@mui/material'; +import { CheckCircle as CheckIcon, RadioButtonUnchecked as PendingIcon } from '@mui/icons-material'; +import { motion } from 'framer-motion'; +import { WorkflowProgress as WorkflowProgressType } from '../context/SessionContext'; + +interface WorkflowProgressProps { + progress: WorkflowProgressType; +} + +const WorkflowProgress: React.FC = ({ progress }) => { + const theme = useTheme(); + const { steps, step_status } = progress; + + // Calculate progress based on completed steps + const completedCount = steps.filter(s => step_status[s] === 'completed').length; + const runningCount = steps.filter(s => step_status[s] === 'running').length; + const progressPercent = steps.length > 0 ? ((completedCount + runningCount * 0.5) / steps.length) * 100 : 0; + + return ( + + + Workflow: + + + + {steps.map((step, index) => { + const status = step_status[step] || 'pending'; + const isCompleted = status === 'completed'; + const isCurrent = status === 'running'; + + return ( + + : isCurrent ? undefined : } + label={step.replace(/_/g, ' ')} + sx={{ + height: 24, + fontSize: '0.7rem', + backgroundColor: isCompleted + ? alpha(theme.palette.success.main, 0.1) + : isCurrent + ? alpha(theme.palette.info.main, 0.15) + : alpha(theme.palette.action.disabled, 0.1), + color: isCompleted + ? theme.palette.success.main + : isCurrent + ? theme.palette.info.main + : theme.palette.text.secondary, + border: isCurrent ? `1px solid ${theme.palette.info.main}` : 'none', + '& .MuiChip-icon': { + color: 'inherit', + }, + }} + /> + + ); + })} + + + + + + + ); +}; + +export default WorkflowProgress; diff --git a/webui/frontend/src/context/SessionContext.tsx b/webui/frontend/src/context/SessionContext.tsx new file mode 100644 index 000000000..153210a18 --- /dev/null +++ b/webui/frontend/src/context/SessionContext.tsx @@ -0,0 +1,414 @@ +import React, { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from 'react'; + +export interface Message { + id: string; + role: 'user' | 'assistant' | 'system' | 'tool'; + content: string; + type: 'text' | 'tool_call' | 'tool_result' | 'error' | 'log' | 'file_output' | 'step_start' | 'step_complete'; + timestamp: string; + metadata?: Record; +} + +export interface Project { + id: string; + name: string; + display_name: string; + description: string; + type: 'workflow' | 'agent' | 'script'; + path: string; + has_readme: boolean; +} + +export interface WorkflowProgress { + current_step: string; + steps: string[]; + step_status: Record; +} + +export interface FileProgress { + file: string; + status: 'writing' | 'completed'; +} + +export interface Session { + id: string; + project_id: string; + project_name: string; + status: 'idle' | 'running' | 'completed' | 'error' | 'stopped'; + created_at: string; + workflow_progress?: WorkflowProgress; + file_progress?: FileProgress; + current_step?: string; +} + +export interface LogEntry { + level: 'info' | 'warning' | 'error' | 'debug'; + message: string; + timestamp: string; + session_id?: string; +} + +interface SessionContextType { + projects: Project[]; + sessions: Session[]; + currentSession: Session | null; + messages: Message[]; + logs: LogEntry[]; + streamingContent: string; + isStreaming: boolean; + isLoading: boolean; + loadProjects: () => Promise; + createSession: (projectId: string) => Promise; + selectSession: (sessionId: string, initialQuery?: string, sessionObj?: Session) => void; + sendMessage: (content: string) => void; + stopAgent: () => void; + clearLogs: () => void; +} + +const SessionContext = createContext(undefined); + +const API_BASE = '/api'; +const WS_BASE = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`; + +const PROJECT_DESCRIPTION_OVERRIDES: Record = { + deep_research: + 'This project provides a framework for deep research, enabling agents to autonomously explore and execute complex tasks.', + code_genesis: + 'This project provides a code generation workflow that helps agents plan, scaffold, and refine software projects end-to-end.', + agent_skills: + 'This project provides a collection of reusable agent skills and tools to automate tasks and extend agent capabilities.', + doc_research: + 'This project provides a document research workflow for ingesting, searching, and summarizing documents with agent assistance.', + fin_research: + 'This project provides a financial research workflow that combines data analysis and information gathering to produce structured reports.', + singularity_cinema: + 'This project provides a creative workflow for generating stories, scripts, and media ideas with agent collaboration.', +}; + +export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [projects, setProjects] = useState([]); + const [sessions, setSessions] = useState([]); + const [currentSession, setCurrentSession] = useState(null); + const [messages, setMessages] = useState([]); + const [logs, setLogs] = useState([]); + const [streamingContent, setStreamingContent] = useState(''); + const [isStreaming, setIsStreaming] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [ws, setWs] = useState(null); + const pendingQueryRef = useRef(null); + + // Load projects + const loadProjects = useCallback(async () => { + try { + const response = await fetch(`${API_BASE}/projects`); + if (response.ok) { + const data = await response.json(); + const projectsWithOverrides: Project[] = (Array.isArray(data) ? data : []).map((project: Project) => { + const overrideDescription = project?.id ? PROJECT_DESCRIPTION_OVERRIDES[project.id] : undefined; + if (overrideDescription) { + return { ...project, description: overrideDescription }; + } + return project; + }); + setProjects(projectsWithOverrides); + } + } catch (error) { + console.error('Failed to load projects:', error); + } + }, []); + + // Create session + const createSession = useCallback(async (projectId: string): Promise => { + try { + const response = await fetch(`${API_BASE}/sessions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ project_id: projectId }), + }); + + if (response.ok) { + const session = await response.json(); + setSessions(prev => [...prev, session]); + return session; + } + } catch (error) { + console.error('Failed to create session:', error); + } + return null; + }, []); + + // Connect WebSocket for session + const connectWebSocket = useCallback((sessionId: string, initialQuery?: string) => { + if (ws) { + ws.close(); + } + + // Store pending query to send after connection + if (initialQuery) { + pendingQueryRef.current = initialQuery; + } + + const socket = new WebSocket(`${WS_BASE}/session/${sessionId}`); + + socket.onopen = () => { + console.log('WebSocket connected'); + // Send pending query if exists + if (pendingQueryRef.current && socket.readyState === WebSocket.OPEN) { + const query = pendingQueryRef.current; + pendingQueryRef.current = null; + + // Add user message locally + setMessages(prev => [...prev, { + id: Date.now().toString(), + role: 'user', + content: query, + type: 'text', + timestamp: new Date().toISOString(), + }]); + + socket.send(JSON.stringify({ + action: 'start', + query: query, + })); + + setIsLoading(true); + } + }; + + socket.onmessage = (event) => { + const data = JSON.parse(event.data); + handleWebSocketMessage(data); + }; + + socket.onclose = () => { + console.log('WebSocket disconnected'); + }; + + socket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + setWs(socket); + }, [ws]); + + // Handle WebSocket messages + const handleWebSocketMessage = useCallback((data: Record) => { + const type = data.type as string; + + switch (type) { + case 'message': + setMessages(prev => [...prev, { + id: Date.now().toString(), + role: data.role as Message['role'], + content: data.content as string, + type: (data.message_type as Message['type']) || 'text', + timestamp: new Date().toISOString(), + metadata: data.metadata as Record, + }]); + break; + + case 'stream': + setStreamingContent(data.content as string); + setIsStreaming(!data.done); + if (data.done) { + setMessages(prev => [...prev, { + id: Date.now().toString(), + role: 'assistant', + content: data.content as string, + type: 'text', + timestamp: new Date().toISOString(), + }]); + setStreamingContent(''); + } + break; + + case 'log': + setLogs(prev => [...prev, { + level: data.level as LogEntry['level'], + message: data.message as string, + timestamp: data.timestamp as string, + session_id: currentSession?.id, + }]); + break; + + case 'progress': + setCurrentSession(prev => { + if (!prev) return prev; + + const progressType = data.type as string; + if (progressType === 'workflow') { + return { + ...prev, + workflow_progress: { + current_step: data.current_step as string, + steps: data.steps as string[], + step_status: data.step_status as WorkflowProgress['step_status'], + }, + current_step: data.current_step as string, + }; + } else if (progressType === 'file') { + return { + ...prev, + file_progress: { + file: data.file as string, + status: data.status as FileProgress['status'], + }, + }; + } + return prev; + }); + break; + + case 'status': + { + const nextStatus = (data.status as Session['status'] | undefined) ?? ((data as any)?.session?.status as Session['status'] | undefined); + if (nextStatus) { + setCurrentSession(prev => { + if (!prev) return prev; + if (nextStatus !== 'running') { + return { ...prev, status: nextStatus, workflow_progress: undefined, file_progress: undefined, current_step: undefined }; + } + return { ...prev, status: nextStatus }; + }); + setSessions(prev => prev.map(s => (s.id === currentSession?.id ? { ...s, status: nextStatus } : s))); + setIsLoading(nextStatus === 'running'); + if (nextStatus !== 'running') { + setIsStreaming(false); + setStreamingContent(''); + } + } + } + break; + + case 'complete': + setCurrentSession(prev => { + if (!prev) return prev; + return { ...prev, status: 'completed' }; + }); + setSessions(prev => prev.map(s => (s.id === currentSession?.id ? { ...s, status: 'completed' } : s))); + setIsLoading(false); + break; + + case 'error': + setCurrentSession(prev => { + if (!prev) return prev; + return { ...prev, status: 'error' }; + }); + setSessions(prev => prev.map(s => (s.id === currentSession?.id ? { ...s, status: 'error' } : s))); + setMessages(prev => [...prev, { + id: Date.now().toString(), + role: 'system', + content: data.message as string, + type: 'error', + timestamp: new Date().toISOString(), + }]); + setIsLoading(false); + break; + } + }, [currentSession?.id]); + + // Select session (can pass session object directly for newly created sessions) + const selectSession = useCallback((sessionId: string, initialQuery?: string, sessionObj?: Session) => { + // Use passed session object or find from sessions array + const session = sessionObj || sessions.find(s => s.id === sessionId); + if (session) { + console.log('[Session] Selecting session:', session.id); + setCurrentSession(session); + setMessages([]); + setLogs([]); + setStreamingContent(''); + connectWebSocket(sessionId, initialQuery); + } else { + console.error('[Session] Session not found:', sessionId); + } + }, [sessions, connectWebSocket]); + + // Send message + const sendMessage = useCallback((content: string) => { + if (!currentSession || !ws || ws.readyState !== WebSocket.OPEN) return; + + // Add user message locally + setMessages(prev => [...prev, { + id: Date.now().toString(), + role: 'user', + content, + type: 'text', + timestamp: new Date().toISOString(), + }]); + + // Send to server + ws.send(JSON.stringify({ + action: 'start', + query: content, + })); + + setIsLoading(true); + }, [currentSession, ws]); + + // Stop agent + const stopAgent = useCallback(() => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ action: 'stop' })); + } + + // Optimistic UI update: reflect stop immediately without waiting for backend + setCurrentSession(prev => { + if (!prev) return prev; + return { ...prev, status: 'stopped', workflow_progress: undefined, file_progress: undefined, current_step: undefined }; + }); + setSessions(prev => prev.map(s => (s.id === currentSession?.id ? { ...s, status: 'stopped' } : s))); + setIsLoading(false); + setIsStreaming(false); + setStreamingContent(''); + }, [ws, currentSession?.id]); + + // Clear logs + const clearLogs = useCallback(() => { + setLogs([]); + }, []); + + // Initial load + useEffect(() => { + loadProjects(); + }, [loadProjects]); + + // Cleanup WebSocket on unmount + useEffect(() => { + return () => { + if (ws) { + ws.close(); + } + }; + }, [ws]); + + return ( + + {children} + + ); +}; + +export const useSession = () => { + const context = useContext(SessionContext); + if (!context) { + throw new Error('useSession must be used within a SessionProvider'); + } + return context; +}; diff --git a/webui/frontend/src/context/ThemeContext.tsx b/webui/frontend/src/context/ThemeContext.tsx new file mode 100644 index 000000000..4bc51ee8d --- /dev/null +++ b/webui/frontend/src/context/ThemeContext.tsx @@ -0,0 +1,281 @@ +import React, { createContext, useContext, useState, useMemo, ReactNode } from 'react'; +import { createTheme, Theme, PaletteMode } from '@mui/material'; + +// Luxury color palettes +const darkPalette = { + primary: { + main: '#C9A962', // Gold accent + light: '#E5D4A1', + dark: '#A08840', + contrastText: '#0A0A0A', + }, + secondary: { + main: '#6B7280', + light: '#9CA3AF', + dark: '#4B5563', + contrastText: '#FFFFFF', + }, + background: { + default: '#0A0A0A', + paper: '#141414', + }, + text: { + primary: '#F5F5F5', + secondary: '#A0A0A0', + }, + divider: 'rgba(201, 169, 98, 0.12)', + error: { + main: '#EF4444', + light: '#F87171', + dark: '#DC2626', + }, + success: { + main: '#10B981', + light: '#34D399', + dark: '#059669', + }, + warning: { + main: '#F59E0B', + light: '#FBBF24', + dark: '#D97706', + }, + info: { + main: '#3B82F6', + light: '#60A5FA', + dark: '#2563EB', + }, +}; + +const lightPalette = { + primary: { + main: '#1A1A1A', + light: '#404040', + dark: '#0A0A0A', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#C9A962', + light: '#E5D4A1', + dark: '#A08840', + contrastText: '#0A0A0A', + }, + background: { + default: '#FAFAFA', + paper: '#FFFFFF', + }, + text: { + primary: '#1A1A1A', + secondary: '#6B7280', + }, + divider: 'rgba(0, 0, 0, 0.08)', + error: { + main: '#DC2626', + light: '#EF4444', + dark: '#B91C1C', + }, + success: { + main: '#059669', + light: '#10B981', + dark: '#047857', + }, + warning: { + main: '#D97706', + light: '#F59E0B', + dark: '#B45309', + }, + info: { + main: '#2563EB', + light: '#3B82F6', + dark: '#1D4ED8', + }, +}; + +const createAppTheme = (mode: PaletteMode): Theme => { + const palette = mode === 'dark' ? darkPalette : lightPalette; + + return createTheme({ + palette: { + mode, + ...palette, + }, + typography: { + fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", + h1: { + fontSize: '3rem', + fontWeight: 600, + letterSpacing: '-0.02em', + }, + h2: { + fontSize: '2.25rem', + fontWeight: 600, + letterSpacing: '-0.02em', + }, + h3: { + fontSize: '1.875rem', + fontWeight: 600, + letterSpacing: '-0.01em', + }, + h4: { + fontSize: '1.5rem', + fontWeight: 600, + }, + h5: { + fontSize: '1.25rem', + fontWeight: 500, + }, + h6: { + fontSize: '1rem', + fontWeight: 500, + }, + body1: { + fontSize: '1rem', + lineHeight: 1.6, + }, + body2: { + fontSize: '0.875rem', + lineHeight: 1.5, + }, + button: { + textTransform: 'none', + fontWeight: 500, + }, + }, + shape: { + borderRadius: 12, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + borderRadius: 8, + padding: '10px 24px', + fontSize: '0.875rem', + fontWeight: 500, + boxShadow: 'none', + '&:hover': { + boxShadow: 'none', + }, + }, + contained: { + '&:hover': { + transform: 'translateY(-1px)', + transition: 'transform 0.2s ease', + }, + }, + outlined: { + borderWidth: 1.5, + '&:hover': { + borderWidth: 1.5, + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + elevation1: { + boxShadow: mode === 'dark' + ? '0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2)' + : '0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04)', + }, + elevation2: { + boxShadow: mode === 'dark' + ? '0 4px 6px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.2)' + : '0 4px 6px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04)', + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: 16, + border: `1px solid ${palette.divider}`, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '& .MuiOutlinedInput-root': { + borderRadius: 10, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: palette.primary.main, + }, + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: 6, + fontWeight: 500, + }, + }, + }, + MuiTooltip: { + styleOverrides: { + tooltip: { + backgroundColor: mode === 'dark' ? '#2D2D2D' : '#1A1A1A', + fontSize: '0.75rem', + padding: '8px 12px', + borderRadius: 6, + }, + }, + }, + MuiDialog: { + styleOverrides: { + paper: { + borderRadius: 20, + }, + }, + }, + MuiDrawer: { + styleOverrides: { + paper: { + borderRight: `1px solid ${palette.divider}`, + }, + }, + }, + }, + }); +}; + +interface ThemeContextType { + mode: PaletteMode; + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [mode, setMode] = useState(() => { + const stored = localStorage.getItem('theme-mode'); + return (stored as PaletteMode) || 'dark'; + }); + + const theme = useMemo(() => createAppTheme(mode), [mode]); + + const toggleTheme = () => { + const newMode = mode === 'dark' ? 'light' : 'dark'; + setMode(newMode); + localStorage.setItem('theme-mode', newMode); + }; + + return ( + + {children} + + ); +}; + +export const useThemeContext = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useThemeContext must be used within a ThemeProvider'); + } + return context; +}; diff --git a/webui/frontend/src/main.tsx b/webui/frontend/src/main.tsx new file mode 100644 index 000000000..692912fa6 --- /dev/null +++ b/webui/frontend/src/main.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { ThemeProvider as MuiThemeProvider, CssBaseline } from '@mui/material' +import App from './App' +import { ThemeProvider, useThemeContext } from './context/ThemeContext' +import { SessionProvider } from './context/SessionContext' + +const ThemedApp: React.FC = () => { + const { theme } = useThemeContext(); + + return ( + + + + + + + ); +}; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/webui/frontend/tsconfig.json b/webui/frontend/tsconfig.json new file mode 100644 index 000000000..3934b8f6d --- /dev/null +++ b/webui/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/webui/frontend/tsconfig.node.json b/webui/frontend/tsconfig.node.json new file mode 100644 index 000000000..97ede7ee6 --- /dev/null +++ b/webui/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/webui/frontend/vite.config.ts b/webui/frontend/vite.config.ts new file mode 100644 index 000000000..63159c4b1 --- /dev/null +++ b/webui/frontend/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:7860', + changeOrigin: true + }, + '/ws': { + target: 'ws://localhost:7860', + ws: true + } + } + }, + build: { + outDir: 'dist', + sourcemap: true + } +}) diff --git a/webui/requirements.txt b/webui/requirements.txt new file mode 100644 index 000000000..038d28eca --- /dev/null +++ b/webui/requirements.txt @@ -0,0 +1,3 @@ +ms-agent +pandas +aiohttp \ No newline at end of file diff --git a/webui/start.sh b/webui/start.sh new file mode 100755 index 000000000..0d5df3e1e --- /dev/null +++ b/webui/start.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# MS-Agent Web UI Startup Script +# This script starts both the backend server and frontend development server + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get the directory where the script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo -e "${BLUE}" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ MS-Agent Web UI ║" +echo "║ Intelligent Agent Platform ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo -e "${NC}" + +# Check for Python 3.10+ +PYTHON_CMD="" +for py in python3.12 python3.11 python3.10; do + if command -v $py &> /dev/null; then + PYTHON_CMD=$py + break + fi +done + +if [ -z "$PYTHON_CMD" ]; then + echo -e "${RED}Error: Python 3.10 or higher is required but not found.${NC}" + echo -e "${YELLOW}Please install Python 3.10+ and try again.${NC}" + exit 1 +fi + +echo -e "${GREEN}Using Python: $PYTHON_CMD ($(${PYTHON_CMD} --version))${NC}" + +# Check for Node.js +if ! command -v node &> /dev/null; then + echo -e "${RED}Error: Node.js is required but not installed.${NC}" + exit 1 +fi + +# Create virtual environment if not exists +VENV_DIR="$SCRIPT_DIR/.venv" +if [ ! -d "$VENV_DIR" ]; then + echo -e "${YELLOW}Creating Python virtual environment with ${PYTHON_CMD}...${NC}" + $PYTHON_CMD -m venv "$VENV_DIR" +fi + +# Activate virtual environment +source "$VENV_DIR/bin/activate" + +# Install Python dependencies +echo -e "${YELLOW}Installing Python dependencies...${NC}" +pip install -q -r "$SCRIPT_DIR/requirements.txt" + +# Install ms-agent in development mode if not installed +if ! python -c "import ms_agent" 2>/dev/null; then + echo -e "${YELLOW}Installing ms-agent...${NC}" + pip install -q -e "$SCRIPT_DIR/../ms-agent" +fi + + +# Install frontend dependencies if needed +if [ ! -d "$SCRIPT_DIR/frontend/node_modules" ]; then + echo -e "${YELLOW}Installing frontend dependencies...${NC}" + cd "$SCRIPT_DIR/frontend" + npm install + cd "$SCRIPT_DIR" +fi + +# Parse command line arguments +MODE="dev" +PORT=7860 +HOST="0.0.0.0" + +while [[ $# -gt 0 ]]; do + case $1 in + --production|-p) + MODE="production" + shift + ;; + --port) + PORT="$2" + shift 2 + ;; + --host) + HOST="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Build frontend for production +if [ "$MODE" = "production" ]; then + echo -e "${YELLOW}Building frontend for production...${NC}" + cd "$SCRIPT_DIR/frontend" + npm run build + cd "$SCRIPT_DIR" +fi + +# Function to cleanup background processes +cleanup() { + echo -e "\n${YELLOW}Shutting down...${NC}" + if [ ! -z "$BACKEND_PID" ]; then + kill $BACKEND_PID 2>/dev/null + fi + if [ ! -z "$FRONTEND_PID" ]; then + kill $FRONTEND_PID 2>/dev/null + fi + exit 0 +} + +trap cleanup SIGINT SIGTERM + +# Start backend server +echo -e "${GREEN}Starting backend server on port $PORT...${NC}" +cd "$SCRIPT_DIR/backend" +if [ "$MODE" = "production" ]; then + python main.py --host "$HOST" --port "$PORT" & +else + python main.py --host "$HOST" --port "$PORT" --reload & +fi +BACKEND_PID=$! +cd "$SCRIPT_DIR" + +# Wait for backend to start +sleep 2 + +if [ "$MODE" = "dev" ]; then + # Start frontend development server + echo -e "${GREEN}Starting frontend development server...${NC}" + cd "$SCRIPT_DIR/frontend" + npm run dev & + FRONTEND_PID=$! + cd "$SCRIPT_DIR" + + echo -e "\n${GREEN}✓ Development servers are running!${NC}" + echo -e " Backend: ${BLUE}http://localhost:$PORT${NC}" + echo -e " Frontend: ${BLUE}http://localhost:5173${NC}" + echo -e " API Docs: ${BLUE}http://localhost:$PORT/docs${NC}" +else + echo -e "\n${GREEN}✓ Production server is running!${NC}" + echo -e " Server: ${BLUE}http://$HOST:$PORT${NC}" +fi + +echo -e "\n${YELLOW}Press Ctrl+C to stop the servers${NC}\n" + +# Wait for processes +wait From 86ec01b567319890c530272553568b057cfa136a Mon Sep 17 00:00:00 2001 From: vinci Date: Fri, 16 Jan 2026 15:40:01 +0800 Subject: [PATCH 2/2] pre-commit --- webui/README.md | 24 +- webui/backend/agent_runner.py | 237 ++++++++++-------- webui/backend/api.py | 237 +++++++++++------- webui/backend/config_manager.py | 80 +++--- webui/backend/main.py | 69 +++-- webui/backend/project_discovery.py | 56 +++-- webui/backend/session_manager.py | 63 ++--- webui/backend/shared.py | 13 +- webui/backend/websocket_handler.py | 151 ++++++----- webui/config/mcp_servers.json | 2 +- webui/config/settings.json | 2 +- webui/frontend/src/App.tsx | 10 +- .../src/components/ConversationView.tsx | 40 +-- .../frontend/src/components/FileProgress.tsx | 2 +- webui/frontend/src/components/LogViewer.tsx | 10 +- .../src/components/MessageContent.tsx | 2 +- webui/frontend/src/components/SearchView.tsx | 14 +- .../src/components/SettingsDialog.tsx | 20 +- .../src/components/WorkflowProgress.tsx | 8 +- webui/frontend/src/context/SessionContext.tsx | 42 ++-- webui/frontend/src/context/ThemeContext.tsx | 10 +- webui/frontend/src/main.tsx | 2 +- webui/requirements.txt | 2 +- webui/start.sh | 2 +- 24 files changed, 595 insertions(+), 503 deletions(-) diff --git a/webui/README.md b/webui/README.md index 9f8dccead..89cec5543 100644 --- a/webui/README.md +++ b/webui/README.md @@ -52,7 +52,7 @@ def _analyze_project(self, name: str, path: str): workflow_file = os.path.join(path, 'workflow.yaml') agent_file = os.path.join(path, 'agent.yaml') run_file = os.path.join(path, 'run.py') - + if os.path.exists(workflow_file): project_type = 'workflow' # 工作流项目 config_file = workflow_file @@ -79,7 +79,7 @@ def _analyze_project(self, name: str, path: str): @router.websocket("/session/{session_id}") async def websocket_session(websocket: WebSocket, session_id: str): await connection_manager.connect(websocket, session_id) - + try: while True: data = await websocket.receive_json() @@ -104,10 +104,10 @@ async def websocket_session(websocket: WebSocket, session_id: str): async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocket): # 1. 获取会话信息 session = session_manager.get_session(session_id) - + # 2. 获取项目信息 project = project_discovery.get_project(session['project_id']) - + # 3. 创建AgentRunner runner = AgentRunner( session_id=session_id, @@ -119,7 +119,7 @@ async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocke on_complete=lambda result: asyncio.create_task(on_agent_complete(session_id, result)), on_error=lambda err: asyncio.create_task(on_agent_error(session_id, err)) ) - + # 4. 启动代理 task = asyncio.create_task(runner.start(data.get('query', ''))) ``` @@ -134,7 +134,7 @@ async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocke def _build_command(self, query: str) -> list: project_type = self.project.get('type') config_file = self.project.get('config_file', '') - + if project_type == 'workflow' or project_type == 'agent': # workflow/agent类型:使用ms-agent CLI cmd = [ @@ -142,15 +142,15 @@ def _build_command(self, query: str) -> list: '--config', config_file, # workflow.yaml 或 agent.yaml '--trust_remote_code', 'true' ] - + if query: cmd.extend(['--query', query]) - + # 添加MCP服务器配置 mcp_file = self.config_manager.get_mcp_file_path() if os.path.exists(mcp_file): cmd.extend(['--mcp_server_file', mcp_file]) - + # 添加LLM配置 llm_config = self.config_manager.get_llm_config() if llm_config.get('api_key'): @@ -159,11 +159,11 @@ def _build_command(self, query: str) -> list: cmd.extend(['--modelscope_api_key', llm_config['api_key']]) elif provider == 'openai': cmd.extend(['--openai_api_key', llm_config['api_key']]) - + elif project_type == 'script': # script类型:直接运行Python脚本 cmd = [python, self.project['config_file']] # run.py - + return cmd ``` @@ -173,4 +173,4 @@ def _build_command(self, query: str) -> list: |---------|----------|--------------| | **workflow** | `workflow.yaml` | `ms-agent run --config workflow.yaml --trust_remote_code true --query "xxx" --mcp_server_file xxx.json --modelscope_api_key xxx` | | **agent** | `agent.yaml` | `ms-agent run --config agent.yaml --trust_remote_code true --query "xxx" --mcp_server_file xxx.json --modelscope_api_key xxx` | -| **script** | `run.py` | `python run.py` | \ No newline at end of file +| **script** | `run.py` | `python run.py` | diff --git a/webui/backend/agent_runner.py b/webui/backend/agent_runner.py index 44c872f96..afe50ea16 100644 --- a/webui/backend/agent_runner.py +++ b/webui/backend/agent_runner.py @@ -3,30 +3,28 @@ Agent runner for MS-Agent Web UI Manages the execution of ms-agent through subprocess with log streaming. """ +import asyncio import os -import sys import re -import asyncio -import subprocess import signal -from typing import Dict, Any, Callable, Optional +import subprocess +import sys from datetime import datetime +from typing import Any, Callable, Dict, Optional class AgentRunner: """Runs ms-agent as a subprocess with output streaming""" - - def __init__( - self, - session_id: str, - project: Dict[str, Any], - config_manager, - on_output: Callable[[Dict[str, Any]], None] = None, - on_log: Callable[[Dict[str, Any]], None] = None, - on_progress: Callable[[Dict[str, Any]], None] = None, - on_complete: Callable[[Dict[str, Any]], None] = None, - on_error: Callable[[Dict[str, Any]], None] = None - ): + + def __init__(self, + session_id: str, + project: Dict[str, Any], + config_manager, + on_output: Callable[[Dict[str, Any]], None] = None, + on_log: Callable[[Dict[str, Any]], None] = None, + on_progress: Callable[[Dict[str, Any]], None] = None, + on_complete: Callable[[Dict[str, Any]], None] = None, + on_error: Callable[[Dict[str, Any]], None] = None): self.session_id = session_id self.project = project self.config_manager = config_manager @@ -35,28 +33,28 @@ def __init__( self.on_progress = on_progress self.on_complete = on_complete self.on_error = on_error - + self.process: Optional[asyncio.subprocess.Process] = None self.is_running = False - self._accumulated_output = "" + self._accumulated_output = '' self._current_step = None self._workflow_steps = [] self._stop_requested = False - + async def start(self, query: str): """Start the agent""" try: self._stop_requested = False self.is_running = True - + # Build command based on project type cmd = self._build_command(query) env = self._build_env() - - print(f"[Runner] Starting agent with command:") + + print('[Runner] Starting agent with command:') print(f"[Runner] {' '.join(cmd)}") print(f"[Runner] Working directory: {self.project['path']}") - + # Log the command if self.on_log: self.on_log({ @@ -64,7 +62,7 @@ async def start(self, query: str): 'message': f'Starting agent: {" ".join(cmd[:5])}...', 'timestamp': datetime.now().isoformat() }) - + # Start subprocess self.process = await asyncio.create_subprocess_exec( *cmd, @@ -73,24 +71,20 @@ async def start(self, query: str): stdin=asyncio.subprocess.PIPE, env=env, cwd=self.project['path'], - start_new_session=True - ) - - print(f"[Runner] Process started with PID: {self.process.pid}") - + start_new_session=True) + + print(f'[Runner] Process started with PID: {self.process.pid}') + # Start output reader await self._read_output() - + except Exception as e: - print(f"[Runner] ERROR: {e}") + print(f'[Runner] ERROR: {e}') import traceback traceback.print_exc() if self.on_error: - self.on_error({ - 'message': str(e), - 'type': 'startup_error' - }) - + self.on_error({'message': str(e), 'type': 'startup_error'}) + async def stop(self): """Stop the agent""" self._stop_requested = True @@ -125,39 +119,38 @@ async def stop(self): pass except Exception: pass - + async def send_input(self, text: str): """Send input to the agent""" if self.process and self.process.stdin: self.process.stdin.write((text + '\n').encode()) await self.process.stdin.drain() - + def _build_command(self, query: str) -> list: """Build the command to run the agent""" project_type = self.project.get('type') project_path = self.project['path'] config_file = self.project.get('config_file', '') - + # Get python executable python = sys.executable - + # Get MCP config file path mcp_file = self.config_manager.get_mcp_file_path() - + if project_type == 'workflow' or project_type == 'agent': # Use ms-agent CLI command (installed via entry point) cmd = [ - 'ms-agent', 'run', - '--config', config_file, + 'ms-agent', 'run', '--config', config_file, '--trust_remote_code', 'true' ] - + if query: cmd.extend(['--query', query]) - + if os.path.exists(mcp_file): cmd.extend(['--mcp_server_file', mcp_file]) - + # Add LLM config llm_config = self.config_manager.get_llm_config() if llm_config.get('api_key'): @@ -166,45 +159,46 @@ def _build_command(self, query: str) -> list: cmd.extend(['--modelscope_api_key', llm_config['api_key']]) elif provider == 'openai': cmd.extend(['--openai_api_key', llm_config['api_key']]) - + elif project_type == 'script': # Run the script directly cmd = [python, self.project['config_file']] else: cmd = [python, '-m', 'ms_agent', 'run', '--config', project_path] - + return cmd - + def _build_env(self) -> Dict[str, str]: """Build environment variables""" env = os.environ.copy() - + # Add config env vars env.update(self.config_manager.get_env_vars()) - + # Set PYTHONUNBUFFERED for real-time output env['PYTHONUNBUFFERED'] = '1' - + return env - + async def _read_output(self): """Read and process output from the subprocess""" - print(f"[Runner] Starting to read output...") + print('[Runner] Starting to read output...') try: while self.is_running and self.process and self.process.stdout: line = await self.process.stdout.readline() if not line: - print(f"[Runner] No more output, breaking...") + print('[Runner] No more output, breaking...') break - + text = line.decode('utf-8', errors='replace').rstrip() - print(f"[Runner] Output: {text[:200]}" if len(text) > 200 else f"[Runner] Output: {text}") + print(f'[Runner] Output: {text[:200]}' + if len(text) > 200 else f'[Runner] Output: {text}') await self._process_line(text) - + # Wait for process to complete if self.process: return_code = await self.process.wait() - print(f"[Runner] Process exited with code: {return_code}") + print(f'[Runner] Process exited with code: {return_code}') # If stop was requested, do not report as completion/error if self._stop_requested: @@ -219,8 +213,10 @@ async def _read_output(self): if return_code == 0: if self.on_complete: self.on_complete({ - 'status': 'success', - 'message': 'Agent completed successfully' + 'status': + 'success', + 'message': + 'Agent completed successfully' }) else: if self.on_error: @@ -229,20 +225,17 @@ async def _read_output(self): 'type': 'exit_error', 'code': return_code }) - + except Exception as e: - print(f"[Runner] Read error: {e}") + print(f'[Runner] Read error: {e}') import traceback traceback.print_exc() if not self._stop_requested and self.on_error: - self.on_error({ - 'message': str(e), - 'type': 'read_error' - }) + self.on_error({'message': str(e), 'type': 'read_error'}) finally: self.is_running = False - print(f"[Runner] Finished reading output") - + print('[Runner] Finished reading output') + async def _process_line(self, line: str): """Process a line of output""" # Log the line @@ -253,10 +246,10 @@ async def _process_line(self, line: str): 'message': line, 'timestamp': datetime.now().isoformat() }) - + # Parse for special patterns await self._detect_patterns(line) - + def _detect_log_level(self, line: str) -> str: """Detect log level from line""" line_lower = line.lower() @@ -267,37 +260,41 @@ def _detect_log_level(self, line: str) -> str: elif '[debug' in line_lower: return 'debug' return 'info' - + async def _detect_patterns(self, line: str): """Detect special patterns in output""" # Detect workflow step beginning: "[tag] Agent tag task beginning." - begin_match = re.search(r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+beginning', line) + begin_match = re.search( + r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+beginning', line) if begin_match: step_name = begin_match.group(1) - + # Skip sub-steps (contain -r0-, -diversity-, etc.) if '-r' in step_name and '-' in step_name.split('-r')[-1]: - print(f"[Runner] Skipping sub-step: {step_name}") + print(f'[Runner] Skipping sub-step: {step_name}') return - - print(f"[Runner] Detected step beginning: {step_name}") - + + print(f'[Runner] Detected step beginning: {step_name}') + # If there's a previous step running, mark it as completed first if self._current_step and self._current_step != step_name: prev_step = self._current_step - print(f"[Runner] Auto-completing previous step: {prev_step}") + print(f'[Runner] Auto-completing previous step: {prev_step}') if self.on_output: self.on_output({ 'type': 'step_complete', 'content': prev_step, 'role': 'assistant', - 'metadata': {'step': prev_step, 'status': 'completed'} + 'metadata': { + 'step': prev_step, + 'status': 'completed' + } }) - + self._current_step = step_name if step_name not in self._workflow_steps: self._workflow_steps.append(step_name) - + # Build step status - all previous steps completed, current running step_status = {} for i, s in enumerate(self._workflow_steps): @@ -307,7 +304,7 @@ async def _detect_patterns(self, line: str): step_status[s] = 'completed' else: step_status[s] = 'pending' - + if self.on_progress: self.on_progress({ 'type': 'workflow', @@ -315,33 +312,38 @@ async def _detect_patterns(self, line: str): 'steps': self._workflow_steps.copy(), 'step_status': step_status }) - + # Send step start message if self.on_output: self.on_output({ 'type': 'step_start', 'content': step_name, 'role': 'assistant', - 'metadata': {'step': step_name, 'status': 'running'} + 'metadata': { + 'step': step_name, + 'status': 'running' + } }) return - + # Detect workflow step finished: "[tag] Agent tag task finished." - end_match = re.search(r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+finished', line) + end_match = re.search(r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+finished', + line) if end_match: step_name = end_match.group(1) - + # Skip sub-steps if '-r' in step_name and '-' in step_name.split('-r')[-1]: return - - print(f"[Runner] Detected step finished: {step_name}") - + + print(f'[Runner] Detected step finished: {step_name}') + # Build step status dict - all steps up to current are completed step_status = {} for s in self._workflow_steps: - step_status[s] = 'completed' if self._workflow_steps.index(s) <= self._workflow_steps.index(step_name) else 'pending' - + step_status[s] = 'completed' if self._workflow_steps.index( + s) <= self._workflow_steps.index(step_name) else 'pending' + if self.on_progress: self.on_progress({ 'type': 'workflow', @@ -349,22 +351,25 @@ async def _detect_patterns(self, line: str): 'steps': self._workflow_steps.copy(), 'step_status': step_status }) - + # Send step complete message if self.on_output: self.on_output({ 'type': 'step_complete', 'content': step_name, 'role': 'assistant', - 'metadata': {'step': step_name, 'status': 'completed'} + 'metadata': { + 'step': step_name, + 'status': 'completed' + } }) return - + # Detect assistant output: "[tag] [assistant]:" if '[assistant]:' in line: - self._accumulated_output = "" + self._accumulated_output = '' return - + # Detect tool calls: "[tag] [tool_calling]:" if '[tool_calling]:' in line: if self.on_output: @@ -374,11 +379,13 @@ async def _detect_patterns(self, line: str): 'role': 'assistant' }) return - + # Detect file writing - file_match = re.search(r'writing file:?\s*["\']?([^\s"\']+)["\']?', line.lower()) + file_match = re.search(r'writing file:?\s*["\']?([^\s"\']+)["\']?', + line.lower()) if not file_match: - file_match = re.search(r'creating file:?\s*["\']?([^\s"\']+)["\']?', line.lower()) + file_match = re.search( + r'creating file:?\s*["\']?([^\s"\']+)["\']?', line.lower()) if file_match and self.on_progress: filename = file_match.group(1) self.on_progress({ @@ -387,22 +394,28 @@ async def _detect_patterns(self, line: str): 'status': 'writing' }) return - + # Detect file written/created/saved - multiple patterns - file_keywords = ['file created', 'file written', 'file saved', 'saved to:', 'wrote to', 'generated:', 'output:'] + file_keywords = [ + 'file created', 'file written', 'file saved', 'saved to:', + 'wrote to', 'generated:', 'output:' + ] if any(keyword in line.lower() for keyword in file_keywords): # Try to extract filename with extension - file_match = re.search(r'["\']?([^\s"\'\[\]]+\.[a-zA-Z0-9]+)["\']?', line) + file_match = re.search( + r'["\']?([^\s"\'\[\]]+\.[a-zA-Z0-9]+)["\']?', line) if file_match and self.on_progress: filename = file_match.group(1) - print(f"[Runner] Detected file output: {filename}") + print(f'[Runner] Detected file output: {filename}') # Send as output file if self.on_output: self.on_output({ 'type': 'file_output', 'content': filename, 'role': 'assistant', - 'metadata': {'filename': filename} + 'metadata': { + 'filename': filename + } }) self.on_progress({ 'type': 'file', @@ -410,18 +423,22 @@ async def _detect_patterns(self, line: str): 'status': 'completed' }) return - + # Detect output file paths (e.g., "output/user_story.txt" standalone) - output_path_match = re.search(r'(?:^|\s)((?:output|projects)/[^\s]+\.[a-zA-Z0-9]+)(?:\s|$)', line) + output_path_match = re.search( + r'(?:^|\s)((?:output|projects)/[^\s]+\.[a-zA-Z0-9]+)(?:\s|$)', + line) if output_path_match and self.on_progress: filename = output_path_match.group(1) - print(f"[Runner] Detected output path: {filename}") + print(f'[Runner] Detected output path: {filename}') if self.on_output: self.on_output({ 'type': 'file_output', 'content': filename, 'role': 'assistant', - 'metadata': {'filename': filename} + 'metadata': { + 'filename': filename + } }) self.on_progress({ 'type': 'file', diff --git a/webui/backend/api.py b/webui/backend/api.py index d8ee59164..c33eda324 100644 --- a/webui/backend/api.py +++ b/webui/backend/api.py @@ -4,12 +4,12 @@ """ import os import uuid -from typing import Dict, List, Optional, Any +from typing import Any, Dict, List, Optional + from fastapi import APIRouter, HTTPException from pydantic import BaseModel - # Import shared instances -from shared import project_discovery, config_manager, session_manager +from shared import config_manager, project_discovery, session_manager router = APIRouter() @@ -39,8 +39,8 @@ class SessionInfo(BaseModel): class LLMConfig(BaseModel): - provider: str = "openai" - model: str = "qwen3-coder-plus" + provider: str = 'openai' + model: str = 'qwen3-coder-plus' api_key: Optional[str] = None base_url: Optional[str] = None temperature: float = 0.7 @@ -59,152 +59,179 @@ class MCPServer(BaseModel): class GlobalConfig(BaseModel): llm: LLMConfig mcp_servers: Dict[str, Any] - theme: str = "dark" - output_dir: str = "./output" + theme: str = 'dark' + output_dir: str = './output' # Project Endpoints -@router.get("/projects", response_model=List[ProjectInfo]) +@router.get('/projects', response_model=List[ProjectInfo]) async def list_projects(): """List all available projects""" return project_discovery.discover_projects() -@router.get("/projects/{project_id}") +@router.get('/projects/{project_id}') async def get_project(project_id: str): """Get detailed information about a specific project""" project = project_discovery.get_project(project_id) if not project: - raise HTTPException(status_code=404, detail="Project not found") + raise HTTPException(status_code=404, detail='Project not found') return project -@router.get("/projects/{project_id}/readme") +@router.get('/projects/{project_id}/readme') async def get_project_readme(project_id: str): """Get the README content for a project""" readme = project_discovery.get_project_readme(project_id) if readme is None: - raise HTTPException(status_code=404, detail="README not found") - return {"content": readme} + raise HTTPException(status_code=404, detail='README not found') + return {'content': readme} # Session Endpoints -@router.post("/sessions", response_model=SessionInfo) +@router.post('/sessions', response_model=SessionInfo) async def create_session(session_data: SessionCreate): """Create a new session for a project""" project = project_discovery.get_project(session_data.project_id) if not project: - raise HTTPException(status_code=404, detail="Project not found") - + raise HTTPException(status_code=404, detail='Project not found') + session = session_manager.create_session( - project_id=session_data.project_id, - project_name=project['name'] - ) + project_id=session_data.project_id, project_name=project['name']) return session -@router.get("/sessions", response_model=List[SessionInfo]) +@router.get('/sessions', response_model=List[SessionInfo]) async def list_sessions(): """List all active sessions""" return session_manager.list_sessions() -@router.get("/sessions/{session_id}") +@router.get('/sessions/{session_id}') async def get_session(session_id: str): """Get session details""" session = session_manager.get_session(session_id) if not session: - raise HTTPException(status_code=404, detail="Session not found") + raise HTTPException(status_code=404, detail='Session not found') return session -@router.delete("/sessions/{session_id}") +@router.delete('/sessions/{session_id}') async def delete_session(session_id: str): """Delete a session""" success = session_manager.delete_session(session_id) if not success: - raise HTTPException(status_code=404, detail="Session not found") - return {"status": "deleted"} + raise HTTPException(status_code=404, detail='Session not found') + return {'status': 'deleted'} -@router.get("/sessions/{session_id}/messages") +@router.get('/sessions/{session_id}/messages') async def get_session_messages(session_id: str): """Get all messages for a session""" messages = session_manager.get_messages(session_id) if messages is None: - raise HTTPException(status_code=404, detail="Session not found") - return {"messages": messages} + raise HTTPException(status_code=404, detail='Session not found') + return {'messages': messages} # Configuration Endpoints -@router.get("/config") +@router.get('/config') async def get_config(): """Get global configuration""" return config_manager.get_config() -@router.put("/config") +@router.put('/config') async def update_config(config: GlobalConfig): """Update global configuration""" config_manager.update_config(config.model_dump()) - return {"status": "updated"} + return {'status': 'updated'} -@router.get("/config/llm") +@router.get('/config/llm') async def get_llm_config(): """Get LLM configuration""" return config_manager.get_llm_config() -@router.put("/config/llm") +@router.put('/config/llm') async def update_llm_config(config: LLMConfig): """Update LLM configuration""" config_manager.update_llm_config(config.model_dump()) - return {"status": "updated"} + return {'status': 'updated'} -@router.get("/config/mcp") +@router.get('/config/mcp') async def get_mcp_config(): """Get MCP servers configuration""" return config_manager.get_mcp_config() -@router.put("/config/mcp") +@router.put('/config/mcp') async def update_mcp_config(servers: Dict[str, Any]): """Update MCP servers configuration""" config_manager.update_mcp_config(servers) - return {"status": "updated"} + return {'status': 'updated'} -@router.post("/config/mcp/servers") +@router.post('/config/mcp/servers') async def add_mcp_server(server: MCPServer): """Add a new MCP server""" - config_manager.add_mcp_server(server.name, server.model_dump(exclude={'name'})) - return {"status": "added"} + config_manager.add_mcp_server(server.name, + server.model_dump(exclude={'name'})) + return {'status': 'added'} -@router.delete("/config/mcp/servers/{server_name}") +@router.delete('/config/mcp/servers/{server_name}') async def remove_mcp_server(server_name: str): """Remove an MCP server""" success = config_manager.remove_mcp_server(server_name) if not success: - raise HTTPException(status_code=404, detail="Server not found") - return {"status": "removed"} + raise HTTPException(status_code=404, detail='Server not found') + return {'status': 'removed'} # Available models endpoint -@router.get("/models") +@router.get('/models') async def list_available_models(): """List available LLM models""" return { - "models": [ - {"provider": "modelscope", "model": "Qwen/Qwen3-235B-A22B-Instruct-2507", "display_name": "Qwen3-235B (Recommended)"}, - {"provider": "modelscope", "model": "Qwen/Qwen2.5-72B-Instruct", "display_name": "Qwen2.5-72B"}, - {"provider": "modelscope", "model": "Qwen/Qwen2.5-32B-Instruct", "display_name": "Qwen2.5-32B"}, - {"provider": "modelscope", "model": "deepseek-ai/DeepSeek-V3", "display_name": "DeepSeek-V3"}, - {"provider": "openai", "model": "gpt-4o", "display_name": "GPT-4o"}, - {"provider": "openai", "model": "gpt-4o-mini", "display_name": "GPT-4o Mini"}, - {"provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "display_name": "Claude 3.5 Sonnet"}, + 'models': [ + { + 'provider': 'modelscope', + 'model': 'Qwen/Qwen3-235B-A22B-Instruct-2507', + 'display_name': 'Qwen3-235B (Recommended)' + }, + { + 'provider': 'modelscope', + 'model': 'Qwen/Qwen2.5-72B-Instruct', + 'display_name': 'Qwen2.5-72B' + }, + { + 'provider': 'modelscope', + 'model': 'Qwen/Qwen2.5-32B-Instruct', + 'display_name': 'Qwen2.5-32B' + }, + { + 'provider': 'modelscope', + 'model': 'deepseek-ai/DeepSeek-V3', + 'display_name': 'DeepSeek-V3' + }, + { + 'provider': 'openai', + 'model': 'gpt-4o', + 'display_name': 'GPT-4o' + }, + { + 'provider': 'openai', + 'model': 'gpt-4o-mini', + 'display_name': 'GPT-4o Mini' + }, + { + 'provider': 'anthropic', + 'model': 'claude-3-5-sonnet-20241022', + 'display_name': 'Claude 3.5 Sonnet' + }, ] } @@ -215,34 +242,37 @@ class FileReadRequest(BaseModel): session_id: Optional[str] = None -@router.get("/files/list") +@router.get('/files/list') async def list_output_files(): """List all files in the output directory as a tree structure""" - base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + base_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) output_dir = os.path.join(base_dir, 'ms-agent', 'output') - + # Folders to exclude - exclude_dirs = {'node_modules', '__pycache__', '.git', '.venv', 'venv', 'dist', 'build'} - + exclude_dirs = { + 'node_modules', '__pycache__', '.git', '.venv', 'venv', 'dist', 'build' + } + def build_tree(dir_path: str) -> dict: """Recursively build a tree structure""" result = {'folders': {}, 'files': []} - + if not os.path.exists(dir_path): return result - + try: items = os.listdir(dir_path) except PermissionError: return result - + for item in sorted(items): # Skip hidden files/folders and excluded directories if item.startswith('.') or item in exclude_dirs: continue - + full_path = os.path.join(dir_path, item) - + if os.path.isdir(full_path): # Recursively build subtree subtree = build_tree(full_path) @@ -256,26 +286,27 @@ def build_tree(dir_path: str) -> dict: 'size': os.path.getsize(full_path), 'modified': os.path.getmtime(full_path) }) - + # Sort files by modification time (newest first) result['files'].sort(key=lambda x: x['modified'], reverse=True) - + return result - + tree = build_tree(output_dir) return {'tree': tree, 'output_dir': output_dir} -@router.post("/files/read") +@router.post('/files/read') async def read_file_content(request: FileReadRequest): """Read content of a generated file""" file_path = request.path - + # Get base directories for security check - base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + base_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) output_dir = os.path.join(base_dir, 'ms-agent', 'output') projects_dir = os.path.join(base_dir, 'ms-agent', 'projects') - + # Resolve the file path if not os.path.isabs(file_path): # Try output dir first @@ -285,51 +316,67 @@ async def read_file_content(request: FileReadRequest): full_path = os.path.join(projects_dir, file_path) else: full_path = file_path - + # Normalize path full_path = os.path.normpath(full_path) - + # Security check: ensure file is within allowed directories allowed_dirs = [output_dir, projects_dir] - is_allowed = any(full_path.startswith(os.path.normpath(d)) for d in allowed_dirs) - + is_allowed = any( + full_path.startswith(os.path.normpath(d)) for d in allowed_dirs) + if not is_allowed: - raise HTTPException(status_code=403, detail="Access denied: file outside allowed directories") - + raise HTTPException( + status_code=403, + detail='Access denied: file outside allowed directories') + if not os.path.exists(full_path): - raise HTTPException(status_code=404, detail=f"File not found: {file_path}") - + raise HTTPException( + status_code=404, detail=f'File not found: {file_path}') + if not os.path.isfile(full_path): - raise HTTPException(status_code=400, detail="Path is not a file") - + raise HTTPException(status_code=400, detail='Path is not a file') + # Check file size (limit to 1MB) file_size = os.path.getsize(full_path) if file_size > 1024 * 1024: - raise HTTPException(status_code=400, detail="File too large (max 1MB)") - + raise HTTPException(status_code=400, detail='File too large (max 1MB)') + try: with open(full_path, 'r', encoding='utf-8') as f: content = f.read() - + # Detect language from extension ext = os.path.splitext(full_path)[1].lower() lang_map = { - '.py': 'python', '.js': 'javascript', '.ts': 'typescript', - '.tsx': 'typescript', '.jsx': 'javascript', '.json': 'json', - '.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown', - '.html': 'html', '.css': 'css', '.txt': 'text', - '.sh': 'bash', '.java': 'java', '.go': 'go', '.rs': 'rust', + '.py': 'python', + '.js': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.jsx': 'javascript', + '.json': 'json', + '.yaml': 'yaml', + '.yml': 'yaml', + '.md': 'markdown', + '.html': 'html', + '.css': 'css', + '.txt': 'text', + '.sh': 'bash', + '.java': 'java', + '.go': 'go', + '.rs': 'rust', } language = lang_map.get(ext, 'text') - + return { - "content": content, - "path": full_path, - "filename": os.path.basename(full_path), - "language": language, - "size": file_size + 'content': content, + 'path': full_path, + 'filename': os.path.basename(full_path), + 'language': language, + 'size': file_size } except UnicodeDecodeError: - raise HTTPException(status_code=400, detail="File is not a text file") + raise HTTPException(status_code=400, detail='File is not a text file') except Exception as e: - raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}") + raise HTTPException( + status_code=500, detail=f'Error reading file: {str(e)}') diff --git a/webui/backend/config_manager.py b/webui/backend/config_manager.py index 1e9a7718a..0af0f2d4c 100644 --- a/webui/backend/config_manager.py +++ b/webui/backend/config_manager.py @@ -4,28 +4,29 @@ Handles global settings, LLM configuration, and MCP server configuration. """ import os -import json -from typing import Dict, Any, Optional from threading import Lock +from typing import Any, Dict, Optional + +import json class ConfigManager: """Manages global configuration for the Web UI""" - + DEFAULT_CONFIG = { - "llm": { - "provider": "modelscope", - "model": "Qwen/Qwen3-235B-A22B-Instruct-2507", - "api_key": "", - "base_url": "https://api-inference.modelscope.cn/v1/", - "temperature": 0.7, - "max_tokens": 4096 + 'llm': { + 'provider': 'modelscope', + 'model': 'Qwen/Qwen3-235B-A22B-Instruct-2507', + 'api_key': '', + 'base_url': 'https://api-inference.modelscope.cn/v1/', + 'temperature': 0.7, + 'max_tokens': 4096 }, - "mcp_servers": {}, - "theme": "dark", - "output_dir": "./output" + 'mcp_servers': {}, + 'theme': 'dark', + 'output_dir': './output' } - + def __init__(self, config_dir: str): self.config_dir = config_dir self.config_file = os.path.join(config_dir, 'settings.json') @@ -33,16 +34,16 @@ def __init__(self, config_dir: str): self._lock = Lock() self._config: Optional[Dict[str, Any]] = None self._ensure_config_dir() - + def _ensure_config_dir(self): """Ensure config directory exists""" os.makedirs(self.config_dir, exist_ok=True) - + def _load_config(self) -> Dict[str, Any]: """Load configuration from file""" if self._config is not None: return self._config - + if os.path.exists(self.config_file): try: with open(self.config_file, 'r', encoding='utf-8') as f: @@ -51,7 +52,7 @@ def _load_config(self) -> Dict[str, Any]: self._config = self.DEFAULT_CONFIG.copy() else: self._config = self.DEFAULT_CONFIG.copy() - + # Load MCP servers from separate file if exists if os.path.exists(self.mcp_file): try: @@ -63,48 +64,51 @@ def _load_config(self) -> Dict[str, Any]: self._config['mcp_servers'] = mcp_data except Exception: pass - + return self._config - + def _save_config(self): """Save configuration to file""" with self._lock: # Save main config (without mcp_servers) - config_to_save = {k: v for k, v in self._config.items() if k != 'mcp_servers'} + config_to_save = { + k: v + for k, v in self._config.items() if k != 'mcp_servers' + } with open(self.config_file, 'w', encoding='utf-8') as f: json.dump(config_to_save, f, indent=2) - + # Save MCP servers to separate file (compatible with ms-agent format) - mcp_data = {"mcpServers": self._config.get('mcp_servers', {})} + mcp_data = {'mcpServers': self._config.get('mcp_servers', {})} with open(self.mcp_file, 'w', encoding='utf-8') as f: json.dump(mcp_data, f, indent=2) - + def get_config(self) -> Dict[str, Any]: """Get the full configuration""" return self._load_config().copy() - + def update_config(self, config: Dict[str, Any]): """Update the full configuration""" self._load_config() self._config.update(config) self._save_config() - + def get_llm_config(self) -> Dict[str, Any]: """Get LLM configuration""" config = self._load_config() return config.get('llm', self.DEFAULT_CONFIG['llm']) - + def update_llm_config(self, llm_config: Dict[str, Any]): """Update LLM configuration""" self._load_config() self._config['llm'] = llm_config self._save_config() - + def get_mcp_config(self) -> Dict[str, Any]: """Get MCP servers configuration""" config = self._load_config() - return {"mcpServers": config.get('mcp_servers', {})} - + return {'mcpServers': config.get('mcp_servers', {})} + def update_mcp_config(self, mcp_config: Dict[str, Any]): """Update MCP servers configuration""" self._load_config() @@ -113,7 +117,7 @@ def update_mcp_config(self, mcp_config: Dict[str, Any]): else: self._config['mcp_servers'] = mcp_config self._save_config() - + def add_mcp_server(self, name: str, server_config: Dict[str, Any]): """Add a new MCP server""" self._load_config() @@ -121,7 +125,7 @@ def add_mcp_server(self, name: str, server_config: Dict[str, Any]): self._config['mcp_servers'] = {} self._config['mcp_servers'][name] = server_config self._save_config() - + def remove_mcp_server(self, name: str) -> bool: """Remove an MCP server""" self._load_config() @@ -130,18 +134,18 @@ def remove_mcp_server(self, name: str) -> bool: self._save_config() return True return False - + def get_mcp_file_path(self) -> str: """Get the path to the MCP servers file""" return self.mcp_file - + def get_env_vars(self) -> Dict[str, str]: """Get environment variables for running agents""" config = self._load_config() llm = config.get('llm', {}) - + env_vars = {} - + if llm.get('api_key'): provider = llm.get('provider', 'modelscope') if provider == 'modelscope': @@ -150,8 +154,8 @@ def get_env_vars(self) -> Dict[str, str]: env_vars['OPENAI_API_KEY'] = llm['api_key'] elif provider == 'anthropic': env_vars['ANTHROPIC_API_KEY'] = llm['api_key'] - + if llm.get('base_url'): env_vars['OPENAI_BASE_URL'] = llm['base_url'] - + return env_vars diff --git a/webui/backend/main.py b/webui/backend/main.py index ab3bdec2d..2afcebbe3 100644 --- a/webui/backend/main.py +++ b/webui/backend/main.py @@ -5,82 +5,81 @@ """ import os import sys + import uvicorn +from api import router as api_router from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse - -from api import router as api_router +from fastapi.staticfiles import StaticFiles from websocket_handler import router as ws_router # Add ms-agent to path -MS_AGENT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'ms-agent')) +MS_AGENT_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..', 'ms-agent')) if MS_AGENT_PATH not in sys.path: sys.path.insert(0, MS_AGENT_PATH) app = FastAPI( - title="MS-Agent Web UI", - description="Web interface for the MS-Agent framework", - version="1.0.0" -) + title='MS-Agent Web UI', + description='Web interface for the MS-Agent framework', + version='1.0.0') # CORS configuration app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=['*'], allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], + allow_methods=['*'], + allow_headers=['*'], ) # Include API and WebSocket routers -app.include_router(api_router, prefix="/api") -app.include_router(ws_router, prefix="/ws") +app.include_router(api_router, prefix='/api') +app.include_router(ws_router, prefix='/ws') # Serve static files in production STATIC_DIR = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'dist') if os.path.exists(STATIC_DIR): - app.mount("/assets", StaticFiles(directory=os.path.join(STATIC_DIR, "assets")), name="assets") - - @app.get("/{full_path:path}") + app.mount( + '/assets', + StaticFiles(directory=os.path.join(STATIC_DIR, 'assets')), + name='assets') + + @app.get('/{full_path:path}') async def serve_spa(full_path: str): """Serve the SPA for all non-API routes""" file_path = os.path.join(STATIC_DIR, full_path) if os.path.exists(file_path) and os.path.isfile(file_path): return FileResponse(file_path) - return FileResponse(os.path.join(STATIC_DIR, "index.html")) + return FileResponse(os.path.join(STATIC_DIR, 'index.html')) -@app.get("/health") +@app.get('/health') async def health_check(): """Health check endpoint""" - return {"status": "healthy", "service": "ms-agent-webui"} + return {'status': 'healthy', 'service': 'ms-agent-webui'} def main(): """Start the server""" import argparse - parser = argparse.ArgumentParser(description="MS-Agent Web UI Server") - parser.add_argument("--host", default="0.0.0.0", help="Host to bind") - parser.add_argument("--port", type=int, default=7860, help="Port to bind") - parser.add_argument("--reload", action="store_true", help="Enable auto-reload") + parser = argparse.ArgumentParser(description='MS-Agent Web UI Server') + parser.add_argument('--host', default='0.0.0.0', help='Host to bind') + parser.add_argument('--port', type=int, default=7860, help='Port to bind') + parser.add_argument( + '--reload', action='store_true', help='Enable auto-reload') args = parser.parse_args() - + print(f"\n{'='*60}") - print(" MS-Agent Web UI Server") + print(' MS-Agent Web UI Server') print(f"{'='*60}") - print(f" Server running at: http://{args.host}:{args.port}") - print(f" API documentation: http://{args.host}:{args.port}/docs") + print(f' Server running at: http://{args.host}:{args.port}') + print(f' API documentation: http://{args.host}:{args.port}/docs') print(f"{'='*60}\n") - - uvicorn.run( - "main:app", - host=args.host, - port=args.port, - reload=args.reload - ) + + uvicorn.run('main:app', host=args.host, port=args.port, reload=args.reload) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/webui/backend/project_discovery.py b/webui/backend/project_discovery.py index 24d923906..6a74da127 100644 --- a/webui/backend/project_discovery.py +++ b/webui/backend/project_discovery.py @@ -5,46 +5,48 @@ """ import os import re -from typing import Dict, List, Optional, Any +from typing import Any, Dict, List, Optional class ProjectDiscovery: """Discovers and manages projects from the ms-agent projects directory""" - + def __init__(self, projects_dir: str): self.projects_dir = projects_dir self._projects_cache: Optional[List[Dict[str, Any]]] = None - - def discover_projects(self, force_refresh: bool = False) -> List[Dict[str, Any]]: + + def discover_projects(self, + force_refresh: bool = False) -> List[Dict[str, Any]]: """Discover all available projects""" if self._projects_cache is not None and not force_refresh: return self._projects_cache - + projects = [] - + if not os.path.exists(self.projects_dir): return projects - + for item in os.listdir(self.projects_dir): item_path = os.path.join(self.projects_dir, item) if os.path.isdir(item_path) and not item.startswith('.'): project_info = self._analyze_project(item, item_path) if project_info: projects.append(project_info) - + # Sort by display name projects.sort(key=lambda x: x['display_name']) self._projects_cache = projects return projects - - def _analyze_project(self, name: str, path: str) -> Optional[Dict[str, Any]]: + + def _analyze_project(self, name: str, + path: str) -> Optional[Dict[str, Any]]: """Analyze a project directory and extract its information""" # Check for workflow.yaml or agent.yaml workflow_file = os.path.join(path, 'workflow.yaml') agent_file = os.path.join(path, 'agent.yaml') run_file = os.path.join(path, 'run.py') readme_file = os.path.join(path, 'README.md') - + # Determine project type if os.path.exists(workflow_file): project_type = 'workflow' @@ -58,12 +60,13 @@ def _analyze_project(self, name: str, path: str) -> Optional[Dict[str, Any]]: else: # Skip directories without valid config return None - + # Generate display name from directory name display_name = self._format_display_name(name) - + # Extract description from README if available - description = self._extract_description(readme_file) if os.path.exists(readme_file) else "" + description = self._extract_description(readme_file) if os.path.exists( + readme_file) else '' return { 'id': name, @@ -75,7 +78,7 @@ def _analyze_project(self, name: str, path: str) -> Optional[Dict[str, Any]]: 'has_readme': os.path.exists(readme_file), 'config_file': config_file } - + def _format_display_name(self, name: str) -> str: """Convert directory name to display name""" # Replace underscores with spaces and title case @@ -83,23 +86,24 @@ def _format_display_name(self, name: str) -> str: # Handle camelCase display = re.sub(r'([a-z])([A-Z])', r'\1 \2', display) return display.title() - + def _extract_description(self, readme_path: str) -> str: """Extract first paragraph from README as description""" try: with open(readme_path, 'r', encoding='utf-8') as f: content = f.read() - + # Skip title and find first paragraph lines = content.split('\n') description_lines = [] in_description = False - + for line in lines: stripped = line.strip() # Skip headers and empty lines at the beginning if not in_description: - if stripped and not stripped.startswith('#') and not stripped.startswith('['): + if stripped and not stripped.startswith( + '#') and not stripped.startswith('['): in_description = True description_lines.append(stripped) else: @@ -107,15 +111,15 @@ def _extract_description(self, readme_path: str) -> str: description_lines.append(stripped) elif not stripped and description_lines: break - + description = ' '.join(description_lines) # Truncate if too long if len(description) > 300: description = description[:297] + '...' return description except Exception: - return "" - + return '' + def get_project(self, project_id: str) -> Optional[Dict[str, Any]]: """Get a specific project by ID""" projects = self.discover_projects() @@ -123,26 +127,26 @@ def get_project(self, project_id: str) -> Optional[Dict[str, Any]]: if project['id'] == project_id: return project return None - + def get_project_readme(self, project_id: str) -> Optional[str]: """Get the README content for a project""" project = self.get_project(project_id) if not project or not project['has_readme']: return None - + readme_path = os.path.join(project['path'], 'README.md') try: with open(readme_path, 'r', encoding='utf-8') as f: return f.read() except Exception: return None - + def get_project_config(self, project_id: str) -> Optional[Dict[str, Any]]: """Get the configuration for a project""" project = self.get_project(project_id) if not project: return None - + try: import yaml with open(project['config_file'], 'r', encoding='utf-8') as f: diff --git a/webui/backend/session_manager.py b/webui/backend/session_manager.py index 48ff4cc2a..d588fbaf3 100644 --- a/webui/backend/session_manager.py +++ b/webui/backend/session_manager.py @@ -5,19 +5,20 @@ """ import uuid from datetime import datetime -from typing import Dict, List, Any, Optional from threading import Lock +from typing import Any, Dict, List, Optional class SessionManager: """Manages user sessions and their message history""" - + def __init__(self): self._sessions: Dict[str, Dict[str, Any]] = {} self._messages: Dict[str, List[Dict[str, Any]]] = {} self._lock = Lock() - - def create_session(self, project_id: str, project_name: str) -> Dict[str, Any]: + + def create_session(self, project_id: str, + project_name: str) -> Dict[str, Any]: """Create a new session""" session_id = str(uuid.uuid4()) session = { @@ -30,26 +31,26 @@ def create_session(self, project_id: str, project_name: str) -> Dict[str, Any]: 'file_progress': None, 'current_step': None } - + with self._lock: self._sessions[session_id] = session self._messages[session_id] = [] - + return session - + def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: """Get session by ID""" return self._sessions.get(session_id) - + def update_session(self, session_id: str, updates: Dict[str, Any]) -> bool: """Update session data""" if session_id not in self._sessions: return False - + with self._lock: self._sessions[session_id].update(updates) return True - + def delete_session(self, session_id: str) -> bool: """Delete a session""" with self._lock: @@ -59,17 +60,21 @@ def delete_session(self, session_id: str) -> bool: del self._messages[session_id] return True return False - + def list_sessions(self) -> List[Dict[str, Any]]: """List all sessions""" return list(self._sessions.values()) - - def add_message(self, session_id: str, role: str, content: str, - message_type: str = 'text', metadata: Dict[str, Any] = None) -> bool: + + def add_message(self, + session_id: str, + role: str, + content: str, + message_type: str = 'text', + metadata: Dict[str, Any] = None) -> bool: """Add a message to a session""" if session_id not in self._sessions: return False - + message = { 'id': str(uuid.uuid4()), 'role': role, # user, assistant, system, tool @@ -78,52 +83,54 @@ def add_message(self, session_id: str, role: str, content: str, 'timestamp': datetime.now().isoformat(), 'metadata': metadata or {} } - + with self._lock: if session_id not in self._messages: self._messages[session_id] = [] self._messages[session_id].append(message) - + return True - + def get_messages(self, session_id: str) -> Optional[List[Dict[str, Any]]]: """Get all messages for a session""" if session_id not in self._sessions: return None return self._messages.get(session_id, []) - + def update_last_message(self, session_id: str, content: str) -> bool: """Update the content of the last message (for streaming)""" if session_id not in self._messages or not self._messages[session_id]: return False - + with self._lock: self._messages[session_id][-1]['content'] = content return True - - def set_workflow_progress(self, session_id: str, progress: Dict[str, Any]) -> bool: + + def set_workflow_progress(self, session_id: str, + progress: Dict[str, Any]) -> bool: """Set workflow progress for a session""" if session_id not in self._sessions: return False - + with self._lock: self._sessions[session_id]['workflow_progress'] = progress return True - - def set_file_progress(self, session_id: str, progress: Dict[str, Any]) -> bool: + + def set_file_progress(self, session_id: str, progress: Dict[str, + Any]) -> bool: """Set file writing progress for a session""" if session_id not in self._sessions: return False - + with self._lock: self._sessions[session_id]['file_progress'] = progress return True - + def set_current_step(self, session_id: str, step: str) -> bool: """Set the current workflow step""" if session_id not in self._sessions: return False - + with self._lock: self._sessions[session_id]['current_step'] = step return True diff --git a/webui/backend/shared.py b/webui/backend/shared.py index cc109a544..08dbd5775 100644 --- a/webui/backend/shared.py +++ b/webui/backend/shared.py @@ -5,12 +5,13 @@ """ import os -from session_manager import SessionManager -from project_discovery import ProjectDiscovery from config_manager import ConfigManager +from project_discovery import ProjectDiscovery +from session_manager import SessionManager # Initialize paths -BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +BASE_DIR = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) PROJECTS_DIR = os.path.join(BASE_DIR, 'projects') CONFIG_DIR = os.path.join(BASE_DIR, 'webui', 'config') @@ -19,6 +20,6 @@ config_manager = ConfigManager(CONFIG_DIR) session_manager = SessionManager() -print(f"[Shared] Initialized managers") -print(f"[Shared] Projects dir: {PROJECTS_DIR}") -print(f"[Shared] Config dir: {CONFIG_DIR}") +print('[Shared] Initialized managers') +print(f'[Shared] Projects dir: {PROJECTS_DIR}') +print(f'[Shared] Config dir: {CONFIG_DIR}') diff --git a/webui/backend/websocket_handler.py b/webui/backend/websocket_handler.py index d363aa30e..649969171 100644 --- a/webui/backend/websocket_handler.py +++ b/webui/backend/websocket_handler.py @@ -3,38 +3,38 @@ WebSocket handler for real-time communication Handles agent execution, log streaming, and progress updates. """ -import os -import json import asyncio -from typing import Dict, Set, Any -from fastapi import APIRouter, WebSocket, WebSocketDisconnect +import os +from typing import Any, Dict, Set +import json from agent_runner import AgentRunner +from fastapi import APIRouter, WebSocket, WebSocketDisconnect # Import shared instances -from shared import project_discovery, config_manager, session_manager +from shared import config_manager, project_discovery, session_manager router = APIRouter() class ConnectionManager: """Manages WebSocket connections""" - + def __init__(self): self.active_connections: Dict[str, Set[WebSocket]] = {} self.log_connections: Set[WebSocket] = set() - + async def connect(self, websocket: WebSocket, session_id: str): """Connect a client to a session""" await websocket.accept() if session_id not in self.active_connections: self.active_connections[session_id] = set() self.active_connections[session_id].add(websocket) - + async def connect_logs(self, websocket: WebSocket): """Connect a client to log stream""" await websocket.accept() self.log_connections.add(websocket) - + def disconnect(self, websocket: WebSocket, session_id: str = None): """Disconnect a client""" if session_id and session_id in self.active_connections: @@ -42,7 +42,7 @@ def disconnect(self, websocket: WebSocket, session_id: str = None): if not self.active_connections[session_id]: del self.active_connections[session_id] self.log_connections.discard(websocket) - + async def send_to_session(self, session_id: str, message: Dict[str, Any]): """Send message to all clients in a session""" if session_id in self.active_connections: @@ -54,7 +54,7 @@ async def send_to_session(self, session_id: str, message: Dict[str, Any]): disconnected.add(connection) for conn in disconnected: self.active_connections[session_id].discard(conn) - + async def broadcast_log(self, log_entry: Dict[str, Any]): """Broadcast log entry to all log connections""" disconnected = set() @@ -72,20 +72,20 @@ async def broadcast_log(self, log_entry: Dict[str, Any]): agent_tasks: Dict[str, asyncio.Task] = {} -@router.websocket("/session/{session_id}") +@router.websocket('/session/{session_id}') async def websocket_session(websocket: WebSocket, session_id: str): """WebSocket endpoint for session communication""" - print(f"[WS] Client connecting to session: {session_id}") + print(f'[WS] Client connecting to session: {session_id}') await connection_manager.connect(websocket, session_id) - print(f"[WS] Client connected to session: {session_id}") - + print(f'[WS] Client connected to session: {session_id}') + try: while True: data = await websocket.receive_json() - print(f"[WS] Received message: {data}") + print(f'[WS] Received message: {data}') await handle_session_message(session_id, data, websocket) except WebSocketDisconnect: - print(f"[WS] Client disconnected from session: {session_id}") + print(f'[WS] Client disconnected from session: {session_id}') connection_manager.disconnect(websocket, session_id) # Stop agent if running if session_id in agent_runners: @@ -96,11 +96,11 @@ async def websocket_session(websocket: WebSocket, session_id: str): del agent_tasks[session_id] -@router.websocket("/logs") +@router.websocket('/logs') async def websocket_logs(websocket: WebSocket): """WebSocket endpoint for log streaming""" await connection_manager.connect_logs(websocket) - + try: while True: # Keep connection alive @@ -109,10 +109,11 @@ async def websocket_logs(websocket: WebSocket): connection_manager.disconnect(websocket) -async def handle_session_message(session_id: str, data: Dict[str, Any], websocket: WebSocket): +async def handle_session_message(session_id: str, data: Dict[str, Any], + websocket: WebSocket): """Handle incoming WebSocket messages""" action = data.get('action') - + if action == 'start': await start_agent(session_id, data, websocket) elif action == 'stop': @@ -123,19 +124,20 @@ async def handle_session_message(session_id: str, data: Dict[str, Any], websocke await send_status(session_id, websocket) -async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocket): +async def start_agent(session_id: str, data: Dict[str, Any], + websocket: WebSocket): """Start an agent for a session""" - print(f"[Agent] Starting agent for session: {session_id}") - + print(f'[Agent] Starting agent for session: {session_id}') + session = session_manager.get_session(session_id) if not session: - print(f"[Agent] ERROR: Session not found: {session_id}") + print(f'[Agent] ERROR: Session not found: {session_id}') await websocket.send_json({ 'type': 'error', 'message': 'Session not found' }) return - + project = project_discovery.get_project(session['project_id']) if not project: print(f"[Agent] ERROR: Project not found: {session['project_id']}") @@ -144,30 +146,36 @@ async def start_agent(session_id: str, data: Dict[str, Any], websocket: WebSocke 'message': 'Project not found' }) return - - print(f"[Agent] Project: {project['id']}, type: {project['type']}, config: {project['config_file']}") - + + print( + f"[Agent] Project: {project['id']}, type: {project['type']}, config: {project['config_file']}" + ) + query = data.get('query', '') - print(f"[Agent] Query: {query[:100]}..." if len(query) > 100 else f"[Agent] Query: {query}") - + print(f'[Agent] Query: {query[:100]}...' + if len(query) > 100 else f'[Agent] Query: {query}') + # Add user message to session (but don't broadcast - frontend already has it) session_manager.add_message(session_id, 'user', query, 'text') - + # Create agent runner runner = AgentRunner( session_id=session_id, project=project, config_manager=config_manager, - on_output=lambda msg: asyncio.create_task(on_agent_output(session_id, msg)), + on_output=lambda msg: asyncio.create_task( + on_agent_output(session_id, msg)), on_log=lambda log: asyncio.create_task(on_agent_log(session_id, log)), - on_progress=lambda prog: asyncio.create_task(on_agent_progress(session_id, prog)), - on_complete=lambda result: asyncio.create_task(on_agent_complete(session_id, result)), - on_error=lambda err: asyncio.create_task(on_agent_error(session_id, err)) - ) - + on_progress=lambda prog: asyncio.create_task( + on_agent_progress(session_id, prog)), + on_complete=lambda result: asyncio.create_task( + on_agent_complete(session_id, result)), + on_error=lambda err: asyncio.create_task( + on_agent_error(session_id, err))) + agent_runners[session_id] = runner session_manager.update_session(session_id, {'status': 'running'}) - + # Notify session started await connection_manager.send_to_session(session_id, { 'type': 'status', @@ -192,7 +200,7 @@ async def stop_agent(session_id: str): if session_id in agent_tasks: agent_tasks[session_id].cancel() del agent_tasks[session_id] - + session_manager.update_session(session_id, {'status': 'stopped'}) await connection_manager.send_to_session(session_id, { 'type': 'status', @@ -211,9 +219,12 @@ async def send_status(session_id: str, websocket: WebSocket): session = session_manager.get_session(session_id) if session: await websocket.send_json({ - 'type': 'status', - 'session': session, - 'messages': session_manager.get_messages(session_id) + 'type': + 'status', + 'session': + session, + 'messages': + session_manager.get_messages(session_id) }) @@ -222,25 +233,28 @@ async def on_agent_output(session_id: str, message: Dict[str, Any]): msg_type = message.get('type', 'text') content = message.get('content', '') role = message.get('role', 'assistant') - + if msg_type == 'stream': # Streaming update - await connection_manager.send_to_session(session_id, { - 'type': 'stream', - 'content': content, - 'done': message.get('done', False) - }) + await connection_manager.send_to_session( + session_id, { + 'type': 'stream', + 'content': content, + 'done': message.get('done', False) + }) if message.get('done'): session_manager.add_message(session_id, role, content, 'text') else: - session_manager.add_message(session_id, role, content, msg_type, message.get('metadata')) - await connection_manager.send_to_session(session_id, { - 'type': 'message', - 'role': role, - 'content': content, - 'message_type': msg_type, - 'metadata': message.get('metadata') - }) + session_manager.add_message(session_id, role, content, msg_type, + message.get('metadata')) + await connection_manager.send_to_session( + session_id, { + 'type': 'message', + 'role': role, + 'content': content, + 'message_type': msg_type, + 'metadata': message.get('metadata') + }) async def on_agent_log(session_id: str, log: Dict[str, Any]): @@ -249,22 +263,20 @@ async def on_agent_log(session_id: str, log: Dict[str, Any]): 'type': 'log', **log }) - await connection_manager.broadcast_log({ - 'session_id': session_id, - **log - }) + await connection_manager.broadcast_log({'session_id': session_id, **log}) async def on_agent_progress(session_id: str, progress: Dict[str, Any]): """Handle progress update""" progress_type = progress.get('type', 'workflow') - + if progress_type == 'workflow': session_manager.set_workflow_progress(session_id, progress) - session_manager.set_current_step(session_id, progress.get('current_step')) + session_manager.set_current_step(session_id, + progress.get('current_step')) elif progress_type == 'file': session_manager.set_file_progress(session_id, progress) - + await connection_manager.send_to_session(session_id, { 'type': 'progress', **progress @@ -274,13 +286,13 @@ async def on_agent_progress(session_id: str, progress: Dict[str, Any]): async def on_agent_complete(session_id: str, result: Dict[str, Any]): """Handle agent completion""" session_manager.update_session(session_id, {'status': 'completed'}) - + if session_id in agent_runners: del agent_runners[session_id] if session_id in agent_tasks: agent_tasks[session_id].cancel() del agent_tasks[session_id] - + await connection_manager.send_to_session(session_id, { 'type': 'complete', 'result': result @@ -290,14 +302,15 @@ async def on_agent_complete(session_id: str, result: Dict[str, Any]): async def on_agent_error(session_id: str, error: Dict[str, Any]): """Handle agent error""" session_manager.update_session(session_id, {'status': 'error'}) - session_manager.add_message(session_id, 'system', error.get('message', 'Unknown error'), 'error') - + session_manager.add_message(session_id, 'system', + error.get('message', 'Unknown error'), 'error') + if session_id in agent_runners: del agent_runners[session_id] if session_id in agent_tasks: agent_tasks[session_id].cancel() del agent_tasks[session_id] - + await connection_manager.send_to_session(session_id, { 'type': 'error', **error diff --git a/webui/config/mcp_servers.json b/webui/config/mcp_servers.json index 700113020..da39e4ffa 100644 --- a/webui/config/mcp_servers.json +++ b/webui/config/mcp_servers.json @@ -1,3 +1,3 @@ { "mcpServers": {} -} \ No newline at end of file +} diff --git a/webui/config/settings.json b/webui/config/settings.json index ebd5155b5..26d2bd591 100644 --- a/webui/config/settings.json +++ b/webui/config/settings.json @@ -9,4 +9,4 @@ }, "theme": "dark", "output_dir": "./output" -} \ No newline at end of file +} diff --git a/webui/frontend/src/App.tsx b/webui/frontend/src/App.tsx index 6d57fde85..c2b5a8cff 100644 --- a/webui/frontend/src/App.tsx +++ b/webui/frontend/src/App.tsx @@ -29,19 +29,19 @@ const App: React.FC = () => { {!currentSession ? ( ) : ( - )} - + {showSettings && ( setShowSettings(false)} + open={showSettings} + onClose={() => setShowSettings(false)} /> )} diff --git a/webui/frontend/src/components/ConversationView.tsx b/webui/frontend/src/components/ConversationView.tsx index 6c75e8598..99c4dcb94 100644 --- a/webui/frontend/src/components/ConversationView.tsx +++ b/webui/frontend/src/components/ConversationView.tsx @@ -204,26 +204,26 @@ const ConversationView: React.FC = ({ showLogs }) => { currentSession?.status === 'completed' ? 'success' : currentSession?.status === 'error' ? 'error' : 'default' } - sx={{ + sx={{ textTransform: 'capitalize', borderRadius: '8px', '& .MuiChip-icon': { ml: 0.5 }, }} /> - + {/* Workflow Progress */} {currentSession?.workflow_progress && ( )} - + {/* File Progress */} {currentSession?.file_progress && ( )} - + {/* View Output Files Button */} = ({ showLogs }) => { {/* File Tree */} - = ({ showLogs }) => { completedSteps={completedSteps} /> ))} - + {/* Streaming Content */} {isStreaming && streamingContent && ( = ({ showLogs }) => { /> )} - + {/* Loading Indicator */} {isLoading && !isStreaming && messages.length > 0 && (() => { // Find current running step const runningSteps = messages.filter(m => m.type === 'step_start'); const completedSteps = messages.filter(m => m.type === 'step_complete'); - const currentStep = runningSteps.length > completedSteps.length + const currentStep = runningSteps.length > completedSteps.length ? runningSteps[runningSteps.length - 1]?.content?.replace(/_/g, ' ') : null; - + return ( = ({ message, isStreaming, ses // Skip empty messages if (!message.content?.trim()) return null; - + // Skip old format system messages if (isSystem && message.content.startsWith('Starting step:')) return null; if (isSystem && message.content.startsWith('Completed step:')) return null; @@ -566,7 +566,7 @@ const MessageBubble: React.FC = ({ message, isStreaming, ses : isStopped ? theme.palette.warning.main : theme.palette.info.main; - + return ( = ({ message, isStreaming, ses }} > - + {isStreaming && ( = ({ return stripped.length > 0 ? stripped : name; }; const hasContent = Object.keys(tree.folders).length > 0 || tree.files.length > 0; - + if (!hasContent && depth === 0) { return ( @@ -765,14 +765,14 @@ const FileTreeView: React.FC = ({ ); } - + return ( <> {/* Folders */} {Object.entries(tree.folders).map(([folderName, subtree]) => { const folderPath = path ? `${path}/${folderName}` : folderName; const isExpanded = expandedFolders.has(folderPath); - + return ( = ({ ); })} - + {/* Files */} {tree.files.map((file) => ( = ({ filename }) => { setDialogOpen(true); setFileLoading(true); setFileError(null); - + try { const response = await fetch('/api/files/read', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: filename }), }); - + if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load file'); } - + const data = await response.json(); setFileContent(data.content); setFileLang(data.language || 'text'); @@ -934,7 +934,7 @@ const FileOutputChip: React.FC<{ filename: string }> = ({ filename }) => { - + {/* File Viewer Dialog */} = ({ progress }) => { const theme = useTheme(); const { file, status } = progress; const isWriting = status === 'writing'; - + // Extract filename from path const filename = file.split('/').pop() || file; diff --git a/webui/frontend/src/components/LogViewer.tsx b/webui/frontend/src/components/LogViewer.tsx index d4746e3a3..9961ddf67 100644 --- a/webui/frontend/src/components/LogViewer.tsx +++ b/webui/frontend/src/components/LogViewer.tsx @@ -50,10 +50,10 @@ const LogViewer: React.FC = ({ logs, onClear }) => { }; const handleDownload = () => { - const content = logs.map((log) => + const content = logs.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}` ).join('\n'); - + const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -86,13 +86,13 @@ const LogViewer: React.FC = ({ logs, onClear }) => { Logs - + - + {onClear && ( @@ -119,7 +119,7 @@ const LogViewer: React.FC = ({ logs, onClear }) => { ), }} /> - + {['info', 'warning', 'error', 'debug'].map((level) => ( = ({ content }) => { code({ node, inline, className, children, ...props }: any) { const match = /language-(\w+)/.exec(className || ''); const language = match ? match[1] : ''; - + if (!inline && language) { return ( diff --git a/webui/frontend/src/components/SearchView.tsx b/webui/frontend/src/components/SearchView.tsx index 5f611cc53..ec724eafa 100644 --- a/webui/frontend/src/components/SearchView.tsx +++ b/webui/frontend/src/components/SearchView.tsx @@ -48,7 +48,7 @@ const SearchView: React.FC = () => { const handleSubmit = useCallback(async () => { if (!selectedProject || !query.trim()) return; - + console.log('[SearchView] Submitting with project:', selectedProject.id, 'query:', query); setIsSubmitting(true); try { @@ -194,7 +194,7 @@ const SearchView: React.FC = () => { ), }} /> - + {/* Selected Project Badge */} {selectedProject && ( @@ -243,7 +243,7 @@ const SearchView: React.FC = () => { > Select a Project - + {projects.map((project, index) => ( @@ -310,13 +310,13 @@ const SearchView: React.FC = () => { height: 20, fontSize: '0.65rem', backgroundColor: alpha( - project.type === 'workflow' - ? theme.palette.info.main + project.type === 'workflow' + ? theme.palette.info.main : theme.palette.success.main, 0.1 ), - color: project.type === 'workflow' - ? theme.palette.info.main + color: project.type === 'workflow' + ? theme.palette.info.main : theme.palette.success.main, }} /> diff --git a/webui/frontend/src/components/SettingsDialog.tsx b/webui/frontend/src/components/SettingsDialog.tsx index ad4a8b747..6a273be81 100644 --- a/webui/frontend/src/components/SettingsDialog.tsx +++ b/webui/frontend/src/components/SettingsDialog.tsx @@ -92,12 +92,12 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { fetch('/api/config/llm'), fetch('/api/config/mcp'), ]); - + if (llmRes.ok) { const data = await llmRes.json(); setLlmConfig(data); } - + if (mcpRes.ok) { const data = await mcpRes.json(); setMcpServers(data.mcpServers || {}); @@ -115,13 +115,13 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(llmConfig), }); - + const mcpRes = await fetch('/api/config/mcp', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mcpServers: mcpServers }), }); - + if (llmRes.ok && mcpRes.ok) { setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 2000); @@ -135,7 +135,7 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { const handleAddMCPServer = () => { if (!newServerName.trim()) return; - + setMcpServers((prev) => ({ ...prev, [newServerName]: { type: 'sse', url: '' }, @@ -193,9 +193,9 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { - + - + = ({ open, onClose }) => { Configure MCP (Model Context Protocol) servers to extend agent capabilities with additional tools. - + {/* Add new server */} = ({ open, onClose }) => { - + - + {saveStatus === 'saved' && ( diff --git a/webui/frontend/src/components/WorkflowProgress.tsx b/webui/frontend/src/components/WorkflowProgress.tsx index 84f705518..d863337b1 100644 --- a/webui/frontend/src/components/WorkflowProgress.tsx +++ b/webui/frontend/src/components/WorkflowProgress.tsx @@ -11,7 +11,7 @@ interface WorkflowProgressProps { const WorkflowProgress: React.FC = ({ progress }) => { const theme = useTheme(); const { steps, step_status } = progress; - + // Calculate progress based on completed steps const completedCount = steps.filter(s => step_status[s] === 'completed').length; const runningCount = steps.filter(s => step_status[s] === 'running').length; @@ -22,13 +22,13 @@ const WorkflowProgress: React.FC = ({ progress }) => { Workflow: - + {steps.map((step, index) => { const status = step_status[step] || 'pending'; const isCompleted = status === 'completed'; const isCurrent = status === 'running'; - + return ( = ({ progress }) => { ); })} - + = ({ children }) headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: projectId }), }); - + if (response.ok) { const session = await response.json(); setSessions(prev => [...prev, session]); @@ -149,14 +149,14 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) } const socket = new WebSocket(`${WS_BASE}/session/${sessionId}`); - + socket.onopen = () => { console.log('WebSocket connected'); // Send pending query if exists if (pendingQueryRef.current && socket.readyState === WebSocket.OPEN) { const query = pendingQueryRef.current; pendingQueryRef.current = null; - + // Add user message locally setMessages(prev => [...prev, { id: Date.now().toString(), @@ -165,36 +165,36 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) type: 'text', timestamp: new Date().toISOString(), }]); - + socket.send(JSON.stringify({ action: 'start', query: query, })); - + setIsLoading(true); } }; - + socket.onmessage = (event) => { const data = JSON.parse(event.data); handleWebSocketMessage(data); }; - + socket.onclose = () => { console.log('WebSocket disconnected'); }; - + socket.onerror = (error) => { console.error('WebSocket error:', error); }; - + setWs(socket); }, [ws]); // Handle WebSocket messages const handleWebSocketMessage = useCallback((data: Record) => { const type = data.type as string; - + switch (type) { case 'message': setMessages(prev => [...prev, { @@ -206,7 +206,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) metadata: data.metadata as Record, }]); break; - + case 'stream': setStreamingContent(data.content as string); setIsStreaming(!data.done); @@ -221,7 +221,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) setStreamingContent(''); } break; - + case 'log': setLogs(prev => [...prev, { level: data.level as LogEntry['level'], @@ -230,11 +230,11 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) session_id: currentSession?.id, }]); break; - + case 'progress': setCurrentSession(prev => { if (!prev) return prev; - + const progressType = data.type as string; if (progressType === 'workflow') { return { @@ -258,7 +258,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) return prev; }); break; - + case 'status': { const nextStatus = (data.status as Session['status'] | undefined) ?? ((data as any)?.session?.status as Session['status'] | undefined); @@ -279,7 +279,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) } } break; - + case 'complete': setCurrentSession(prev => { if (!prev) return prev; @@ -288,7 +288,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) setSessions(prev => prev.map(s => (s.id === currentSession?.id ? { ...s, status: 'completed' } : s))); setIsLoading(false); break; - + case 'error': setCurrentSession(prev => { if (!prev) return prev; @@ -326,7 +326,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) // Send message const sendMessage = useCallback((content: string) => { if (!currentSession || !ws || ws.readyState !== WebSocket.OPEN) return; - + // Add user message locally setMessages(prev => [...prev, { id: Date.now().toString(), @@ -335,13 +335,13 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) type: 'text', timestamp: new Date().toISOString(), }]); - + // Send to server ws.send(JSON.stringify({ action: 'start', query: content, })); - + setIsLoading(true); }, [currentSession, ws]); @@ -350,7 +350,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'stop' })); } - + // Optimistic UI update: reflect stop immediately without waiting for backend setCurrentSession(prev => { if (!prev) return prev; diff --git a/webui/frontend/src/context/ThemeContext.tsx b/webui/frontend/src/context/ThemeContext.tsx index 4bc51ee8d..dac05dc7a 100644 --- a/webui/frontend/src/context/ThemeContext.tsx +++ b/webui/frontend/src/context/ThemeContext.tsx @@ -92,7 +92,7 @@ const lightPalette = { const createAppTheme = (mode: PaletteMode): Theme => { const palette = mode === 'dark' ? darkPalette : lightPalette; - + return createTheme({ palette: { mode, @@ -176,7 +176,7 @@ const createAppTheme = (mode: PaletteMode): Theme => { backgroundImage: 'none', }, elevation1: { - boxShadow: mode === 'dark' + boxShadow: mode === 'dark' ? '0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2)' : '0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04)', }, @@ -256,15 +256,15 @@ export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) = const stored = localStorage.getItem('theme-mode'); return (stored as PaletteMode) || 'dark'; }); - + const theme = useMemo(() => createAppTheme(mode), [mode]); - + const toggleTheme = () => { const newMode = mode === 'dark' ? 'light' : 'dark'; setMode(newMode); localStorage.setItem('theme-mode', newMode); }; - + return ( {children} diff --git a/webui/frontend/src/main.tsx b/webui/frontend/src/main.tsx index 692912fa6..7fd1eb284 100644 --- a/webui/frontend/src/main.tsx +++ b/webui/frontend/src/main.tsx @@ -7,7 +7,7 @@ import { SessionProvider } from './context/SessionContext' const ThemedApp: React.FC = () => { const { theme } = useThemeContext(); - + return ( diff --git a/webui/requirements.txt b/webui/requirements.txt index 038d28eca..c8aa95ee8 100644 --- a/webui/requirements.txt +++ b/webui/requirements.txt @@ -1,3 +1,3 @@ +aiohttp ms-agent pandas -aiohttp \ No newline at end of file diff --git a/webui/start.sh b/webui/start.sh index 0d5df3e1e..bf8a38336 100755 --- a/webui/start.sh +++ b/webui/start.sh @@ -143,7 +143,7 @@ if [ "$MODE" = "dev" ]; then npm run dev & FRONTEND_PID=$! cd "$SCRIPT_DIR" - + echo -e "\n${GREEN}✓ Development servers are running!${NC}" echo -e " Backend: ${BLUE}http://localhost:$PORT${NC}" echo -e " Frontend: ${BLUE}http://localhost:5173${NC}"