diff --git a/app.py b/app.py
new file mode 100644
index 0000000..fba1d83
--- /dev/null
+++ b/app.py
@@ -0,0 +1,247 @@
+from fastapi import FastAPI, Request, Body, HTTPException
+from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from datetime import datetime
+import asyncio
+import uuid
+from json import dumps
+
+app = FastAPI()
+
+app.mount("/static", StaticFiles(directory="static"), name="static")
+templates = Jinja2Templates(directory="templates")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+class Task(BaseModel):
+ id: str
+ prompt: str
+ created_at: datetime
+ status: str
+ steps: list = []
+
+ def model_dump(self, *args, **kwargs):
+ data = super().model_dump(*args, **kwargs)
+ data['created_at'] = self.created_at.isoformat()
+ return data
+
+class TaskManager:
+ def __init__(self):
+ self.tasks = {}
+ self.queues = {}
+
+ def create_task(self, prompt: str) -> Task:
+ task_id = str(uuid.uuid4())
+ task = Task(
+ id=task_id,
+ prompt=prompt,
+ created_at=datetime.now(),
+ status="pending"
+ )
+ self.tasks[task_id] = task
+ self.queues[task_id] = asyncio.Queue()
+ return task
+
+ async def update_task_step(self, task_id: str, step: int, result: str, step_type: str = "step"):
+ if task_id in self.tasks:
+ task = self.tasks[task_id]
+ task.steps.append({"step": step, "result": result, "type": step_type})
+ await self.queues[task_id].put({
+ "type": step_type,
+ "step": step,
+ "result": result
+ })
+ await self.queues[task_id].put({
+ "type": "status",
+ "status": task.status,
+ "steps": task.steps
+ })
+
+ async def complete_task(self, task_id: str):
+ if task_id in self.tasks:
+ task = self.tasks[task_id]
+ task.status = "completed"
+ await self.queues[task_id].put({
+ "type": "status",
+ "status": task.status,
+ "steps": task.steps
+ })
+ await self.queues[task_id].put({"type": "complete"})
+
+ async def fail_task(self, task_id: str, error: str):
+ if task_id in self.tasks:
+ self.tasks[task_id].status = f"failed: {error}"
+ await self.queues[task_id].put({
+ "type": "error",
+ "message": error
+ })
+
+task_manager = TaskManager()
+
+@app.get("/", response_class=HTMLResponse)
+async def index(request: Request):
+ return templates.TemplateResponse("index.html", {"request": request})
+
+@app.post("/tasks")
+async def create_task(prompt: str = Body(..., embed=True)):
+ task = task_manager.create_task(prompt)
+ asyncio.create_task(run_task(task.id, prompt))
+ return {"task_id": task.id}
+
+from app.agent.toolcall import ToolCallAgent
+
+async def run_task(task_id: str, prompt: str):
+ try:
+ task_manager.tasks[task_id].status = "running"
+
+ agent = ToolCallAgent(
+ name="TaskAgent",
+ description="Agent for handling task execution",
+ max_steps=30
+ )
+
+ async def on_think(thought):
+ await task_manager.update_task_step(task_id, 0, thought, "think")
+
+ async def on_tool_execute(tool, input):
+ await task_manager.update_task_step(task_id, 0, f"执行工具: {tool}\n输入: {input}", "tool")
+
+ async def on_action(action):
+ await task_manager.update_task_step(task_id, 0, f"执行动作: {action}", "act")
+
+ async def on_run(step, result):
+ await task_manager.update_task_step(task_id, step, result, "run")
+
+ from app.logger import logger
+
+ class SSELogHandler:
+ def __init__(self, task_id):
+ self.task_id = task_id
+
+ async def __call__(self, message):
+ import re
+ # 提取 - 后面的内容
+ cleaned_message = re.sub(r'^.*? - ', '', message)
+
+ event_type = "log"
+ if "✨ TaskAgent's thoughts:" in cleaned_message:
+ event_type = "think"
+ elif "🛠️ TaskAgent selected" in cleaned_message:
+ event_type = "tool"
+ elif "🎯 Tool" in cleaned_message:
+ event_type = "act"
+ elif "📝 Oops!" in cleaned_message:
+ event_type = "error"
+ elif "🏁 Special tool" in cleaned_message:
+ event_type = "complete"
+
+ await task_manager.update_task_step(self.task_id, 0, cleaned_message, event_type)
+
+ sse_handler = SSELogHandler(task_id)
+ logger.add(sse_handler)
+
+ result = await agent.run(prompt)
+ await task_manager.update_task_step(task_id, 1, result, "result")
+ await task_manager.complete_task(task_id)
+ except Exception as e:
+ await task_manager.fail_task(task_id, str(e))
+
+@app.get("/tasks/{task_id}/events")
+async def task_events(task_id: str):
+ async def event_generator():
+ if task_id not in task_manager.queues:
+ yield f"event: error\ndata: {dumps({'message': 'Task not found'})}\n\n"
+ return
+
+ queue = task_manager.queues[task_id]
+
+ task = task_manager.tasks.get(task_id)
+ if task:
+ yield f"event: status\ndata: {dumps({
+ 'type': 'status',
+ 'status': task.status,
+ 'steps': task.steps
+ })}\n\n"
+
+ while True:
+ try:
+ event = await queue.get()
+ formatted_event = dumps(event)
+
+ yield ": heartbeat\n\n"
+
+ if event["type"] == "complete":
+ yield f"event: complete\ndata: {formatted_event}\n\n"
+ break
+ elif event["type"] == "error":
+ yield f"event: error\ndata: {formatted_event}\n\n"
+ break
+ elif event["type"] == "step":
+ task = task_manager.tasks.get(task_id)
+ if task:
+ yield f"event: status\ndata: {dumps({
+ 'type': 'status',
+ 'status': task.status,
+ 'steps': task.steps
+ })}\n\n"
+ yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
+ elif event["type"] in ["think", "tool", "act", "run"]:
+ yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
+ else:
+ yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
+
+ except asyncio.CancelledError:
+ print(f"Client disconnected for task {task_id}")
+ break
+ except Exception as e:
+ print(f"Error in event stream: {str(e)}")
+ yield f"event: error\ndata: {dumps({'message': str(e)})}\n\n"
+ break
+
+ return StreamingResponse(
+ event_generator(),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no"
+ }
+ )
+
+@app.get("/tasks")
+async def get_tasks():
+ sorted_tasks = sorted(
+ task_manager.tasks.values(),
+ key=lambda task: task.created_at,
+ reverse=True
+ )
+ return JSONResponse(
+ content=[task.model_dump() for task in sorted_tasks],
+ headers={"Content-Type": "application/json"}
+ )
+
+@app.get("/tasks/{task_id}")
+async def get_task(task_id: str):
+ if task_id not in task_manager.tasks:
+ raise HTTPException(status_code=404, detail="Task not found")
+ return task_manager.tasks[task_id]
+
+@app.exception_handler(Exception)
+async def generic_exception_handler(request: Request, exc: Exception):
+ return JSONResponse(
+ status_code=500,
+ content={"message": f"服务器内部错误: {str(exc)}"}
+ )
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/run.bat b/run.bat
new file mode 100644
index 0000000..4c48f2f
--- /dev/null
+++ b/run.bat
@@ -0,0 +1,3 @@
+@echo off
+venv\Scripts\python.exe app.py
+pause
diff --git a/static/main.js b/static/main.js
new file mode 100644
index 0000000..fbf61ff
--- /dev/null
+++ b/static/main.js
@@ -0,0 +1,585 @@
+let currentEventSource = null;
+
+function createTask() {
+ const promptInput = document.getElementById('prompt-input');
+ const prompt = promptInput.value.trim();
+
+ if (!prompt) {
+ alert("请输入有效的提示内容");
+ promptInput.focus();
+ return;
+ }
+
+ if (currentEventSource) {
+ currentEventSource.close();
+ currentEventSource = null;
+ }
+
+ const container = document.getElementById('task-container');
+ container.innerHTML = '
任务初始化中...
';
+ document.getElementById('input-container').classList.add('bottom');
+
+ fetch('/tasks', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ prompt })
+ })
+ .then(response => {
+ if (!response.ok) {
+ return response.json().then(err => { throw new Error(err.detail || '请求失败') });
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (!data.task_id) {
+ throw new Error('无效的任务ID');
+ }
+ setupSSE(data.task_id);
+ loadHistory();
+ })
+ .catch(error => {
+ container.innerHTML = `错误: ${error.message}
`;
+ console.error('创建任务失败:', error);
+ });
+}
+
+function setupSSE(taskId) {
+ let retryCount = 0;
+ const maxRetries = 3;
+ const retryDelay = 2000;
+
+ function connect() {
+ const eventSource = new EventSource(`/tasks/${taskId}/events`);
+ currentEventSource = eventSource;
+
+ const container = document.getElementById('task-container');
+
+ let heartbeatTimer = setInterval(() => {
+ container.innerHTML += '·
';
+ }, 5000);
+
+ const pollInterval = setInterval(() => {
+ fetch(`/tasks/${taskId}`)
+ .then(response => response.json())
+ .then(task => {
+ updateTaskStatus(task);
+ })
+ .catch(error => {
+ console.error('轮询失败:', error);
+ });
+ }, 10000);
+
+ if (!eventSource._listenersAdded) {
+ eventSource._listenersAdded = true;
+
+ let lastResultContent = '';
+ eventSource.addEventListener('status', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+ container.classList.add('active');
+ const welcomeMessage = document.querySelector('.welcome-message');
+ if (welcomeMessage) {
+ welcomeMessage.style.display = 'none';
+ }
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ // 保存result内容
+ if (data.steps && data.steps.length > 0) {
+ // 遍历所有步骤,找到最后一个result类型
+ for (let i = data.steps.length - 1; i >= 0; i--) {
+ if (data.steps[i].type === 'result') {
+ lastResultContent = data.steps[i].result;
+ break;
+ }
+ }
+ }
+
+ // Parse and display each step with proper formatting
+ stepContainer.innerHTML = data.steps.map(step => {
+ const content = step.result;
+ const timestamp = new Date().toLocaleTimeString();
+ return `
+
+
+
${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}:
+
${content}
+
+
+ `;
+ }).join('');
+
+ // Auto-scroll to bottom
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+ } catch (e) {
+ console.error('状态更新失败:', e);
+ }
+ });
+
+ // 添加对think事件的处理
+ eventSource.addEventListener('think', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ const content = data.result;
+ const timestamp = new Date().toLocaleTimeString();
+
+ const step = document.createElement('div');
+ step.className = 'step-item think';
+ step.innerHTML = `
+
+
${getEventIcon('think')} [${timestamp}] ${getEventLabel('think')}:
+
${content}
+
+ `;
+
+ stepContainer.appendChild(step);
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+
+ // 更新任务状态
+ fetch(`/tasks/${taskId}`)
+ .then(response => response.json())
+ .then(task => {
+ updateTaskStatus(task);
+ })
+ .catch(error => {
+ console.error('状态更新失败:', error);
+ });
+ } catch (e) {
+ console.error('思考事件处理失败:', e);
+ }
+ });
+
+ // 添加对tool事件的处理
+ eventSource.addEventListener('tool', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ const content = data.result;
+ const timestamp = new Date().toLocaleTimeString();
+
+ const step = document.createElement('div');
+ step.className = 'step-item tool';
+ step.innerHTML = `
+
+
${getEventIcon('tool')} [${timestamp}] ${getEventLabel('tool')}:
+
${content}
+
+ `;
+
+ stepContainer.appendChild(step);
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+
+ // 更新任务状态
+ fetch(`/tasks/${taskId}`)
+ .then(response => response.json())
+ .then(task => {
+ updateTaskStatus(task);
+ })
+ .catch(error => {
+ console.error('状态更新失败:', error);
+ });
+ } catch (e) {
+ console.error('工具事件处理失败:', e);
+ }
+ });
+
+ // 添加对act事件的处理
+ eventSource.addEventListener('act', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ const content = data.result;
+ const timestamp = new Date().toLocaleTimeString();
+
+ const step = document.createElement('div');
+ step.className = 'step-item act';
+ step.innerHTML = `
+
+
${getEventIcon('act')} [${timestamp}] ${getEventLabel('act')}:
+
${content}
+
+ `;
+
+ stepContainer.appendChild(step);
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+
+ // 更新任务状态
+ fetch(`/tasks/${taskId}`)
+ .then(response => response.json())
+ .then(task => {
+ updateTaskStatus(task);
+ })
+ .catch(error => {
+ console.error('状态更新失败:', error);
+ });
+ } catch (e) {
+ console.error('执行事件处理失败:', e);
+ }
+ });
+
+ // 添加对run事件的处理
+ // 添加对log事件的处理
+ eventSource.addEventListener('log', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ const content = data.result;
+ const timestamp = new Date().toLocaleTimeString();
+
+ const step = document.createElement('div');
+ step.className = 'step-item log';
+ step.innerHTML = `
+
+
${getEventIcon('log')} [${timestamp}] ${getEventLabel('log')}:
+
${content}
+
+ `;
+
+ stepContainer.appendChild(step);
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+
+ // 更新任务状态
+ fetch(`/tasks/${taskId}`)
+ .then(response => response.json())
+ .then(task => {
+ updateTaskStatus(task);
+ })
+ .catch(error => {
+ console.error('状态更新失败:', error);
+ });
+ } catch (e) {
+ console.error('日志事件处理失败:', e);
+ }
+ });
+
+ eventSource.addEventListener('run', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ const content = data.result;
+ const timestamp = new Date().toLocaleTimeString();
+
+ const step = document.createElement('div');
+ step.className = 'step-item run';
+ step.innerHTML = `
+
+
${getEventIcon('run')} [${timestamp}] ${getEventLabel('run')}:
+
${content}
+
+ `;
+
+ stepContainer.appendChild(step);
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+
+ // 更新任务状态
+ fetch(`/tasks/${taskId}`)
+ .then(response => response.json())
+ .then(task => {
+ updateTaskStatus(task);
+ })
+ .catch(error => {
+ console.error('状态更新失败:', error);
+ });
+ } catch (e) {
+ console.error('运行事件处理失败:', e);
+ }
+ });
+
+ eventSource.addEventListener('message', (event) => {
+ clearInterval(heartbeatTimer);
+ try {
+ const data = JSON.parse(event.data);
+ container.querySelector('.loading')?.remove();
+
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ stepContainer = container.querySelector('.step-container');
+ }
+
+ // Create new step element
+ const step = document.createElement('div');
+ step.className = `step-item ${data.type || 'step'}`;
+
+ // Format content and timestamp
+ const content = data.result;
+ const timestamp = new Date().toLocaleTimeString();
+
+ step.innerHTML = `
+
+
${getEventIcon(data.type)} [${timestamp}] ${getEventLabel(data.type)}:
+
${content}
+
+ `;
+
+ // Add step to container with animation
+ stepContainer.prepend(step);
+ setTimeout(() => {
+ step.classList.add('show');
+ }, 10);
+
+ // Auto-scroll to bottom
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+ } catch (e) {
+ console.error('消息处理失败:', e);
+ }
+ });
+
+ let isTaskComplete = false;
+
+ eventSource.addEventListener('complete', (event) => {
+ isTaskComplete = true;
+ clearInterval(heartbeatTimer);
+ clearInterval(pollInterval);
+ container.innerHTML += `
+
+
✅ 任务完成
+
${lastResultContent}
+
+ `;
+ eventSource.close();
+ currentEventSource = null;
+ lastResultContent = ''; // 清空结果内容
+ });
+
+ eventSource.addEventListener('error', (event) => {
+ clearInterval(heartbeatTimer);
+ clearInterval(pollInterval);
+ try {
+ const data = JSON.parse(event.data);
+ container.innerHTML += `
+
+ ❌ 错误: ${data.message}
+
+ `;
+ eventSource.close();
+ currentEventSource = null;
+ } catch (e) {
+ console.error('错误处理失败:', e);
+ }
+ });
+ }
+
+ container.scrollTo({
+ top: container.scrollHeight,
+ behavior: 'smooth'
+ });
+
+ eventSource.onerror = (err) => {
+ if (isTaskComplete) {
+ return;
+ }
+
+ console.error('SSE连接错误:', err);
+ clearInterval(heartbeatTimer);
+ clearInterval(pollInterval);
+ eventSource.close();
+
+ if (retryCount < maxRetries) {
+ retryCount++;
+ container.innerHTML += `
+
+ ⚠ 连接中断,${retryDelay/1000}秒后重试 (${retryCount}/${maxRetries})...
+
+ `;
+ setTimeout(connect, retryDelay);
+ } else {
+ container.innerHTML += `
+
+ ⚠ 连接中断,请尝试刷新页面
+
+ `;
+ }
+ };
+ }
+
+ connect();
+}
+
+function getEventIcon(eventType) {
+ switch(eventType) {
+ case 'think': return '🤔';
+ case 'tool': return '🛠️';
+ case 'act': return '🚀';
+ case 'result': return '🏁';
+ case 'error': return '❌';
+ case 'complete': return '✅';
+ case 'warning': return '⚠️';
+ case 'log': return '📝';
+ default: return '⚡';
+ }
+}
+
+function getEventLabel(eventType) {
+ switch(eventType) {
+ case 'think': return '思考';
+ case 'tool': return '工具执行';
+ case 'act': return '执行';
+ case 'result': return '结果';
+ case 'error': return '错误';
+ case 'complete': return '完成';
+ case 'warning': return '警告';
+ case 'log': return '日志';
+ default: return '步骤';
+ }
+}
+
+function formatContent(content) {
+ // Remove timestamp and log level prefixes
+ content = content.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} \| [A-Z]+\s*\| /gm, '');
+ // Format the remaining content
+ return content
+ .replace(/\n/g, '
')
+ .replace(/ /g, ' ')
+ .replace(/✨ Manus's thoughts:/g, '')
+ .replace(/🛠️ Manus selected/g, '')
+ .replace(/🧰 Tools being prepared:/g, '')
+ .replace(/🔧 Activating tool:/g, '')
+ .replace(/🎯 Tool/g, '')
+ .replace(/📝 Oops!/g, '')
+ .replace(/🏁 Special tool/g, '');
+}
+
+function updateTaskStatus(task) {
+ const taskCard = document.querySelector(`.task-card[data-task-id="${task.id}"]`);
+ if (taskCard) {
+ const statusEl = taskCard.querySelector('.task-meta .status');
+ if (statusEl) {
+ statusEl.className = `status-${task.status ? task.status.toLowerCase() : 'unknown'}`;
+ statusEl.textContent = task.status || '未知状态';
+ }
+ }
+}
+
+function loadHistory() {
+ fetch('/tasks')
+ .then(response => {
+ if (!response.ok) {
+ return response.text().then(text => {
+ throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`);
+ });
+ }
+ return response.json();
+ })
+ .then(tasks => {
+ const listContainer = document.getElementById('task-list');
+ listContainer.innerHTML = tasks.map(task => `
+
+
${task.prompt}
+
+ ${new Date(task.created_at).toLocaleString()} -
+
+ ${task.status || '未知状态'}
+
+
+
+ `).join('');
+ })
+ .catch(error => {
+ console.error('加载历史记录失败:', error);
+ const listContainer = document.getElementById('task-list');
+ listContainer.innerHTML = `加载失败: ${error.message}
`;
+ });
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ const welcomeMessage = document.querySelector('.welcome-message');
+ if (welcomeMessage) {
+ welcomeMessage.style.display = 'flex';
+ }
+
+ // 监听任务容器显示状态
+ const taskContainer = document.getElementById('task-container');
+ if (taskContainer) {
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach(mutation => {
+ if (mutation.attributeName === 'class') {
+ const welcomeMessage = document.querySelector('.welcome-message');
+ if (taskContainer.classList.contains('active')) {
+ if (welcomeMessage) {
+ welcomeMessage.style.display = 'none';
+ }
+ } else {
+ if (welcomeMessage) {
+ welcomeMessage.style.display = 'block';
+ }
+ }
+ }
+ });
+ });
+
+ observer.observe(taskContainer, {
+ attributes: true
+ });
+ }
+});
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..a1173fb
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,338 @@
+:root {
+ --primary-color: #007bff;
+ --primary-hover: #0056b3;
+ --success-color: #28a745;
+ --error-color: #dc3545;
+ --warning-color: #ff9800;
+ --info-color: #2196f3;
+ --text-color: #333;
+ --text-light: #666;
+ --bg-color: #f8f9fa;
+ --border-color: #ddd;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+}
+
+.container {
+ display: flex;
+ min-height: 100vh;
+ width: 90%;
+ margin: 0 auto;
+ padding: 20px;
+ gap: 20px;
+}
+
+.card {
+ background-color: white;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.history-panel {
+ width: 300px;
+}
+
+.main-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.task-list {
+ margin-top: 10px;
+ max-height: calc(100vh - 160px);
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.task-container {
+ @extend .card;
+ width: 100%;
+ position: relative;
+ min-height: 300px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ overflow: auto;
+ height: 100%;
+}
+
+.welcome-message {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ color: var(--text-light);
+ background: white;
+ z-index: 1;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ width: 100%;
+ height: 100%;
+}
+
+.welcome-message h1 {
+ font-size: 2rem;
+ margin-bottom: 10px;
+ color: var(--text-color);
+}
+
+.input-container {
+ @extend .card;
+ display: flex;
+ gap: 10px;
+}
+
+#prompt-input {
+ flex: 1;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 1rem;
+}
+
+button {
+ padding: 12px 24px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background-color 0.2s;
+}
+
+button:hover {
+ background-color: var(--primary-hover);
+}
+
+.task-item {
+ padding: 10px;
+ margin-bottom: 10px;
+ background-color: #f8f9fa;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.task-item:hover {
+ background-color: #e9ecef;
+}
+
+.task-item.active {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+#input-container.bottom {
+ margin-top: auto;
+}
+
+.task-card {
+ background: #fff;
+ padding: 15px;
+ margin-bottom: 10px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.task-card:hover {
+ transform: translateX(5px);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.status-pending {
+ color: var(--text-light);
+}
+
+.status-running {
+ color: var(--primary-color);
+}
+
+.status-completed {
+ color: var(--success-color);
+}
+
+.status-failed {
+ color: var(--error-color);
+}
+
+.step-container {
+ display: block;
+ padding: 15px;
+ width: 100%;
+ max-height: calc(100vh - 300px);
+ overflow-y: auto;
+}
+
+.step-item {
+ padding: 15px;
+ background: white;
+ border-radius: 8px;
+ width: 100%;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ margin-bottom: 10px;
+ opacity: 1;
+ transform: none;
+}
+
+.step-item .log-line:not(.result) {
+ opacity: 0.7;
+ color: #666;
+ font-size: 0.9em;
+}
+
+.step-item .log-line.result {
+ opacity: 1;
+ color: #333;
+ font-size: 1em;
+ background: #e8f5e9;
+ border-left: 4px solid #4caf50;
+ padding: 10px;
+ border-radius: 4px;
+}
+
+.step-item.show {
+ opacity: 1;
+ transform: none;
+}
+
+.log-line {
+ padding: 10px;
+ border-radius: 4px;
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.log-line.think,
+.step-item pre.think {
+ background: var(--info-color-light);
+ border-left: 4px solid var(--info-color);
+}
+
+.log-line.tool,
+.step-item pre.tool {
+ background: var(--warning-color-light);
+ border-left: 4px solid var(--warning-color);
+}
+
+.log-line.result,
+.step-item pre.result {
+ background: var(--success-color-light);
+ border-left: 4px solid var(--success-color);
+}
+
+.log-line.error,
+.step-item pre.error {
+ background: var(--error-color-light);
+ border-left: 4px solid var(--error-color);
+}
+
+.log-line.info,
+.step-item pre.info {
+ background: var(--bg-color);
+ border-left: 4px solid var(--text-light);
+}
+
+/* 删除重复的样式定义 */
+
+.log-prefix {
+ font-weight: bold;
+ margin-bottom: 5px;
+ color: #666;
+}
+
+.step-item pre {
+ padding: 10px;
+ border-radius: 4px;
+ margin: 10px 0;
+ overflow-x: auto;
+ padding: 10px;
+ border-radius: 4px;
+ margin: 10px 0;
+ overflow-x: auto;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9em;
+ line-height: 1.4;
+ white-space: pre-wrap;
+ color: var(--text-color);
+ background: var(--bg-color);
+
+ &.log {
+ background: var(--bg-color);
+ border-left: 4px solid var(--text-light);
+ }
+ &.think {
+ background: var(--info-color-light);
+ border-left: 4px solid var(--info-color);
+ }
+ &.tool {
+ background: var(--warning-color-light);
+ border-left: 4px solid var(--warning-color);
+ }
+ &.result {
+ background: var(--success-color-light);
+ border-left: 4px solid var(--success-color);
+ }
+}
+
+.step-item strong {
+ display: block;
+ margin-bottom: 8px;
+ color: #007bff;
+ font-size: 0.9em;
+}
+
+.step-item div {
+ color: #444;
+ line-height: 1.6;
+}
+
+.loading {
+ padding: 15px;
+ color: #666;
+ text-align: center;
+}
+
+.ping {
+ color: #ccc;
+ text-align: center;
+ margin: 5px 0;
+}
+
+.error {
+ color: #dc3545;
+ padding: 10px;
+ background: #ffe6e6;
+ border-radius: 4px;
+ margin: 10px 0;
+}
+
+.complete {
+ color: #28a745;
+ padding: 10px;
+ background: #e6ffe6;
+ border-radius: 4px;
+ margin: 10px 0;
+}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..af097f9
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+ OpenManus本地版
+
+
+
+
+
+
+
+
+
+
欢迎使用OpenManus本地版
+
请输入任务提示开始新的任务
+
+
+
+
+
+
+
+
+
+
+
+
+
+