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 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"])

View File

@ -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

34
run.bat
View File

@ -2,15 +2,41 @@
setlocal
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...
git pull origin front-end || (
echo Failed to sync with GitHub, skipping update...
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

View File

@ -38,6 +38,7 @@ function createTask() {
}
setupSSE(data.task_id);
loadHistory();
promptInput.value = '';
})
.catch(error => {
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
@ -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 += '<div class="ping">·</div>';
}, 5000);
const pollInterval = setInterval(() => {
// Initial polling
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Polling failed:', error);
console.error('Initial status fetch failed:', error);
});
}, 10000);
const handleEvent = (event, type) => {
clearInterval(heartbeatTimer);
@ -105,20 +106,35 @@ function setupSSE(taskId) {
eventSource.addEventListener('complete', (event) => {
clearInterval(heartbeatTimer);
clearInterval(pollInterval);
try {
const data = JSON.parse(event.data);
lastResultContent = data.result || '';
container.innerHTML += `
<div class="complete">
<div> Task completed</div>
<pre>${lastResultContent}</pre>
</div>
`;
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,10 +154,27 @@ function setupSSE(taskId) {
console.error('SSE connection error:', err);
clearInterval(heartbeatTimer);
clearInterval(pollInterval);
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++;
container.innerHTML += `
<div class="warning">
@ -156,6 +189,14 @@ function setupSSE(taskId) {
</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 => {
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() {
<div class="task-meta">
${new Date(task.created_at).toLocaleString()} -
<span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}">
${task.status || '未知状态'}
${task.status || 'Unknown state'}
</span>
</div>
</div>
`).join('');
})
.catch(error => {
console.error('加载历史记录失败:', error);
console.error('Failed to load history records:', error);
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) {
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.innerHTML = `
<div class="log-line">
@ -219,6 +334,7 @@ function createStepElement(type, content, timestamp) {
<pre>${content}</pre>
</div>
`;
}
return step;
}
@ -269,13 +385,115 @@ function updateTaskStatus(task) {
if (task.status === 'completed') {
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else if (task.status === 'failed') {
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else {
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', () => {
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');
}
}
});
});

View File

@ -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;