commit
8280b68462
8
app.py
8
app.py
@ -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)
|
||||||
|
605
static/main.js
605
static/main.js
@ -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', () => {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user