Optimize front-end display

This commit is contained in:
Feige-cn 2025-03-12 15:45:02 +08:00
parent 7b96e12858
commit fa2b05b658
5 changed files with 515 additions and 59 deletions

33
app.py
View File

@ -1,9 +1,12 @@
import asyncio import asyncio
import threading import threading
import tomllib
import uuid import uuid
import webbrowser import webbrowser
from datetime import datetime from datetime import datetime
from functools import partial
from json import dumps from json import dumps
from pathlib import Path
from fastapi import Body, FastAPI, HTTPException, Request from fastapi import Body, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -135,7 +138,7 @@ async def run_task(task_id: str, prompt: str):
async def __call__(self, message): async def __call__(self, message):
import re import re
# 提取 - 后面的内容 # Extract - Subsequent Content
cleaned_message = re.sub(r"^.*? - ", "", message) cleaned_message = re.sub(r"^.*? - ", "", message)
event_type = "log" event_type = "log"
@ -244,12 +247,32 @@ async def generic_exception_handler(request: Request, exc: Exception):
) )
def open_local_browser(): def open_local_browser(config):
webbrowser.open_new_tab("http://localhost:5172") 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__": if __name__ == "__main__":
import uvicorn import uvicorn
threading.Timer(3, open_local_browser).start() config = load_config()
uvicorn.run(app, host="localhost", port=5172) open_with_config = partial(open_local_browser, config)
threading.Timer(3, open_with_config).start()
uvicorn.run(app, host=config["host"], port=config["port"])

View File

@ -20,3 +20,8 @@ temperature = 0.0
model = "claude-3-5-sonnet" model = "claude-3-5-sonnet"
base_url = "https://api.openai.com/v1" base_url = "https://api.openai.com/v1"
api_key = "sk-..." api_key = "sk-..."
# Server configuration
[server]
host = "localhost"
port = 5172

34
run.bat
View File

@ -2,15 +2,41 @@
setlocal setlocal
cd /d %~dp0 cd /d %~dp0
call venv\Scripts\activate.bat set "VENV_DIR=%~dp0venv"
set "PYTHON_PATH=%VENV_DIR%\python.exe"
where git >nul 2>&1
if !errorlevel! == 0 (
echo Trying to sync with GitHub repository... echo Trying to sync with GitHub repository...
git pull origin front-end || ( git pull origin front-end 2>&1 || echo Failed to sync with GitHub, skipping update...
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... 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 pause
endlocal endlocal

View File

@ -38,6 +38,7 @@ function createTask() {
} }
setupSSE(data.task_id); setupSSE(data.task_id);
loadHistory(); loadHistory();
promptInput.value = '';
}) })
.catch(error => { .catch(error => {
container.innerHTML = `<div class="error">Error: ${error.message}</div>`; container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
@ -49,6 +50,7 @@ function setupSSE(taskId) {
let retryCount = 0; let retryCount = 0;
const maxRetries = 3; const maxRetries = 3;
const retryDelay = 2000; const retryDelay = 2000;
let lastResultContent = '';
const container = document.getElementById('task-container'); const container = document.getElementById('task-container');
@ -60,16 +62,15 @@ function setupSSE(taskId) {
container.innerHTML += '<div class="ping">·</div>'; container.innerHTML += '<div class="ping">·</div>';
}, 5000); }, 5000);
const pollInterval = setInterval(() => { // Initial polling
fetch(`/tasks/${taskId}`) fetch(`/tasks/${taskId}`)
.then(response => response.json()) .then(response => response.json())
.then(task => { .then(task => {
updateTaskStatus(task); updateTaskStatus(task);
}) })
.catch(error => { .catch(error => {
console.error('Polling failed:', error); console.error('Initial status fetch failed:', error);
}); });
}, 10000);
const handleEvent = (event, type) => { const handleEvent = (event, type) => {
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
@ -105,20 +106,35 @@ function setupSSE(taskId) {
eventSource.addEventListener('complete', (event) => { eventSource.addEventListener('complete', (event) => {
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
clearInterval(pollInterval); try {
const data = JSON.parse(event.data);
lastResultContent = data.result || '';
container.innerHTML += ` container.innerHTML += `
<div class="complete"> <div class="complete">
<div> Task completed</div> <div> Task completed</div>
<pre>${lastResultContent}</pre> <pre>${lastResultContent}</pre>
</div> </div>
`; `;
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Final status update failed:', error);
});
eventSource.close(); eventSource.close();
currentEventSource = null; currentEventSource = null;
} catch (e) {
console.error('Error handling complete event:', e);
}
}); });
eventSource.addEventListener('error', (event) => { eventSource.addEventListener('error', (event) => {
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
clearInterval(pollInterval);
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
container.innerHTML += ` container.innerHTML += `
@ -138,10 +154,27 @@ function setupSSE(taskId) {
console.error('SSE connection error:', err); console.error('SSE connection error:', err);
clearInterval(heartbeatTimer); clearInterval(heartbeatTimer);
clearInterval(pollInterval);
eventSource.close(); eventSource.close();
if (retryCount < maxRetries) { fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
if (task.status === 'completed' || task.status === 'failed') {
updateTaskStatus(task);
if (task.status === 'completed') {
container.innerHTML += `
<div class="complete">
<div> Task completed</div>
</div>
`;
} else {
container.innerHTML += `
<div class="error">
Error: ${task.error || 'Task failed'}
</div>
`;
}
} else if (retryCount < maxRetries) {
retryCount++; retryCount++;
container.innerHTML += ` container.innerHTML += `
<div class="warning"> <div class="warning">
@ -156,6 +189,14 @@ function setupSSE(taskId) {
</div> </div>
`; `;
} }
})
.catch(error => {
console.error('Task status check failed:', error);
if (retryCount < maxRetries) {
retryCount++;
setTimeout(connect, retryDelay);
}
});
}; };
} }
@ -167,7 +208,7 @@ function loadHistory() {
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
return response.text().then(text => { 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(); return response.json();
@ -180,16 +221,16 @@ function loadHistory() {
<div class="task-meta"> <div class="task-meta">
${new Date(task.created_at).toLocaleString()} - ${new Date(task.created_at).toLocaleString()} -
<span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}"> <span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}">
${task.status || '未知状态'} ${task.status || 'Unknown state'}
</span> </span>
</div> </div>
</div> </div>
`).join(''); `).join('');
}) })
.catch(error => { .catch(error => {
console.error('加载历史记录失败:', error); console.error('Failed to load history records:', error);
const listContainer = document.getElementById('task-list'); const listContainer = document.getElementById('task-list');
listContainer.innerHTML = `<div class="error">加载失败: ${error.message}</div>`; listContainer.innerHTML = `<div class="error">Load Fail: ${error.message}</div>`;
}); });
} }
@ -212,6 +253,80 @@ function formatStepContent(data, eventType) {
function createStepElement(type, content, timestamp) { function createStepElement(type, content, timestamp) {
const step = document.createElement('div'); const step = document.createElement('div');
// 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 = `
<div class="step-circle">${currentStep}</div>
<div class="step-line"></div>
<div class="step-info">${currentStep}/${totalSteps}</div>
`;
} 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 = `
<div class="file-interaction image-preview">
<img src="${filePath}" alt="${fileName}" class="preview-image" onclick="showFullImage('${filePath}')">
<a href="${filePath}" download="${fileName}" class="download-link"> 下载图片</a>
</div>
`;
} else if (['mp3', 'wav', 'ogg'].includes(fileExtension)) {
fileInteractionHtml = `
<div class="file-interaction audio-player">
<audio controls src="${filePath}"></audio>
<a href="${filePath}" download="${fileName}" class="download-link"> 下载音频</a>
</div>
`;
} else if (fileExtension === 'py') {
fileInteractionHtml = `
<div class="file-interaction code-file">
<button onclick="simulateRunPython('${filePath}')" class="run-button"> 模拟运行</button>
<a href="${filePath}" download="${fileName}" class="download-link"> 下载文件</a>
</div>
`;
} else {
fileInteractionHtml = `
<div class="file-interaction">
<a href="${filePath}" download="${fileName}" class="download-link"> 下载文件: ${fileName}</a>
</div>
`;
}
step.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
${fileInteractionHtml}
</div>
`;
} else {
step.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
</div>
`;
}
} else {
step.className = `step-item ${type}`; step.className = `step-item ${type}`;
step.innerHTML = ` step.innerHTML = `
<div class="log-line"> <div class="log-line">
@ -219,6 +334,7 @@ function createStepElement(type, content, timestamp) {
<pre>${content}</pre> <pre>${content}</pre>
</div> </div>
`; `;
}
return step; return step;
} }
@ -269,13 +385,115 @@ function updateTaskStatus(task) {
if (task.status === 'completed') { if (task.status === 'completed') {
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`; statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else if (task.status === 'failed') { } else if (task.status === 'failed') {
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`; statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else { } else {
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`; statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
} }
} }
// 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 = `
<span class="close-modal">&times;</span>
<img src="${imageSrc}" class="modal-content" id="full-image">
`;
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 = `
<div class="python-console">
<div class="close-modal">&times;</div>
<div class="python-output">Loading Python file contents...</div>
</div>
`;
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 = `
<div style="color: #4CAF50; margin-top: 10px; margin-bottom: 10px;">
> Simulated operation output:</div>
<pre style="color: #f8f8f8;">
#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
</pre>
`;
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', () => { document.addEventListener('DOMContentLoaded', () => {
loadHistory(); loadHistory();
@ -304,4 +522,19 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('prompt-input').focus(); 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');
}
}
});
}); });

View File

@ -181,7 +181,6 @@ button:hover {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
padding: 15px;
width: 100%; width: 100%;
max-height: calc(100vh - 200px); max-height: calc(100vh - 200px);
overflow-y: auto; overflow-y: auto;
@ -270,7 +269,7 @@ button:hover {
.step-item pre { .step-item pre {
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;
margin: 10px 0; margin-left: 20px;
overflow-x: hidden; overflow-x: hidden;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 0.9em; 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 { .step-item strong {
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;