From fa2b05b6586103f39d3f7b9be42a78f96477fa65 Mon Sep 17 00:00:00 2001 From: Feige-cn Date: Wed, 12 Mar 2025 15:45:02 +0800 Subject: [PATCH] Optimize front-end display --- app.py | 33 +++- config/config.example.toml | 5 + run.bat | 36 +++- static/main.js | 327 +++++++++++++++++++++++++++++++------ static/style.css | 173 +++++++++++++++++++- 5 files changed, 515 insertions(+), 59 deletions(-) diff --git a/app.py b/app.py index 2e1959a..0649402 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,12 @@ import asyncio import threading +import tomllib import uuid import webbrowser from datetime import datetime +from functools import partial from json import dumps +from pathlib import Path from fastapi import Body, FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware @@ -135,7 +138,7 @@ async def run_task(task_id: str, prompt: str): async def __call__(self, message): import re - # 提取 - 后面的内容 + # Extract - Subsequent Content cleaned_message = re.sub(r"^.*? - ", "", message) event_type = "log" @@ -244,12 +247,32 @@ async def generic_exception_handler(request: Request, exc: Exception): ) -def open_local_browser(): - webbrowser.open_new_tab("http://localhost:5172") +def open_local_browser(config): + webbrowser.open_new_tab(f"http://{config['host']}:{config['port']}") + + +def load_config(): + try: + config_path = Path(__file__).parent / "config" / "config.toml" + + with open(config_path, "rb") as f: + config = tomllib.load(f) + + return {"host": config["server"]["host"], "port": config["server"]["port"]} + except FileNotFoundError: + raise RuntimeError( + "Configuration file not found, please check if config/fig.toml exists" + ) + except KeyError as e: + raise RuntimeError( + f"The configuration file is missing necessary fields: {str(e)}" + ) if __name__ == "__main__": import uvicorn - threading.Timer(3, open_local_browser).start() - uvicorn.run(app, host="localhost", port=5172) + config = load_config() + open_with_config = partial(open_local_browser, config) + threading.Timer(3, open_with_config).start() + uvicorn.run(app, host=config["host"], port=config["port"]) diff --git a/config/config.example.toml b/config/config.example.toml index c1d51f0..e1a0d8b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -20,3 +20,8 @@ temperature = 0.0 model = "claude-3-5-sonnet" base_url = "https://api.openai.com/v1" api_key = "sk-..." + +# Server configuration +[server] +host = "localhost" +port = 5172 diff --git a/run.bat b/run.bat index 23bf078..d9272db 100644 --- a/run.bat +++ b/run.bat @@ -2,15 +2,41 @@ setlocal cd /d %~dp0 -call venv\Scripts\activate.bat +set "VENV_DIR=%~dp0venv" +set "PYTHON_PATH=%VENV_DIR%\python.exe" -echo Trying to sync with GitHub repository... -git pull origin front-end || ( - echo Failed to sync with GitHub, skipping update... +where git >nul 2>&1 +if !errorlevel! == 0 ( + echo Trying to sync with GitHub repository... + git pull origin front-end 2>&1 || echo Failed to sync with GitHub, skipping update... +) else ( + echo Git not detected, skipping code synchronization +) + +if not exist "%VENV_DIR%\" ( + echo Virtual environment not found, initializing installation... + python -m venv "%VENV_DIR%" || ( + echo Failed to create virtual environment, please install Python 3.12 first + pause + exit /b 1 + ) + call "%VENV_DIR%\Scripts\activate.bat" + pip install -r requirements.txt || ( + echo Dependency installation failed, please check requirements. txt + pause + exit /b 1 + ) ) echo Starting Python application... -python app.py +if not exist "%PYTHON_PATH%" ( + echo Error: Python executable file does not exist in %PYTHON_PATH% + echo Please try deleting the venv folder and running the script again + pause + exit /b 1 +) + +"%PYTHON_PATH%" "%~dp0app.py" pause endlocal diff --git a/static/main.js b/static/main.js index c219d20..9bfd2f3 100644 --- a/static/main.js +++ b/static/main.js @@ -38,6 +38,7 @@ function createTask() { } setupSSE(data.task_id); loadHistory(); + promptInput.value = ''; }) .catch(error => { container.innerHTML = `
Error: ${error.message}
`; @@ -49,6 +50,7 @@ function setupSSE(taskId) { let retryCount = 0; const maxRetries = 3; const retryDelay = 2000; + let lastResultContent = ''; const container = document.getElementById('task-container'); @@ -60,16 +62,15 @@ function setupSSE(taskId) { container.innerHTML += '
·
'; }, 5000); - const pollInterval = setInterval(() => { - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Polling failed:', error); - }); - }, 10000); + // Initial polling + fetch(`/tasks/${taskId}`) + .then(response => response.json()) + .then(task => { + updateTaskStatus(task); + }) + .catch(error => { + console.error('Initial status fetch failed:', error); + }); const handleEvent = (event, type) => { clearInterval(heartbeatTimer); @@ -105,20 +106,35 @@ function setupSSE(taskId) { eventSource.addEventListener('complete', (event) => { clearInterval(heartbeatTimer); - clearInterval(pollInterval); - container.innerHTML += ` -
-
✅ Task completed
-
${lastResultContent}
-
- `; - eventSource.close(); - currentEventSource = null; + try { + const data = JSON.parse(event.data); + lastResultContent = data.result || ''; + + container.innerHTML += ` +
+
✅ Task completed
+
${lastResultContent}
+
+ `; + + fetch(`/tasks/${taskId}`) + .then(response => response.json()) + .then(task => { + updateTaskStatus(task); + }) + .catch(error => { + console.error('Final status update failed:', error); + }); + + eventSource.close(); + currentEventSource = null; + } catch (e) { + console.error('Error handling complete event:', e); + } }); eventSource.addEventListener('error', (event) => { clearInterval(heartbeatTimer); - clearInterval(pollInterval); try { const data = JSON.parse(event.data); container.innerHTML += ` @@ -138,24 +154,49 @@ function setupSSE(taskId) { console.error('SSE connection error:', err); clearInterval(heartbeatTimer); - clearInterval(pollInterval); eventSource.close(); - if (retryCount < maxRetries) { - retryCount++; - container.innerHTML += ` -
- ⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})... -
- `; - setTimeout(connect, retryDelay); - } else { - container.innerHTML += ` -
- ⚠ Connection lost, please try refreshing the page -
- `; - } + fetch(`/tasks/${taskId}`) + .then(response => response.json()) + .then(task => { + if (task.status === 'completed' || task.status === 'failed') { + updateTaskStatus(task); + if (task.status === 'completed') { + container.innerHTML += ` +
+
✅ Task completed
+
+ `; + } else { + container.innerHTML += ` +
+ ❌ Error: ${task.error || 'Task failed'} +
+ `; + } + } else if (retryCount < maxRetries) { + retryCount++; + container.innerHTML += ` +
+ ⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})... +
+ `; + setTimeout(connect, retryDelay); + } else { + container.innerHTML += ` +
+ ⚠ Connection lost, please try refreshing the page +
+ `; + } + }) + .catch(error => { + console.error('Task status check failed:', error); + if (retryCount < maxRetries) { + retryCount++; + setTimeout(connect, retryDelay); + } + }); }; } @@ -167,7 +208,7 @@ function loadHistory() { .then(response => { if (!response.ok) { return response.text().then(text => { - throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`); + throw new Error(`request failure: ${response.status} - ${text.substring(0, 100)}`); }); } return response.json(); @@ -180,16 +221,16 @@ function loadHistory() {
${new Date(task.created_at).toLocaleString()} - - ${task.status || '未知状态'} + ${task.status || 'Unknown state'}
`).join(''); }) .catch(error => { - console.error('加载历史记录失败:', error); + console.error('Failed to load history records:', error); const listContainer = document.getElementById('task-list'); - listContainer.innerHTML = `
加载失败: ${error.message}
`; + listContainer.innerHTML = `
Load Fail: ${error.message}
`; }); } @@ -212,13 +253,88 @@ function formatStepContent(data, eventType) { function createStepElement(type, content, timestamp) { const step = document.createElement('div'); - step.className = `step-item ${type}`; - step.innerHTML = ` -
- ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: -
${content}
-
- `; + + // Executing step + const stepRegex = /Executing step (\d+)\/(\d+)/; + if (type === 'log' && stepRegex.test(content)) { + const match = content.match(stepRegex); + const currentStep = parseInt(match[1]); + const totalSteps = parseInt(match[2]); + + step.className = 'step-divider'; + step.innerHTML = ` +
${currentStep}
+
+
${currentStep}/${totalSteps}
+ `; + } else if (type === 'act') { + // Check if it contains information about file saving + const saveRegex = /Content successfully saved to (.+)/; + const match = content.match(saveRegex); + + step.className = `step-item ${type}`; + + if (match && match[1]) { + const filePath = match[1].trim(); + const fileName = filePath.split('/').pop(); + const fileExtension = fileName.split('.').pop().toLowerCase(); + + // Handling different types of files + let fileInteractionHtml = ''; + + if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(fileExtension)) { + fileInteractionHtml = ` +
+ ${fileName} + ⬇️ 下载图片 +
+ `; + } else if (['mp3', 'wav', 'ogg'].includes(fileExtension)) { + fileInteractionHtml = ` +
+ + ⬇️ 下载音频 +
+ `; + } else if (fileExtension === 'py') { + fileInteractionHtml = ` +
+ + ⬇️ 下载文件 +
+ `; + } else { + fileInteractionHtml = ` +
+ ⬇️ 下载文件: ${fileName} +
+ `; + } + + step.innerHTML = ` +
+ ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: +
${content}
+ ${fileInteractionHtml} +
+ `; + } else { + step.innerHTML = ` +
+ ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: +
${content}
+
+ `; + } + } else { + step.className = `step-item ${type}`; + step.innerHTML = ` +
+ ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: +
${content}
+
+ `; + } return step; } @@ -269,13 +385,115 @@ function updateTaskStatus(task) { if (task.status === 'completed') { statusBar.innerHTML = `✅ Task completed`; + + if (currentEventSource) { + currentEventSource.close(); + currentEventSource = null; + } } else if (task.status === 'failed') { statusBar.innerHTML = `❌ Task failed: ${task.error || 'Unknown error'}`; + + if (currentEventSource) { + currentEventSource.close(); + currentEventSource = null; + } } else { statusBar.innerHTML = `⚙️ Task running: ${task.status}`; } } +// Display full screen image +function showFullImage(imageSrc) { + const modal = document.getElementById('image-modal'); + if (!modal) { + const modalDiv = document.createElement('div'); + modalDiv.id = 'image-modal'; + modalDiv.className = 'image-modal'; + modalDiv.innerHTML = ` + × + + `; + document.body.appendChild(modalDiv); + + const closeBtn = modalDiv.querySelector('.close-modal'); + closeBtn.addEventListener('click', () => { + modalDiv.classList.remove('active'); + }); + + modalDiv.addEventListener('click', (e) => { + if (e.target === modalDiv) { + modalDiv.classList.remove('active'); + } + }); + + setTimeout(() => modalDiv.classList.add('active'), 10); + } else { + document.getElementById('full-image').src = imageSrc; + modal.classList.add('active'); + } +} + +// Simulate running Python files +function simulateRunPython(filePath) { + let modal = document.getElementById('python-modal'); + if (!modal) { + modal = document.createElement('div'); + modal.id = 'python-modal'; + modal.className = 'python-modal'; + modal.innerHTML = ` +
+
×
+
Loading Python file contents...
+
+ `; + document.body.appendChild(modal); + + const closeBtn = modal.querySelector('.close-modal'); + closeBtn.addEventListener('click', () => { + modal.classList.remove('active'); + }); + } + + modal.classList.add('active'); + + // Load Python file content + fetch(filePath) + .then(response => response.text()) + .then(code => { + const outputDiv = modal.querySelector('.python-output'); + outputDiv.innerHTML = ''; + + const codeElement = document.createElement('pre'); + codeElement.textContent = code; + codeElement.style.marginBottom = '20px'; + codeElement.style.padding = '10px'; + codeElement.style.borderBottom = '1px solid #444'; + outputDiv.appendChild(codeElement); + + // Add simulation run results + const resultElement = document.createElement('div'); + resultElement.innerHTML = ` +
+ > Simulated operation output:
+
+#This is the result of Python code simulation run
+#The actual operational results may vary
+
+# Running ${filePath.split('/').pop()}...
+print("Hello from Python Simulated environment!")
+
+# Code execution completed
+
+ `; + outputDiv.appendChild(resultElement); + }) + .catch(error => { + console.error('Error loading Python file:', error); + const outputDiv = modal.querySelector('.python-output'); + outputDiv.innerHTML = `Error loading file: ${error.message}`; + }); +} + document.addEventListener('DOMContentLoaded', () => { loadHistory(); @@ -304,4 +522,19 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('prompt-input').focus(); }); } + + // Add keyboard event listener to close modal boxes + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + const imageModal = document.getElementById('image-modal'); + if (imageModal && imageModal.classList.contains('active')) { + imageModal.classList.remove('active'); + } + + const pythonModal = document.getElementById('python-modal'); + if (pythonModal && pythonModal.classList.contains('active')) { + pythonModal.classList.remove('active'); + } + } + }); }); diff --git a/static/style.css b/static/style.css index 5aaec28..51c60ee 100644 --- a/static/style.css +++ b/static/style.css @@ -181,7 +181,6 @@ button:hover { display: flex; flex-direction: column; gap: 10px; - padding: 15px; width: 100%; max-height: calc(100vh - 200px); overflow-y: auto; @@ -270,7 +269,7 @@ button:hover { .step-item pre { padding: 10px; border-radius: 4px; - margin: 10px 0; + margin-left: 20px; overflow-x: hidden; font-family: 'Courier New', monospace; font-size: 0.9em; @@ -300,6 +299,176 @@ button:hover { } } +/* Step division line style */ +.step-divider { + display: flex; + align-items: center; + width: 100%; + margin: 15px 0; + position: relative; +} + +.step-circle { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: var(--primary-color); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2em; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + z-index: 2; + flex-shrink: 0; +} + +/* File interaction style */ +.file-interaction { + margin-top: 15px; + padding: 10px; + border-radius: 6px; + background-color: #f5f7fa; + border: 1px solid var(--border-color); +} + +.download-link { + display: inline-block; + padding: 8px 16px; + background-color: var(--primary-color); + color: white; + text-decoration: none; + border-radius: 4px; + font-size: 0.9em; + transition: background-color 0.2s; +} + +.download-link:hover { + background-color: var(--primary-hover); +} + +.preview-image { + max-width: 100%; + max-height: 200px; + margin-bottom: 10px; + border-radius: 4px; + cursor: pointer; + transition: transform 0.2s; +} + +.preview-image:hover { + transform: scale(1.02); +} + +.audio-player { + display: flex; + flex-direction: column; + gap: 10px; +} + +.audio-player audio { + width: 100%; + margin-bottom: 10px; +} + +.run-button { + display: inline-block; + padding: 8px 16px; + background-color: var(--success-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + margin-right: 10px; + transition: background-color 0.2s; +} + +.run-button:hover { + background-color: #218838; +} + +/* Full screen image modal box */ +.image-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.image-modal.active { + display: flex; +} + +.modal-content { + max-width: 90%; + max-height: 90%; +} + +.close-modal { + position: absolute; + top: 20px; + right: 30px; + color: white; + font-size: 30px; + cursor: pointer; +} + +/* Python runs simulation modal boxes */ +.python-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.python-modal.active { + display: flex; +} + +.python-console { + width: 80%; + height: 80%; + background-color: #1e1e1e; + color: #f8f8f8; + border-radius: 8px; + padding: 15px; + font-family: 'Courier New', monospace; + overflow: auto; +} + +.python-output { + white-space: pre-wrap; + line-height: 1.5; +} + +.step-line { + flex-grow: 1; + height: 2px; + background-color: var(--border-color); + margin-left: 10px; +} + +.step-info { + margin-left: 15px; + font-weight: bold; + color: var(--text-light); + font-size: 0.9em; +} + .step-item strong { display: block; margin-bottom: 8px;