Merge branch 'main' of https://github.com/mannaandpoem/OpenManus into tx_dev
This commit is contained in:
commit
e25cfa2cb3
11
README.md
11
README.md
@ -7,12 +7,9 @@ English | [中文](README_zh.md)
|
||||
|
||||
# 👋 OpenManus
|
||||
|
||||
[Official Website](https://openmanus.github.io/)
|
||||
|
||||
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
|
||||
|
||||
Our team
|
||||
members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
||||
Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
||||
|
||||
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
|
||||
|
||||
@ -130,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!
|
||||
|
||||
@ -162,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}},
|
||||
}
|
||||
```
|
||||
```
|
||||
|
@ -7,8 +7,6 @@
|
||||
|
||||
# 👋 OpenManus
|
||||
|
||||
[官方网站](https://openmanus.github.io/)
|
||||
|
||||
Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫!
|
||||
|
||||
我们的团队成员 [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) https://github.com/stellaHSR 来自 [@MetaGPT](https://github.com/geekan/MetaGPT) 组织,我们在 3
|
||||
@ -130,10 +128,6 @@ python run_flow.py
|
||||
|
||||
或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com
|
||||
|
||||
## 发展路线
|
||||
|
||||
- [ ] 提高用户界面的视觉吸引力,以创建更直观和无缝的用户体验。
|
||||
|
||||
## 交流群
|
||||
|
||||
加入我们的飞书交流群,与其他开发者分享经验!
|
||||
|
1
app.py
1
app.py
@ -249,3 +249,4 @@ async def generic_exception_handler(request: Request, exc: Exception):
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="localhost", port=5172)
|
||||
|
||||
|
@ -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: "[ ]",
|
||||
}
|
||||
|
@ -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:
|
||||
|
14
pytest.ini
14
pytest.ini
@ -1,14 +0,0 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
|
||||
# Log settings
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
|
||||
log_cli_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
# Make sure asyncio works properly
|
||||
asyncio_mode = auto
|
694
static/main.js
694
static/main.js
@ -1,694 +0,0 @@
|
||||
let currentEventSource = null;
|
||||
|
||||
function createTask() {
|
||||
const promptInput = document.getElementById('prompt-input');
|
||||
const prompt = promptInput.value.trim();
|
||||
|
||||
if (!prompt) {
|
||||
alert("Please enter a valid prompt");
|
||||
promptInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
container.innerHTML = '<div class="loading">Initializing task...</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 || 'Request failed') });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID');
|
||||
}
|
||||
setupSSE(data.task_id);
|
||||
loadHistory();
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
console.error('Failed to create task:', 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('Polling failed:', 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');
|
||||
}
|
||||
|
||||
// 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>
|
||||
`;
|
||||
|
||||
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('Think event handling failed:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
<pre>${lastResultContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
lastResultContent = ''; // Clear result content
|
||||
});
|
||||
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
❌ Error: ${data.message}
|
||||
</div>
|
||||
`;
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
} catch (e) {
|
||||
console.error('Error handling failed:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
if (isTaskComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('SSE connection error:', err);
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
eventSource.close();
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
container.innerHTML += `
|
||||
<div class="warning">
|
||||
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
|
||||
</div>
|
||||
`;
|
||||
setTimeout(connect, retryDelay);
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
⚠ Connection lost, please try refreshing the page
|
||||
</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 'log': return '📝';
|
||||
case 'run': return '⚙️';
|
||||
default: return 'ℹ️';
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
function updateTaskStatus(task) {
|
||||
const statusBar = document.getElementById('status-bar');
|
||||
if (!statusBar) return;
|
||||
|
||||
if (task.status === 'completed') {
|
||||
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
|
||||
} else if (task.status === 'failed') {
|
||||
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
|
||||
} else {
|
||||
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
createTask();
|
||||
}
|
||||
});
|
||||
|
||||
// Show history button functionality
|
||||
const historyToggle = document.getElementById('history-toggle');
|
||||
if (historyToggle) {
|
||||
historyToggle.addEventListener('click', () => {
|
||||
const historyPanel = document.getElementById('history-panel');
|
||||
if (historyPanel) {
|
||||
historyPanel.classList.toggle('open');
|
||||
historyToggle.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear button functionality
|
||||
const clearButton = document.getElementById('clear-btn');
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener('click', () => {
|
||||
document.getElementById('prompt-input').value = '';
|
||||
document.getElementById('prompt-input').focus();
|
||||
});
|
||||
}
|
||||
});
|
338
static/style.css
338
static/style.css
@ -1,338 +0,0 @@
|
||||
: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;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenManus Local Version</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="history-panel">
|
||||
<h2>History Tasks</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>Welcome to OpenManus Local Version</h1>
|
||||
<p>Please enter a task prompt to start a new task</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="Enter task prompt..."
|
||||
onkeypress="if(event.keyCode === 13) createTask()"
|
||||
>
|
||||
<button onclick="createTask()">Create Task</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/main.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user