Merge remote-tracking branch 'origin/main'

This commit is contained in:
xiangjinyu 2025-03-11 11:47:15 +08:00
commit 2c0b2d1fb3
8 changed files with 243 additions and 593 deletions

View File

@ -1,6 +1,7 @@
English | [中文](README_zh.md)
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus)  
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
 
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)  
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
@ -126,10 +127,6 @@ We welcome any friendly suggestions and helpful contributions! Just create issue
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
## Roadmap
- [ ] Improve the UIs visual appeal to create a more intuitive and seamless user experience.
## Community Group
Join our networking group on Feishu and share your experience with other developers!
@ -158,4 +155,4 @@ OpenManus is built by contributors from MetaGPT. Huge thanks to this agent commu
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
}
```
```

View File

@ -1,6 +1,7 @@
[English](README.md) | 中文
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus)  
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
 
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)  
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
@ -127,10 +128,6 @@ python run_flow.py
或通过 📧 邮件联系 @mannaandpoemmannaandpoem@gmail.com
## 发展路线
- [ ] 提高用户界面的视觉吸引力,以创建更直观和无缝的用户体验。
## 交流群
加入我们的飞书交流群,与其他开发者分享经验!

101
app.py
View File

@ -1,5 +1,7 @@
import asyncio
import threading
import uuid
import webbrowser
from datetime import datetime
from json import dumps
@ -10,6 +12,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
@ -23,6 +26,7 @@ app.add_middleware(
allow_headers=["*"],
)
class Task(BaseModel):
id: str
prompt: str
@ -32,9 +36,10 @@ class Task(BaseModel):
def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
data['created_at'] = self.created_at.isoformat()
data["created_at"] = self.created_at.isoformat()
return data
class TaskManager:
def __init__(self):
self.tasks = {}
@ -43,61 +48,55 @@ class TaskManager:
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"
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"):
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
})
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": "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
})
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.manus import Manus
@ -108,17 +107,21 @@ async def run_task(task_id: str, prompt: str):
agent = Manus(
name="Manus",
description="A versatile agent that can solve various tasks using multiple tools",
max_steps=30
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"Executing tool: {tool}\nInput: {input}", "tool")
await task_manager.update_task_step(
task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool"
)
async def on_action(action):
await task_manager.update_task_step(task_id, 0, f"Executing action: {action}", "act")
await task_manager.update_task_step(
task_id, 0, f"Executing action: {action}", "act"
)
async def on_run(step, result):
await task_manager.update_task_step(task_id, step, result, "run")
@ -133,7 +136,7 @@ async def run_task(task_id: str, prompt: str):
import re
# 提取 - 后面的内容
cleaned_message = re.sub(r'^.*? - ', '', message)
cleaned_message = re.sub(r"^.*? - ", "", message)
event_type = "log"
if "✨ Manus's thoughts:" in cleaned_message:
@ -147,7 +150,9 @@ async def run_task(task_id: str, prompt: str):
elif "🏁 Special tool" in cleaned_message:
event_type = "complete"
await task_manager.update_task_step(self.task_id, 0, cleaned_message, event_type)
await task_manager.update_task_step(
self.task_id, 0, cleaned_message, event_type
)
sse_handler = SSELogHandler(task_id)
logger.add(sse_handler)
@ -158,6 +163,7 @@ async def run_task(task_id: str, prompt: str):
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():
@ -169,11 +175,7 @@ async def task_events(task_id: str):
task = task_manager.tasks.get(task_id)
if task:
yield f"event: status\ndata: {dumps({
'type': 'status',
'status': task.status,
'steps': task.steps
})}\n\n"
yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
while True:
try:
@ -191,11 +193,7 @@ async def task_events(task_id: str):
elif event["type"] == "step":
task = task_manager.tasks.get(task_id)
if task:
yield f"event: status\ndata: {dumps({
'type': 'status',
'status': task.status,
'steps': task.steps
})}\n\n"
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"
@ -216,35 +214,42 @@ async def task_events(task_id: str):
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
"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
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"}
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"Server error: {str(exc)}"}
status_code=500, content={"message": f"Server error: {str(exc)}"}
)
def open_local_browser():
webbrowser.open_new_tab("http://localhost:5172")
if __name__ == "__main__":
import uvicorn
threading.Timer(3, open_local_browser).start()
uvicorn.run(app, host="localhost", port=5172)

View File

@ -60,3 +60,32 @@ class BaseFlow(BaseModel, ABC):
@abstractmethod
async def execute(self, input_text: str) -> str:
"""Execute the flow with given input"""
class PlanStepStatus(str, Enum):
"""Enum class defining possible statuses of a plan step"""
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
BLOCKED = "blocked"
@classmethod
def get_all_statuses(cls) -> list[str]:
"""Return a list of all possible step status values"""
return [status.value for status in cls]
@classmethod
def get_active_statuses(cls) -> list[str]:
"""Return a list of values representing active statuses (not started or in progress)"""
return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value]
@classmethod
def get_status_marks(cls) -> Dict[str, str]:
"""Return a mapping of statuses to their marker symbols"""
return {
cls.COMPLETED.value: "[✓]",
cls.IN_PROGRESS.value: "[→]",
cls.BLOCKED.value: "[!]",
cls.NOT_STARTED.value: "[ ]",
}

View File

@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union
from pydantic import Field
from app.agent.base import BaseAgent
from app.flow.base import BaseFlow
from app.flow.base import BaseFlow, PlanStepStatus
from app.llm import LLM
from app.logger import logger
from app.schema import AgentState, Message
@ -183,11 +183,11 @@ class PlanningFlow(BaseFlow):
# Find first non-completed step
for i, step in enumerate(steps):
if i >= len(step_statuses):
status = "not_started"
status = PlanStepStatus.NOT_STARTED.value
else:
status = step_statuses[i]
if status in ["not_started", "in_progress"]:
if status in PlanStepStatus.get_active_statuses():
# Extract step type/category if available
step_info = {"text": step}
@ -204,17 +204,17 @@ class PlanningFlow(BaseFlow):
command="mark_step",
plan_id=self.active_plan_id,
step_index=i,
step_status="in_progress",
step_status=PlanStepStatus.IN_PROGRESS.value,
)
except Exception as e:
logger.warning(f"Error marking step as in_progress: {e}")
# Update step status directly if needed
if i < len(step_statuses):
step_statuses[i] = "in_progress"
step_statuses[i] = PlanStepStatus.IN_PROGRESS.value
else:
while len(step_statuses) < i:
step_statuses.append("not_started")
step_statuses.append("in_progress")
step_statuses.append(PlanStepStatus.NOT_STARTED.value)
step_statuses.append(PlanStepStatus.IN_PROGRESS.value)
plan_data["step_statuses"] = step_statuses
@ -266,7 +266,7 @@ class PlanningFlow(BaseFlow):
command="mark_step",
plan_id=self.active_plan_id,
step_index=self.current_step_index,
step_status="completed",
step_status=PlanStepStatus.COMPLETED.value,
)
logger.info(
f"Marked step {self.current_step_index} as completed in plan {self.active_plan_id}"
@ -280,10 +280,10 @@ class PlanningFlow(BaseFlow):
# Ensure the step_statuses list is long enough
while len(step_statuses) <= self.current_step_index:
step_statuses.append("not_started")
step_statuses.append(PlanStepStatus.NOT_STARTED.value)
# Update the status
step_statuses[self.current_step_index] = "completed"
step_statuses[self.current_step_index] = PlanStepStatus.COMPLETED.value
plan_data["step_statuses"] = step_statuses
async def _get_plan_text(self) -> str:
@ -311,23 +311,18 @@ class PlanningFlow(BaseFlow):
# Ensure step_statuses and step_notes match the number of steps
while len(step_statuses) < len(steps):
step_statuses.append("not_started")
step_statuses.append(PlanStepStatus.NOT_STARTED.value)
while len(step_notes) < len(steps):
step_notes.append("")
# Count steps by status
status_counts = {
"completed": 0,
"in_progress": 0,
"blocked": 0,
"not_started": 0,
}
status_counts = {status: 0 for status in PlanStepStatus.get_all_statuses()}
for status in step_statuses:
if status in status_counts:
status_counts[status] += 1
completed = status_counts["completed"]
completed = status_counts[PlanStepStatus.COMPLETED.value]
total = len(steps)
progress = (completed / total) * 100 if total > 0 else 0
@ -337,21 +332,19 @@ class PlanningFlow(BaseFlow):
plan_text += (
f"Progress: {completed}/{total} steps completed ({progress:.1f}%)\n"
)
plan_text += f"Status: {status_counts['completed']} completed, {status_counts['in_progress']} in progress, "
plan_text += f"{status_counts['blocked']} blocked, {status_counts['not_started']} not started\n\n"
plan_text += f"Status: {status_counts[PlanStepStatus.COMPLETED.value]} completed, {status_counts[PlanStepStatus.IN_PROGRESS.value]} in progress, "
plan_text += f"{status_counts[PlanStepStatus.BLOCKED.value]} blocked, {status_counts[PlanStepStatus.NOT_STARTED.value]} not started\n\n"
plan_text += "Steps:\n"
status_marks = PlanStepStatus.get_status_marks()
for i, (step, status, notes) in enumerate(
zip(steps, step_statuses, step_notes)
):
if status == "completed":
status_mark = "[✓]"
elif status == "in_progress":
status_mark = "[→]"
elif status == "blocked":
status_mark = "[!]"
else: # not_started
status_mark = "[ ]"
# Use status marks to indicate step status
status_mark = status_marks.get(
status, status_marks[PlanStepStatus.NOT_STARTED.value]
)
plan_text += f"{i}. {status_mark} {step}\n"
if notes:

View File

@ -5,6 +5,7 @@ pyyaml~=6.0.2
loguru~=0.7.3
numpy
datasets~=3.2.0
fastapi~=0.115.11
html2text~=2024.2.26
gymnasium~=1.0.0

View File

@ -50,12 +50,12 @@ function setupSSE(taskId) {
const maxRetries = 3;
const retryDelay = 2000;
const container = document.getElementById('task-container');
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);
@ -71,94 +71,20 @@ function setupSSE(taskId) {
});
}, 10000);
if (!eventSource._listenersAdded) {
eventSource._listenersAdded = true;
let lastResultContent = '';
eventSource.addEventListener('status', (event) => {
const handleEvent = (event, type) => {
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');
}
// Save result content
if (data.steps && data.steps.length > 0) {
// Iterate through all steps, find the last result type
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('Status update failed:', e);
}
});
// Add handler for think event
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>
`;
const stepContainer = ensureStepContainer(container);
const { formattedContent, timestamp } = formatStepContent(data, type);
const step = createStepElement(type, formattedContent, timestamp);
stepContainer.appendChild(step);
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
autoScroll(stepContainer);
// Update task status
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
@ -168,236 +94,16 @@ function setupSSE(taskId) {
console.error('Status update failed:', error);
});
} catch (e) {
console.error('Think event handling failed:', e);
console.error(`Error handling ${type} event:`, e);
}
};
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message'];
eventTypes.forEach(type => {
eventSource.addEventListener(type, (event) => handleEvent(event, type));
});
// Add handler for tool event
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'
});
// Update task status
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Status update failed:', error);
});
} catch (e) {
console.error('Tool event handling failed:', e);
}
});
// Add handler for act event
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'
});
// Update task status
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Status update failed:', error);
});
} catch (e) {
console.error('Act event handling failed:', e);
}
});
// Add handler for log event
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'
});
// Update task status
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Status update failed:', error);
});
} catch (e) {
console.error('Log event handling failed:', 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'
});
// Update task status
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Status update failed:', error);
});
} catch (e) {
console.error('Run event handling failed:', 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('Message handling failed:', e);
}
});
let isTaskComplete = false;
eventSource.addEventListener('complete', (event) => {
isTaskComplete = true;
clearInterval(heartbeatTimer);
clearInterval(pollInterval);
container.innerHTML += `
@ -408,7 +114,6 @@ function setupSSE(taskId) {
`;
eventSource.close();
currentEventSource = null;
lastResultContent = ''; // Clear result content
});
eventSource.addEventListener('error', (event) => {
@ -427,17 +132,9 @@ function setupSSE(taskId) {
console.error('Error handling failed:', e);
}
});
}
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
eventSource.onerror = (err) => {
if (isTaskComplete) {
return;
}
if (eventSource.readyState === EventSource.CLOSED) return;
console.error('SSE connection error:', err);
clearInterval(heartbeatTimer);
@ -465,32 +162,105 @@ function setupSSE(taskId) {
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 'log': return '📝';
case 'run': return '⚙️';
default: return '';
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>`;
});
}
function ensureStepContainer(container) {
let stepContainer = container.querySelector('.step-container');
if (!stepContainer) {
container.innerHTML = '<div class="step-container"></div>';
stepContainer = container.querySelector('.step-container');
}
return stepContainer;
}
function formatStepContent(data, eventType) {
return {
formattedContent: data.result,
timestamp: new Date().toLocaleTimeString()
};
}
function createStepElement(type, content, timestamp) {
const step = document.createElement('div');
step.className = `step-item ${type}`;
step.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
</div>
`;
return step;
}
function autoScroll(element) {
requestAnimationFrame(() => {
element.scrollTo({
top: element.scrollHeight,
behavior: 'smooth'
});
});
setTimeout(() => {
element.scrollTop = element.scrollHeight;
}, 100);
}
function getEventIcon(eventType) {
const icons = {
'think': '🤔',
'tool': '🛠️',
'act': '🚀',
'result': '🏁',
'error': '❌',
'complete': '✅',
'log': '📝',
'run': '⚙️'
};
return icons[eventType] || '';
}
function getEventLabel(eventType) {
switch(eventType) {
case 'think': return 'Thinking';
case 'tool': return 'Using Tool';
case 'act': return 'Action';
case 'result': return 'Result';
case 'error': return 'Error';
case 'complete': return 'Complete';
case 'log': return 'Log';
case 'run': return 'Running';
default: return 'Info';
}
const labels = {
'think': 'Thinking',
'tool': 'Using Tool',
'act': 'Action',
'result': 'Result',
'error': 'Error',
'complete': 'Complete',
'log': 'Log',
'run': 'Running'
};
return labels[eventType] || 'Info';
}
function updateTaskStatus(task) {
@ -506,164 +276,9 @@ function updateTaskStatus(task) {
}
}
function loadHistory() {
fetch('/tasks')
.then(response => {
if (!response.ok) {
throw new Error('Failed to load history');
}
return response.json();
})
.then(tasks => {
const historyContainer = document.getElementById('history-container');
if (!historyContainer) return;
historyContainer.innerHTML = '';
if (tasks.length === 0) {
historyContainer.innerHTML = '<div class="history-empty">No recent tasks</div>';
return;
}
const historyList = document.createElement('div');
historyList.className = 'history-list';
tasks.forEach(task => {
const taskItem = document.createElement('div');
taskItem.className = `history-item ${task.status}`;
taskItem.innerHTML = `
<div class="history-prompt">${task.prompt}</div>
<div class="history-meta">
<span class="history-time">${new Date(task.created_at).toLocaleString()}</span>
<span class="history-status">${getStatusIcon(task.status)}</span>
</div>
`;
taskItem.addEventListener('click', () => {
loadTask(task.id);
});
historyList.appendChild(taskItem);
});
historyContainer.appendChild(historyList);
})
.catch(error => {
console.error('Failed to load history:', error);
const historyContainer = document.getElementById('history-container');
if (historyContainer) {
historyContainer.innerHTML = `<div class="error">Failed to load history: ${error.message}</div>`;
}
});
}
function getStatusIcon(status) {
switch(status) {
case 'completed': return '✅';
case 'failed': return '❌';
case 'running': return '⚙️';
default: return '⏳';
}
}
function loadTask(taskId) {
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
const container = document.getElementById('task-container');
container.innerHTML = '<div class="loading">Loading task...</div>';
document.getElementById('input-container').classList.add('bottom');
fetch(`/tasks/${taskId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to load task');
}
return response.json();
})
.then(task => {
if (task.status === 'running') {
setupSSE(taskId);
} else {
displayTask(task);
}
})
.catch(error => {
console.error('Failed to load task:', error);
container.innerHTML = `<div class="error">Failed to load task: ${error.message}</div>`;
});
}
function displayTask(task) {
const container = document.getElementById('task-container');
container.innerHTML = '';
container.classList.add('active');
const welcomeMessage = document.querySelector('.welcome-message');
if (welcomeMessage) {
welcomeMessage.style.display = 'none';
}
const stepContainer = document.createElement('div');
stepContainer.className = 'step-container';
if (task.steps && task.steps.length > 0) {
task.steps.forEach(step => {
const stepItem = document.createElement('div');
stepItem.className = `step-item ${step.type || 'step'}`;
const content = step.result;
const timestamp = new Date(step.timestamp || task.created_at).toLocaleTimeString();
stepItem.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}:</span>
<pre>${content}</pre>
</div>
`;
stepContainer.appendChild(stepItem);
});
} else {
stepContainer.innerHTML = '<div class="no-steps">No steps recorded for this task</div>';
}
container.appendChild(stepContainer);
if (task.status === 'completed') {
let lastResultContent = '';
if (task.steps && task.steps.length > 0) {
for (let i = task.steps.length - 1; i >= 0; i--) {
if (task.steps[i].type === 'result') {
lastResultContent = task.steps[i].result;
break;
}
}
}
container.innerHTML += `
<div class="complete">
<div> Task completed</div>
<pre>${lastResultContent}</pre>
</div>
`;
} else if (task.status === 'failed') {
container.innerHTML += `
<div class="error">
Error: ${task.error || 'Unknown error'}
</div>
`;
}
updateTaskStatus(task);
}
// Initialize the app when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
loadHistory();
// Set up event listeners
document.getElementById('create-task-btn').addEventListener('click', createTask);
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@ -671,7 +286,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Show history button functionality
const historyToggle = document.getElementById('history-toggle');
if (historyToggle) {
historyToggle.addEventListener('click', () => {
@ -683,7 +297,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Clear button functionality
const clearButton = document.getElementById('clear-btn');
if (clearButton) {
clearButton.addEventListener('click', () => {

View File

@ -28,6 +28,7 @@ body {
.container {
display: flex;
min-height: 100vh;
min-width: 0;
width: 90%;
margin: 0 auto;
padding: 20px;
@ -62,13 +63,13 @@ body {
.task-container {
@extend .card;
width: 100%;
max-width: 100%;
position: relative;
min-height: 300px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
overflow: auto;
height: 100%;
}
@ -177,11 +178,15 @@ button:hover {
}
.step-container {
display: block;
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
width: 100%;
max-height: calc(100vh - 300px);
max-height: calc(100vh - 200px);
overflow-y: auto;
max-width: 100%;
overflow-x: hidden;
}
.step-item {
@ -255,10 +260,9 @@ button:hover {
border-left: 4px solid var(--text-light);
}
/* 删除重复的样式定义 */
.log-prefix {
font-weight: bold;
white-space: nowrap;
margin-bottom: 5px;
color: #666;
}
@ -267,15 +271,14 @@ button:hover {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
overflow-x: auto;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
overflow-x: auto;
overflow-x: hidden;
font-family: 'Courier New', monospace;
font-size: 0.9em;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
max-width: 100%;
color: var(--text-color);
background: var(--bg-color);
@ -336,3 +339,15 @@ button:hover {
border-radius: 4px;
margin: 10px 0;
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.complete pre {
max-width: 100%;
white-space: pre-wrap;
word-break: break-word;
}