OpenManus简易web界面
This commit is contained in:
parent
60bb2ce00f
commit
ecdbb3b26a
247
app.py
Normal file
247
app.py
Normal file
@ -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)
|
7
config/config.toml
Normal file
7
config/config.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Global LLM configuration
|
||||||
|
[llm]
|
||||||
|
model = "qwen-plus"
|
||||||
|
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
|
api_key = "sk-e8dcd91a089f4a71993b7043f84790f4"
|
||||||
|
max_tokens = 8192
|
||||||
|
temperature = 1.0
|
585
static/main.js
Normal file
585
static/main.js
Normal file
@ -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 = '<div class="loading">任务初始化中...</div>';
|
||||||
|
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 = `<div class="error">错误: ${error.message}</div>`;
|
||||||
|
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 += '<div class="ping">·</div>';
|
||||||
|
}, 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 = '<div class="step-container"></div>';
|
||||||
|
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 `
|
||||||
|
<div class="step-item ${step.type || 'step'}">
|
||||||
|
<div class="log-line">
|
||||||
|
<span class="log-prefix">${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).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 = '<div class="step-container"></div>';
|
||||||
|
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 = `
|
||||||
|
<div class="log-line">
|
||||||
|
<span class="log-prefix">${getEventIcon('think')} [${timestamp}] ${getEventLabel('think')}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = '<div class="step-container"></div>';
|
||||||
|
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 = `
|
||||||
|
<div class="log-line">
|
||||||
|
<span class="log-prefix">${getEventIcon('tool')} [${timestamp}] ${getEventLabel('tool')}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = '<div class="step-container"></div>';
|
||||||
|
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 = `
|
||||||
|
<div class="log-line">
|
||||||
|
<span class="log-prefix">${getEventIcon('act')} [${timestamp}] ${getEventLabel('act')}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = '<div class="step-container"></div>';
|
||||||
|
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 = `
|
||||||
|
<div class="log-line">
|
||||||
|
<span class="log-prefix">${getEventIcon('log')} [${timestamp}] ${getEventLabel('log')}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = '<div class="step-container"></div>';
|
||||||
|
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 = `
|
||||||
|
<div class="log-line">
|
||||||
|
<span class="log-prefix">${getEventIcon('run')} [${timestamp}] ${getEventLabel('run')}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = '<div class="step-container"></div>';
|
||||||
|
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 = `
|
||||||
|
<div class="log-line ${data.type || 'info'}">
|
||||||
|
<span class="log-prefix">${getEventIcon(data.type)} [${timestamp}] ${getEventLabel(data.type)}:</span>
|
||||||
|
<pre>${content}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 += `
|
||||||
|
<div class="complete">
|
||||||
|
<div>✅ 任务完成</div>
|
||||||
|
<pre>${lastResultContent}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
eventSource.close();
|
||||||
|
currentEventSource = null;
|
||||||
|
lastResultContent = ''; // 清空结果内容
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.addEventListener('error', (event) => {
|
||||||
|
clearInterval(heartbeatTimer);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
container.innerHTML += `
|
||||||
|
<div class="error">
|
||||||
|
❌ 错误: ${data.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 += `
|
||||||
|
<div class="warning">
|
||||||
|
⚠ 连接中断,${retryDelay/1000}秒后重试 (${retryCount}/${maxRetries})...
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
setTimeout(connect, retryDelay);
|
||||||
|
} else {
|
||||||
|
container.innerHTML += `
|
||||||
|
<div class="error">
|
||||||
|
⚠ 连接中断,请尝试刷新页面
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, '<br>')
|
||||||
|
.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 => `
|
||||||
|
<div class="task-card" data-task-id="${task.id}">
|
||||||
|
<div>${task.prompt}</div>
|
||||||
|
<div class="task-meta">
|
||||||
|
${new Date(task.created_at).toLocaleString()} -
|
||||||
|
<span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}">
|
||||||
|
${task.status || '未知状态'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('加载历史记录失败:', error);
|
||||||
|
const listContainer = document.getElementById('task-list');
|
||||||
|
listContainer.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
338
static/style.css
Normal file
338
static/style.css
Normal file
@ -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;
|
||||||
|
}
|
39
templates/index.html
Normal file
39
templates/index.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OpenManus本地版</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="history-panel">
|
||||||
|
<h2>历史任务</h2>
|
||||||
|
<div id="task-list" class="task-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-panel">
|
||||||
|
<div id="task-container" class="task-container">
|
||||||
|
<div class="welcome-message">
|
||||||
|
<h1>欢迎使用OpenManus本地版</h1>
|
||||||
|
<p>请输入任务提示开始新的任务</p>
|
||||||
|
</div>
|
||||||
|
<div id="log-container" class="step-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="input-container" class="input-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="prompt-input"
|
||||||
|
placeholder="请输入任务提示..."
|
||||||
|
onkeypress="if(event.keyCode === 13) createTask()"
|
||||||
|
>
|
||||||
|
<button onclick="createTask()">创建任务</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user