Merge pull request #432 from Feige-cn/main

modified by hook
This commit is contained in:
mannaandpoem 2025-03-11 10:55:03 +08:00 committed by GitHub
commit 8280b68462
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 142 additions and 506 deletions

8
app.py
View File

@ -1,5 +1,7 @@
import asyncio import asyncio
import threading
import uuid import uuid
import webbrowser
from datetime import datetime from datetime import datetime
from json import dumps from json import dumps
@ -10,6 +12,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
@ -241,7 +244,12 @@ async def generic_exception_handler(request: Request, exc: Exception):
) )
def open_local_browser():
webbrowser.open_new_tab("http://localhost:5172")
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
threading.Timer(3, open_local_browser).start()
uvicorn.run(app, host="localhost", port=5172) uvicorn.run(app, host="localhost", port=5172)

View File

@ -50,12 +50,12 @@ function setupSSE(taskId) {
const maxRetries = 3; const maxRetries = 3;
const retryDelay = 2000; const retryDelay = 2000;
const container = document.getElementById('task-container');
function connect() { function connect() {
const eventSource = new EventSource(`/tasks/${taskId}/events`); const eventSource = new EventSource(`/tasks/${taskId}/events`);
currentEventSource = eventSource; currentEventSource = eventSource;
const container = document.getElementById('task-container');
let heartbeatTimer = setInterval(() => { let heartbeatTimer = setInterval(() => {
container.innerHTML += '<div class="ping">·</div>'; container.innerHTML += '<div class="ping">·</div>';
}, 5000); }, 5000);
@ -71,94 +71,20 @@ function setupSSE(taskId) {
}); });
}, 10000); }, 10000);
if (!eventSource._listenersAdded) { const handleEvent = (event, type) => {
eventSource._listenersAdded = true;
let lastResultContent = '';
eventSource.addEventListener('status', (event) => {
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
container.querySelector('.loading')?.remove(); container.querySelector('.loading')?.remove();
container.classList.add('active'); container.classList.add('active');
const welcomeMessage = document.querySelector('.welcome-message');
if (welcomeMessage) {
welcomeMessage.style.display = 'none';
}
let stepContainer = container.querySelector('.step-container'); const stepContainer = ensureStepContainer(container);
if (!stepContainer) { const { formattedContent, timestamp } = formatStepContent(data, type);
container.innerHTML = '<div class="step-container"></div>'; const step = createStepElement(type, formattedContent, timestamp);
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); stepContainer.appendChild(step);
container.scrollTo({ autoScroll(stepContainer);
top: container.scrollHeight,
behavior: 'smooth'
});
// Update task status
fetch(`/tasks/${taskId}`) fetch(`/tasks/${taskId}`)
.then(response => response.json()) .then(response => response.json())
.then(task => { .then(task => {
@ -168,236 +94,16 @@ function setupSSE(taskId) {
console.error('Status update failed:', error); console.error('Status update failed:', error);
}); });
} catch (e) { } 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) => { eventSource.addEventListener('complete', (event) => {
isTaskComplete = true;
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
clearInterval(pollInterval); clearInterval(pollInterval);
container.innerHTML += ` container.innerHTML += `
@ -408,7 +114,6 @@ function setupSSE(taskId) {
`; `;
eventSource.close(); eventSource.close();
currentEventSource = null; currentEventSource = null;
lastResultContent = ''; // Clear result content
}); });
eventSource.addEventListener('error', (event) => { eventSource.addEventListener('error', (event) => {
@ -427,17 +132,9 @@ function setupSSE(taskId) {
console.error('Error handling failed:', e); console.error('Error handling failed:', e);
} }
}); });
}
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
eventSource.onerror = (err) => { eventSource.onerror = (err) => {
if (isTaskComplete) { if (eventSource.readyState === EventSource.CLOSED) return;
return;
}
console.error('SSE connection error:', err); console.error('SSE connection error:', err);
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
@ -465,32 +162,105 @@ function setupSSE(taskId) {
connect(); connect();
} }
function getEventIcon(eventType) { function loadHistory() {
switch(eventType) { fetch('/tasks')
case 'think': return '🤔'; .then(response => {
case 'tool': return '🛠️'; if (!response.ok) {
case 'act': return '🚀'; return response.text().then(text => {
case 'result': return '🏁'; throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`);
case 'error': return '❌'; });
case 'complete': return '✅'; }
case 'log': return '📝'; return response.json();
case 'run': return '⚙️'; })
default: return ''; .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) { function getEventLabel(eventType) {
switch(eventType) { const labels = {
case 'think': return 'Thinking'; 'think': 'Thinking',
case 'tool': return 'Using Tool'; 'tool': 'Using Tool',
case 'act': return 'Action'; 'act': 'Action',
case 'result': return 'Result'; 'result': 'Result',
case 'error': return 'Error'; 'error': 'Error',
case 'complete': return 'Complete'; 'complete': 'Complete',
case 'log': return 'Log'; 'log': 'Log',
case 'run': return 'Running'; 'run': 'Running'
default: return 'Info'; };
} return labels[eventType] || 'Info';
} }
function updateTaskStatus(task) { 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', () => { document.addEventListener('DOMContentLoaded', () => {
loadHistory(); loadHistory();
// Set up event listeners
document.getElementById('create-task-btn').addEventListener('click', createTask);
document.getElementById('prompt-input').addEventListener('keydown', (e) => { document.getElementById('prompt-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
@ -671,7 +286,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
// Show history button functionality
const historyToggle = document.getElementById('history-toggle'); const historyToggle = document.getElementById('history-toggle');
if (historyToggle) { if (historyToggle) {
historyToggle.addEventListener('click', () => { historyToggle.addEventListener('click', () => {
@ -683,7 +297,6 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
// Clear button functionality
const clearButton = document.getElementById('clear-btn'); const clearButton = document.getElementById('clear-btn');
if (clearButton) { if (clearButton) {
clearButton.addEventListener('click', () => { clearButton.addEventListener('click', () => {

View File

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