Optimize front-end display
This commit is contained in:
parent
7b96e12858
commit
fa2b05b658
33
app.py
33
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"])
|
||||
|
@ -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
|
||||
|
36
run.bat
36
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
|
||||
|
255
static/main.js
255
static/main.js
@ -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">×</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">×</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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
173
static/style.css
173
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user