OpenManus简易web界面

This commit is contained in:
Feige-cn 2025-03-10 12:17:07 +08:00
parent 60bb2ce00f
commit ecdbb3b26a
6 changed files with 1219 additions and 0 deletions

247
app.py Normal file
View 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
View 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

3
run.bat Normal file
View File

@ -0,0 +1,3 @@
@echo off
venv\Scripts\python.exe app.py
pause

585
static/main.js Normal file
View 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, '&nbsp;&nbsp;')
.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
View 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
View 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>