diff --git a/README.md b/README.md
index 6039f0a..8cac223 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
English | [中文](README_zh.md)
-[](https://github.com/mannaandpoem/OpenManus)
+[](https://github.com/mannaandpoem/OpenManus/stargazers)
+
[](https://opensource.org/licenses/MIT)
[](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 UI’s 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}},
}
-```
\ No newline at end of file
+```
diff --git a/README_zh.md b/README_zh.md
index 1f12b65..13bb768 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,6 +1,7 @@
[English](README.md) | 中文
-[](https://github.com/mannaandpoem/OpenManus)
+[](https://github.com/mannaandpoem/OpenManus/stargazers)
+
[](https://opensource.org/licenses/MIT)
[](https://discord.gg/DYn29wFk9z)
@@ -127,10 +128,6 @@ python run_flow.py
或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com
-## 发展路线
-
-- [ ] 提高用户界面的视觉吸引力,以创建更直观和无缝的用户体验。
-
## 交流群
加入我们的飞书交流群,与其他开发者分享经验!
diff --git a/app.py b/app.py
index baad1cf..2e1959a 100644
--- a/app.py
+++ b/app.py
@@ -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)
diff --git a/app/flow/base.py b/app/flow/base.py
index 13b3e17..13066ce 100644
--- a/app/flow/base.py
+++ b/app/flow/base.py
@@ -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: "[ ]",
+ }
diff --git a/app/flow/planning.py b/app/flow/planning.py
index b3df48a..808949f 100644
--- a/app/flow/planning.py
+++ b/app/flow/planning.py
@@ -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:
diff --git a/requirements.txt b/requirements.txt
index a4128d2..7ce4b52 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/static/main.js b/static/main.js
index 49d5dd7..c219d20 100644
--- a/static/main.js
+++ b/static/main.js
@@ -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 += '
·
';
}, 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 = '';
- 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 `
-
-
-
${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}:
-
${content}
-
-
- `;
- }).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 = '';
- stepContainer = container.querySelector('.step-container');
- }
-
- const content = data.result;
- const timestamp = new Date().toLocaleTimeString();
-
- const step = document.createElement('div');
- step.className = 'step-item think';
- step.innerHTML = `
-
-
${getEventIcon('think')} [${timestamp}] ${getEventLabel('think')}:
-
${content}
-
- `;
+ 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 = '';
- stepContainer = container.querySelector('.step-container');
- }
-
- const content = data.result;
- const timestamp = new Date().toLocaleTimeString();
-
- const step = document.createElement('div');
- step.className = 'step-item tool';
- step.innerHTML = `
-
-
${getEventIcon('tool')} [${timestamp}] ${getEventLabel('tool')}:
-
${content}
-
- `;
-
- stepContainer.appendChild(step);
- container.scrollTo({
- top: container.scrollHeight,
- behavior: 'smooth'
- });
-
- // 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 = '';
- stepContainer = container.querySelector('.step-container');
- }
-
- const content = data.result;
- const timestamp = new Date().toLocaleTimeString();
-
- const step = document.createElement('div');
- step.className = 'step-item act';
- step.innerHTML = `
-
-
${getEventIcon('act')} [${timestamp}] ${getEventLabel('act')}:
-
${content}
-
- `;
-
- stepContainer.appendChild(step);
- container.scrollTo({
- top: container.scrollHeight,
- behavior: 'smooth'
- });
-
- // 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 = '';
- stepContainer = container.querySelector('.step-container');
- }
-
- const content = data.result;
- const timestamp = new Date().toLocaleTimeString();
-
- const step = document.createElement('div');
- step.className = 'step-item log';
- step.innerHTML = `
-
-
${getEventIcon('log')} [${timestamp}] ${getEventLabel('log')}:
-
${content}
-
- `;
-
- stepContainer.appendChild(step);
- container.scrollTo({
- top: container.scrollHeight,
- behavior: 'smooth'
- });
-
- // 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 = '';
- stepContainer = container.querySelector('.step-container');
- }
-
- const content = data.result;
- const timestamp = new Date().toLocaleTimeString();
-
- const step = document.createElement('div');
- step.className = 'step-item run';
- step.innerHTML = `
-
-
${getEventIcon('run')} [${timestamp}] ${getEventLabel('run')}:
-
${content}
-
- `;
-
- stepContainer.appendChild(step);
- container.scrollTo({
- top: container.scrollHeight,
- behavior: 'smooth'
- });
-
- // 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 = '';
- stepContainer = container.querySelector('.step-container');
- }
-
- // Create new step element
- const step = document.createElement('div');
- step.className = `step-item ${data.type || 'step'}`;
-
- // Format content and timestamp
- const content = data.result;
- const timestamp = new Date().toLocaleTimeString();
-
- step.innerHTML = `
-
-
${getEventIcon(data.type)} [${timestamp}] ${getEventLabel(data.type)}:
-
${content}
-
- `;
-
- // Add step to container with animation
- stepContainer.prepend(step);
- setTimeout(() => {
- step.classList.add('show');
- }, 10);
-
- // Auto-scroll to bottom
- container.scrollTo({
- top: container.scrollHeight,
- behavior: 'smooth'
- });
- } catch (e) {
- console.error('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 => `
+
+
${task.prompt}
+
+ ${new Date(task.created_at).toLocaleString()} -
+
+ ${task.status || '未知状态'}
+
+
+
+ `).join('');
+ })
+ .catch(error => {
+ console.error('加载历史记录失败:', error);
+ const listContainer = document.getElementById('task-list');
+ listContainer.innerHTML = `加载失败: ${error.message}
`;
+ });
+}
+
+
+function ensureStepContainer(container) {
+ let stepContainer = container.querySelector('.step-container');
+ if (!stepContainer) {
+ container.innerHTML = '';
+ 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 = `
+
+
${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:
+
${content}
+
+ `;
+ 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 = 'No recent tasks
';
- 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 = `
- ${task.prompt}
-
- ${new Date(task.created_at).toLocaleString()}
- ${getStatusIcon(task.status)}
-
- `;
- 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 = `Failed to load history: ${error.message}
`;
- }
- });
-}
-
-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 = 'Loading task...
';
- 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 = `Failed to load task: ${error.message}
`;
- });
-}
-
-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 = `
-
-
${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}:
-
${content}
-
- `;
-
- stepContainer.appendChild(stepItem);
- });
- } else {
- stepContainer.innerHTML = 'No steps recorded for this task
';
- }
-
- 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 += `
-
-
✅ Task completed
-
${lastResultContent}
-
- `;
- } else if (task.status === 'failed') {
- container.innerHTML += `
-
- ❌ Error: ${task.error || 'Unknown error'}
-
- `;
- }
-
- 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', () => {
diff --git a/static/style.css b/static/style.css
index 776304e..5aaec28 100644
--- a/static/style.css
+++ b/static/style.css
@@ -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;
+}