diff --git a/app.py b/app.py index b827484..2e1959a 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ import asyncio +import threading import uuid +import webbrowser from datetime import datetime from json import dumps @@ -10,6 +12,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel + app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -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__": import uvicorn + threading.Timer(3, open_local_browser).start() uvicorn.run(app, host="localhost", port=5172) diff --git a/static/main.js b/static/main.js index 49d5dd7..c219d20 100644 --- a/static/main.js +++ b/static/main.js @@ -50,12 +50,12 @@ function setupSSE(taskId) { const maxRetries = 3; const retryDelay = 2000; + const container = document.getElementById('task-container'); + function connect() { const eventSource = new EventSource(`/tasks/${taskId}/events`); currentEventSource = eventSource; - const container = document.getElementById('task-container'); - let heartbeatTimer = setInterval(() => { container.innerHTML += '
¡
'; }, 5000); @@ -71,94 +71,20 @@ function setupSSE(taskId) { }); }, 10000); - if (!eventSource._listenersAdded) { - eventSource._listenersAdded = true; - - let lastResultContent = ''; - eventSource.addEventListener('status', (event) => { + const handleEvent = (event, type) => { clearInterval(heartbeatTimer); try { const data = JSON.parse(event.data); container.querySelector('.loading')?.remove(); container.classList.add('active'); - const welcomeMessage = document.querySelector('.welcome-message'); - if (welcomeMessage) { - welcomeMessage.style.display = 'none'; - } - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - // Save result content - if (data.steps && data.steps.length > 0) { - // Iterate through all steps, find the last result type - for (let i = data.steps.length - 1; i >= 0; i--) { - if (data.steps[i].type === 'result') { - lastResultContent = data.steps[i].result; - break; - } - } - } - - // Parse and display each step with proper formatting - stepContainer.innerHTML = data.steps.map(step => { - const content = step.result; - const timestamp = new Date().toLocaleTimeString(); - return ` -
-
- ${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}: -
${content}
-
-
- `; - }).join(''); - - // Auto-scroll to bottom - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - } catch (e) { - console.error('Status update failed:', e); - } - }); - - // Add handler for think event - eventSource.addEventListener('think', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item think'; - step.innerHTML = ` -
- ${getEventIcon('think')} [${timestamp}] ${getEventLabel('think')}: -
${content}
-
- `; + const stepContainer = ensureStepContainer(container); + const { formattedContent, timestamp } = formatStepContent(data, type); + const step = createStepElement(type, formattedContent, timestamp); stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); + autoScroll(stepContainer); - // Update task status fetch(`/tasks/${taskId}`) .then(response => response.json()) .then(task => { @@ -168,236 +94,16 @@ function setupSSE(taskId) { console.error('Status update failed:', error); }); } catch (e) { - console.error('Think event handling failed:', e); + console.error(`Error handling ${type} event:`, e); } + }; + + const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']; + eventTypes.forEach(type => { + eventSource.addEventListener(type, (event) => handleEvent(event, type)); }); - // Add handler for tool event - eventSource.addEventListener('tool', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item tool'; - step.innerHTML = ` -
- ${getEventIcon('tool')} [${timestamp}] ${getEventLabel('tool')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Tool event handling failed:', e); - } - }); - - // Add handler for act event - eventSource.addEventListener('act', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item act'; - step.innerHTML = ` -
- ${getEventIcon('act')} [${timestamp}] ${getEventLabel('act')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Act event handling failed:', e); - } - }); - - // Add handler for log event - eventSource.addEventListener('log', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item log'; - step.innerHTML = ` -
- ${getEventIcon('log')} [${timestamp}] ${getEventLabel('log')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Log event handling failed:', e); - } - }); - - eventSource.addEventListener('run', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item run'; - step.innerHTML = ` -
- ${getEventIcon('run')} [${timestamp}] ${getEventLabel('run')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Run event handling failed:', e); - } - }); - - eventSource.addEventListener('message', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - // Create new step element - const step = document.createElement('div'); - step.className = `step-item ${data.type || 'step'}`; - - // Format content and timestamp - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - step.innerHTML = ` -
- ${getEventIcon(data.type)} [${timestamp}] ${getEventLabel(data.type)}: -
${content}
-
- `; - - // Add step to container with animation - stepContainer.prepend(step); - setTimeout(() => { - step.classList.add('show'); - }, 10); - - // Auto-scroll to bottom - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - } catch (e) { - console.error('Message handling failed:', e); - } - }); - - let isTaskComplete = false; - eventSource.addEventListener('complete', (event) => { - isTaskComplete = true; clearInterval(heartbeatTimer); clearInterval(pollInterval); container.innerHTML += ` @@ -408,7 +114,6 @@ function setupSSE(taskId) { `; eventSource.close(); currentEventSource = null; - lastResultContent = ''; // Clear result content }); eventSource.addEventListener('error', (event) => { @@ -427,17 +132,9 @@ function setupSSE(taskId) { console.error('Error handling failed:', e); } }); - } - - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); eventSource.onerror = (err) => { - if (isTaskComplete) { - return; - } + if (eventSource.readyState === EventSource.CLOSED) return; console.error('SSE connection error:', err); clearInterval(heartbeatTimer); @@ -465,32 +162,105 @@ function setupSSE(taskId) { connect(); } -function getEventIcon(eventType) { - switch(eventType) { - case 'think': return '🤔'; - case 'tool': return '🛠️'; - case 'act': return '🚀'; - case 'result': return '🏁'; - case 'error': return '❌'; - case 'complete': return '✅'; - case 'log': return '📝'; - case 'run': return '⚙️'; - default: return 'ℹ️'; +function loadHistory() { + fetch('/tasks') + .then(response => { + if (!response.ok) { + return response.text().then(text => { + throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`); + }); + } + return response.json(); + }) + .then(tasks => { + const listContainer = document.getElementById('task-list'); + listContainer.innerHTML = tasks.map(task => ` +
+
${task.prompt}
+
+ ${new Date(task.created_at).toLocaleString()} - + + ${task.status || '未知状态'} + +
+
+ `).join(''); + }) + .catch(error => { + console.error('加载历史记录失败:', error); + const listContainer = document.getElementById('task-list'); + listContainer.innerHTML = `
加载失败: ${error.message}
`; + }); +} + + +function ensureStepContainer(container) { + let stepContainer = container.querySelector('.step-container'); + if (!stepContainer) { + container.innerHTML = '
'; + stepContainer = container.querySelector('.step-container'); } + return stepContainer; +} + +function formatStepContent(data, eventType) { + return { + formattedContent: data.result, + timestamp: new Date().toLocaleTimeString() + }; +} + +function createStepElement(type, content, timestamp) { + const step = document.createElement('div'); + step.className = `step-item ${type}`; + step.innerHTML = ` +
+ ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: +
${content}
+
+ `; + return step; +} + +function autoScroll(element) { + requestAnimationFrame(() => { + element.scrollTo({ + top: element.scrollHeight, + behavior: 'smooth' + }); + }); + setTimeout(() => { + element.scrollTop = element.scrollHeight; + }, 100); +} + + +function getEventIcon(eventType) { + const icons = { + 'think': '🤔', + 'tool': '🛠️', + 'act': '🚀', + 'result': '🏁', + 'error': '❌', + 'complete': '✅', + 'log': '📝', + 'run': '⚙️' + }; + return icons[eventType] || 'ℹ️'; } function getEventLabel(eventType) { - switch(eventType) { - case 'think': return 'Thinking'; - case 'tool': return 'Using Tool'; - case 'act': return 'Action'; - case 'result': return 'Result'; - case 'error': return 'Error'; - case 'complete': return 'Complete'; - case 'log': return 'Log'; - case 'run': return 'Running'; - default: return 'Info'; - } + const labels = { + 'think': 'Thinking', + 'tool': 'Using Tool', + 'act': 'Action', + 'result': 'Result', + 'error': 'Error', + 'complete': 'Complete', + 'log': 'Log', + 'run': 'Running' + }; + return labels[eventType] || 'Info'; } function updateTaskStatus(task) { @@ -506,164 +276,9 @@ function updateTaskStatus(task) { } } -function loadHistory() { - fetch('/tasks') - .then(response => { - if (!response.ok) { - throw new Error('Failed to load history'); - } - return response.json(); - }) - .then(tasks => { - const historyContainer = document.getElementById('history-container'); - if (!historyContainer) return; - - historyContainer.innerHTML = ''; - - if (tasks.length === 0) { - historyContainer.innerHTML = '
No recent tasks
'; - return; - } - - const historyList = document.createElement('div'); - historyList.className = 'history-list'; - - tasks.forEach(task => { - const taskItem = document.createElement('div'); - taskItem.className = `history-item ${task.status}`; - taskItem.innerHTML = ` -
${task.prompt}
-
- ${new Date(task.created_at).toLocaleString()} - ${getStatusIcon(task.status)} -
- `; - taskItem.addEventListener('click', () => { - loadTask(task.id); - }); - historyList.appendChild(taskItem); - }); - - historyContainer.appendChild(historyList); - }) - .catch(error => { - console.error('Failed to load history:', error); - const historyContainer = document.getElementById('history-container'); - if (historyContainer) { - historyContainer.innerHTML = `
Failed to load history: ${error.message}
`; - } - }); -} - -function getStatusIcon(status) { - switch(status) { - case 'completed': return '✅'; - case 'failed': return '❌'; - case 'running': return '⚙️'; - default: return '⏳'; - } -} - -function loadTask(taskId) { - if (currentEventSource) { - currentEventSource.close(); - currentEventSource = null; - } - - const container = document.getElementById('task-container'); - container.innerHTML = '
Loading task...
'; - document.getElementById('input-container').classList.add('bottom'); - - fetch(`/tasks/${taskId}`) - .then(response => { - if (!response.ok) { - throw new Error('Failed to load task'); - } - return response.json(); - }) - .then(task => { - if (task.status === 'running') { - setupSSE(taskId); - } else { - displayTask(task); - } - }) - .catch(error => { - console.error('Failed to load task:', error); - container.innerHTML = `
Failed to load task: ${error.message}
`; - }); -} - -function displayTask(task) { - const container = document.getElementById('task-container'); - container.innerHTML = ''; - container.classList.add('active'); - - const welcomeMessage = document.querySelector('.welcome-message'); - if (welcomeMessage) { - welcomeMessage.style.display = 'none'; - } - - const stepContainer = document.createElement('div'); - stepContainer.className = 'step-container'; - - if (task.steps && task.steps.length > 0) { - task.steps.forEach(step => { - const stepItem = document.createElement('div'); - stepItem.className = `step-item ${step.type || 'step'}`; - - const content = step.result; - const timestamp = new Date(step.timestamp || task.created_at).toLocaleTimeString(); - - stepItem.innerHTML = ` -
- ${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}: -
${content}
-
- `; - - stepContainer.appendChild(stepItem); - }); - } else { - stepContainer.innerHTML = '
No steps recorded for this task
'; - } - - container.appendChild(stepContainer); - - if (task.status === 'completed') { - let lastResultContent = ''; - if (task.steps && task.steps.length > 0) { - for (let i = task.steps.length - 1; i >= 0; i--) { - if (task.steps[i].type === 'result') { - lastResultContent = task.steps[i].result; - break; - } - } - } - - container.innerHTML += ` -
-
✅ Task completed
-
${lastResultContent}
-
- `; - } else if (task.status === 'failed') { - container.innerHTML += ` -
- ❌ Error: ${task.error || 'Unknown error'} -
- `; - } - - updateTaskStatus(task); -} - -// Initialize the app when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { loadHistory(); - // Set up event listeners - document.getElementById('create-task-btn').addEventListener('click', createTask); document.getElementById('prompt-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -671,7 +286,6 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // Show history button functionality const historyToggle = document.getElementById('history-toggle'); if (historyToggle) { historyToggle.addEventListener('click', () => { @@ -683,7 +297,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // Clear button functionality const clearButton = document.getElementById('clear-btn'); if (clearButton) { clearButton.addEventListener('click', () => { diff --git a/static/style.css b/static/style.css index 776304e..5aaec28 100644 --- a/static/style.css +++ b/static/style.css @@ -28,6 +28,7 @@ body { .container { display: flex; min-height: 100vh; + min-width: 0; width: 90%; margin: 0 auto; padding: 20px; @@ -62,13 +63,13 @@ body { .task-container { @extend .card; width: 100%; + max-width: 100%; position: relative; min-height: 300px; display: flex; flex-direction: column; justify-content: center; align-items: center; - padding: 20px; overflow: auto; height: 100%; } @@ -177,11 +178,15 @@ button:hover { } .step-container { - display: block; + display: flex; + flex-direction: column; + gap: 10px; padding: 15px; width: 100%; - max-height: calc(100vh - 300px); + max-height: calc(100vh - 200px); overflow-y: auto; + max-width: 100%; + overflow-x: hidden; } .step-item { @@ -255,10 +260,9 @@ button:hover { border-left: 4px solid var(--text-light); } -/* 删除重复的样式定义 */ - .log-prefix { font-weight: bold; + white-space: nowrap; margin-bottom: 5px; color: #666; } @@ -267,15 +271,14 @@ button:hover { padding: 10px; border-radius: 4px; margin: 10px 0; - overflow-x: auto; - padding: 10px; - border-radius: 4px; - margin: 10px 0; - overflow-x: auto; + overflow-x: hidden; font-family: 'Courier New', monospace; font-size: 0.9em; line-height: 1.4; white-space: pre-wrap; + word-wrap: break-word; + word-break: break-all; + max-width: 100%; color: var(--text-color); background: var(--bg-color); @@ -336,3 +339,15 @@ button:hover { border-radius: 4px; margin: 10px 0; } + +pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; +} + +.complete pre { + max-width: 100%; + white-space: pre-wrap; + word-break: break-word; +}