Compare commits

...

15 Commits

Author SHA1 Message Date
mannaandpoem
6160a5d05b
Merge pull request #782 from Feige-cn/front-end
Front endAdd page LLM configuration settings
2025-03-19 16:22:30 +08:00
Feige-cn
b63a0301dc Add page LLM configuration settings 2025-03-18 01:00:01 +08:00
Feige-cn
d0a62e65c7 Add page LLM configuration settings 2025-03-18 00:57:52 +08:00
Feige-cn
6243591e7f Add page LLM configuration settings 2025-03-18 00:57:02 +08:00
Feige-cn
8439fdc7ab Add page LLM configuration settings 2025-03-18 00:54:47 +08:00
Isaac
38e34219d3
Merge pull request #565 from Feige-cn/front-end
fix download bug
2025-03-13 16:34:57 +08:00
Feige-cn
693f6a90ad fix download bug 2025-03-13 11:06:49 +08:00
xiangjinyu
d847b550d6 add max_steps 2025-03-12 19:22:32 +08:00
xiangjinyu
5bba31db3e update terminate prompt 2025-03-12 19:21:13 +08:00
xiangjinyu
548c402316 add max_observe in toolcall and manus 2025-03-12 17:23:08 +08:00
xiangjinyu
c96c016feb update browser-use window size 2025-03-12 17:21:06 +08:00
Isaac
728fdaffd6
Merge pull request #520 from Feige-cn/front-end
Optimize front-end display
2025-03-12 15:53:22 +08:00
Feige-cn
fa2b05b658 Optimize front-end display 2025-03-12 15:45:02 +08:00
Isaac
7b96e12858
Merge pull request #456 from Feige-cn/front-end
Synchronize built-in environment
2025-03-11 13:58:00 +08:00
Feige-cn
62f937c1ca Synchronize built-in environment 2025-03-11 13:48:00 +08:00
10 changed files with 1012 additions and 60 deletions

107
app.py
View File

@ -1,13 +1,22 @@
import asyncio import asyncio
import os
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
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.responses import (
FileResponse,
HTMLResponse,
JSONResponse,
StreamingResponse,
)
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel
@ -90,6 +99,14 @@ async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request}) return templates.TemplateResponse("index.html", {"request": request})
@app.get("/download")
async def download_file(file_path: str):
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(file_path, filename=os.path.basename(file_path))
@app.post("/tasks") @app.post("/tasks")
async def create_task(prompt: str = Body(..., embed=True)): async def create_task(prompt: str = Body(..., embed=True)):
task = task_manager.create_task(prompt) task = task_manager.create_task(prompt)
@ -107,7 +124,6 @@ async def run_task(task_id: str, prompt: str):
agent = Manus( agent = Manus(
name="Manus", name="Manus",
description="A versatile agent that can solve various tasks using multiple tools", description="A versatile agent that can solve various tasks using multiple tools",
max_steps=30,
) )
async def on_think(thought): async def on_think(thought):
@ -135,7 +151,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"
@ -237,6 +253,61 @@ async def get_task(task_id: str):
return task_manager.tasks[task_id] return task_manager.tasks[task_id]
@app.get("/config/status")
async def check_config_status():
config_path = Path(__file__).parent / "config" / "config.toml"
example_config_path = Path(__file__).parent / "config" / "config.example.toml"
if config_path.exists():
return {"status": "exists"}
elif example_config_path.exists():
try:
with open(example_config_path, "rb") as f:
example_config = tomllib.load(f)
return {"status": "missing", "example_config": example_config}
except Exception as e:
return {"status": "error", "message": str(e)}
else:
return {"status": "no_example"}
@app.post("/config/save")
async def save_config(config_data: dict = Body(...)):
try:
config_dir = Path(__file__).parent / "config"
config_dir.mkdir(exist_ok=True)
config_path = config_dir / "config.toml"
toml_content = ""
if "llm" in config_data:
toml_content += "# Global LLM configuration\n[llm]\n"
llm_config = config_data["llm"]
for key, value in llm_config.items():
if key != "vision":
if isinstance(value, str):
toml_content += f'{key} = "{value}"\n'
else:
toml_content += f"{key} = {value}\n"
if "server" in config_data:
toml_content += "\n# Server configuration\n[server]\n"
server_config = config_data["server"]
for key, value in server_config.items():
if isinstance(value, str):
toml_content += f'{key} = "{value}"\n'
else:
toml_content += f"{key} = {value}\n"
with open(config_path, "w", encoding="utf-8") as f:
f.write(toml_content)
return {"status": "success"}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception): async def generic_exception_handler(request: Request, exc: Exception):
return JSONResponse( return JSONResponse(
@ -244,12 +315,34 @@ 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"
if not config_path.exists():
return {"host": "localhost", "port": 5172}
with open(config_path, "rb") as f:
config = tomllib.load(f)
return {"host": config["server"]["host"], "port": config["server"]["port"]}
except FileNotFoundError:
return {"host": "localhost", "port": 5172}
except KeyError as e:
print(
f"The configuration file is missing necessary fields: {str(e)}, use default configuration"
)
return {"host": "localhost", "port": 5172}
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

@ -26,6 +26,9 @@ class Manus(ToolCallAgent):
system_prompt: str = SYSTEM_PROMPT system_prompt: str = SYSTEM_PROMPT
next_step_prompt: str = NEXT_STEP_PROMPT next_step_prompt: str = NEXT_STEP_PROMPT
max_observe: int = 2000
max_steps: int = 20
# Add general-purpose tools to the tool collection # Add general-purpose tools to the tool collection
available_tools: ToolCollection = Field( available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection( default_factory=lambda: ToolCollection(

View File

@ -1,5 +1,5 @@
import json import json
from typing import Any, List, Literal from typing import Any, List, Literal, Optional, Union
from pydantic import Field from pydantic import Field
@ -31,6 +31,7 @@ class ToolCallAgent(ReActAgent):
tool_calls: List[ToolCall] = Field(default_factory=list) tool_calls: List[ToolCall] = Field(default_factory=list)
max_steps: int = 30 max_steps: int = 30
max_observe: Optional[Union[int, bool]] = None
async def think(self) -> bool: async def think(self) -> bool:
"""Process current state and decide next actions using tools""" """Process current state and decide next actions using tools"""
@ -114,6 +115,9 @@ class ToolCallAgent(ReActAgent):
f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}" f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}"
) )
if self.max_observe:
result = result[: self.max_observe]
# Add tool response to memory # Add tool response to memory
tool_msg = Message.tool_message( tool_msg = Message.tool_message(
content=result, tool_call_id=command.id, name=command.function.name content=result, tool_call_id=command.id, name=command.function.name

View File

@ -103,7 +103,12 @@ class BrowserUseTool(BaseTool):
async def _ensure_browser_initialized(self) -> BrowserContext: async def _ensure_browser_initialized(self) -> BrowserContext:
"""Ensure browser and context are initialized.""" """Ensure browser and context are initialized."""
if self.browser is None: if self.browser is None:
self.browser = BrowserUseBrowser(BrowserConfig(headless=False)) # 使用Chrome命令行参数设置窗口大小和位置
browser_config = BrowserConfig(
headless=False,
disable_security=True,
)
self.browser = BrowserUseBrowser(browser_config)
if self.context is None: if self.context is None:
self.context = await self.browser.new_context() self.context = await self.browser.new_context()
self.dom_service = DomService(await self.context.get_current_page()) self.dom_service = DomService(await self.context.get_current_page())

View File

@ -1,7 +1,8 @@
from app.tool.base import BaseTool from app.tool.base import BaseTool
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.""" _TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
When you have finished all the tasks, call this tool to end the work."""
class Terminate(BaseTool): class Terminate(BaseTool):

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

41
run.bat
View File

@ -1,3 +1,42 @@
@echo off @echo off
venv\Scripts\python.exe app.py setlocal
cd /d %~dp0
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 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 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...
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

@ -1,5 +1,157 @@
let currentEventSource = null; let currentEventSource = null;
let exampleApiKey = '';
function checkConfigStatus() {
fetch('/config/status')
.then(response => response.json())
.then(data => {
if (data.status === 'missing') {
showConfigModal(data.example_config);
} else if (data.status === 'no_example') {
alert('Error: Missing configuration example file! Please ensure that the config/config.example.toml file exists.');
} else if (data.status === 'error') {
alert('Configuration check error:' + data.message);
}
})
.catch(error => {
console.error('Configuration check failed:', error);
});
}
// Display configuration pop-up and fill in sample configurations
function showConfigModal(exampleConfig) {
const configModal = document.getElementById('config-modal');
if (!configModal) return;
configModal.classList.add('active');
if (exampleConfig) {
fillConfigForm(exampleConfig);
}
const saveButton = document.getElementById('save-config-btn');
if (saveButton) {
saveButton.onclick = saveConfig;
}
}
// Use example configuration to fill in the form
function fillConfigForm(exampleConfig) {
if (exampleConfig.llm) {
const llm = exampleConfig.llm;
setInputValue('llm-model', llm.model);
setInputValue('llm-base-url', llm.base_url);
setInputValue('llm-api-key', llm.api_key);
exampleApiKey = llm.api_key || '';
setInputValue('llm-max-tokens', llm.max_tokens);
setInputValue('llm-temperature', llm.temperature);
}
if (exampleConfig.server) {
setInputValue('server-host', exampleConfig.server.host);
setInputValue('server-port', exampleConfig.server.port);
}
}
function setInputValue(id, value) {
const input = document.getElementById(id);
if (input && value !== undefined) {
input.value = value;
}
}
function saveConfig() {
const configData = collectFormData();
const requiredFields = [
{ id: 'llm-model', name: 'Model Name' },
{ id: 'llm-base-url', name: 'API Base URL' },
{ id: 'llm-api-key', name: 'API Key' },
{ id: 'server-host', name: 'Server Host' },
{ id: 'server-port', name: 'Server Port' }
];
let missingFields = [];
requiredFields.forEach(field => {
if (!document.getElementById(field.id).value.trim()) {
missingFields.push(field.name);
}
});
if (missingFields.length > 0) {
document.getElementById('config-error').textContent =
`Please fill in the necessary configuration information: ${missingFields.join(', ')}`;
return;
}
// Check if the API key is the same as the example configuration
const apiKey = document.getElementById('llm-api-key').value.trim();
if (apiKey === exampleApiKey && exampleApiKey.includes('sk-')) {
document.getElementById('config-error').textContent =
`Please enter your own API key`;
document.getElementById('llm-api-key').parentElement.classList.add('error');
return;
} else {
document.getElementById('llm-api-key').parentElement.classList.remove('error');
}
fetch('/config/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(configData)
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
document.getElementById('config-modal').classList.remove('active');
alert('Configuration saved successfully! The application will use the new configuration on next startup.');
window.location.reload();
} else {
document.getElementById('config-error').textContent =
`Save failed: ${data.message}`;
}
})
.catch(error => {
document.getElementById('config-error').textContent =
`Request error: ${error.message}`;
});
}
// Collect form data
function collectFormData() {
const configData = {
llm: {
model: document.getElementById('llm-model').value,
base_url: document.getElementById('llm-base-url').value,
api_key: document.getElementById('llm-api-key').value
},
server: {
host: document.getElementById('server-host').value,
port: parseInt(document.getElementById('server-port').value || '5172')
}
};
const maxTokens = document.getElementById('llm-max-tokens').value;
if (maxTokens) {
configData.llm.max_tokens = parseInt(maxTokens);
}
const temperature = document.getElementById('llm-temperature').value;
if (temperature) {
configData.llm.temperature = parseFloat(temperature);
}
return configData;
}
function createTask() { function createTask() {
const promptInput = document.getElementById('prompt-input'); const promptInput = document.getElementById('prompt-input');
const prompt = promptInput.value.trim(); const prompt = promptInput.value.trim();
@ -38,6 +190,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 +202,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 +214,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 +258,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 +306,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 +341,14 @@ function setupSSE(taskId) {
</div> </div>
`; `;
} }
})
.catch(error => {
console.error('Task status check failed:', error);
if (retryCount < maxRetries) {
retryCount++;
setTimeout(connect, retryDelay);
}
});
}; };
} }
@ -167,7 +360,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 +373,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 +405,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="/download?file_path=${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="/download?file_path=${filePath}" download="${fileName}" class="download-link"> 下载音频</a>
</div>
`;
} else if (['html', 'js', 'py'].includes(fileExtension)) {
fileInteractionHtml = `
<div class="file-interaction code-file">
<button onclick="simulateRunPython('${filePath}')" class="run-button"> 模拟运行</button>
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link"> 下载文件</a>
</div>
`;
} else {
fileInteractionHtml = `
<div class="file-interaction">
<a href="/download?file_path=${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 +486,7 @@ function createStepElement(type, content, timestamp) {
<pre>${content}</pre> <pre>${content}</pre>
</div> </div>
`; `;
}
return step; return step;
} }
@ -269,14 +537,119 @@ 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', () => {
// Check configuration status
checkConfigStatus();
loadHistory(); loadHistory();
document.getElementById('prompt-input').addEventListener('keydown', (e) => { document.getElementById('prompt-input').addEventListener('keydown', (e) => {
@ -304,4 +677,21 @@ 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');
}
// Do not close the configuration pop-up, because the configuration is required
}
});
}); });

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;
@ -351,3 +520,186 @@ pre {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
.config-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
.config-modal.active {
display: flex;
}
.config-modal-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
width: 80%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.config-modal-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
background-color: var(--info-color);
color: white;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.config-modal-header h2 {
margin-bottom: 10px;
font-size: 1.8rem;
}
.config-modal-body {
padding: 20px;
overflow-y: auto;
max-height: calc(90vh - 140px);
}
.config-modal-footer {
padding: 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.config-section {
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.config-section:last-child {
border-bottom: none;
}
.config-section h3 {
margin-bottom: 15px;
color: var(--info-color);
}
.form-group {
margin-bottom: 15px;
transition: all 0.3s ease;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: var(--text-color);
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.9rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: var(--info-color);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.config-actions {
display: flex;
gap: 10px;
}
.primary-btn {
background-color: var(--info-color);
transition: background-color 0.3s ease;
}
.primary-btn:hover {
background-color: #0069d9;
}
.secondary-btn {
background-color: var(--text-light);
}
.config-error {
color: var(--error-color);
margin-right: 15px;
font-weight: bold;
padding: 8px 0;
transition: all 0.3s ease;
}
.form-group.error input {
border-color: var(--error-color);
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
background-color: rgba(220, 53, 69, 0.05);
}
.form-group.error label {
color: var(--error-color);
}
.form-group .error-message {
color: var(--error-color);
font-size: 0.8rem;
margin-top: 5px;
display: block;
animation: errorAppear 0.3s ease;
}
@keyframes errorAppear {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.note-box {
background-color: #fff8e1;
border-left: 4px solid #ffc107;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.note-box p {
margin: 0 0 8px 0;
color: #856404;
}
.note-box p:last-child {
margin-bottom: 0;
}
.field-help {
font-size: 0.8rem;
color: #666;
margin-top: 4px;
display: block;
}
.required-mark {
color: var(--error-color);
font-weight: bold;
}

View File

@ -34,6 +34,66 @@
</div> </div>
</div> </div>
<div id="config-modal" class="config-modal">
<div class="config-modal-content">
<div class="config-modal-header">
<h2>System Configuration</h2>
<p>Please fill in the necessary configuration information to continue using the system</p>
</div>
<div class="config-modal-body">
<div class="note-box">
<p>⚠️ Please ensure that the following configuration information is correct.</p>
<p>If you do not have an API key, please obtain one from the corresponding AI service provider.</p>
</div>
<div class="config-section">
<h3>LLM Configuration</h3>
<div class="form-group">
<label for="llm-model">Model Name <span class="required-mark">*</span></label>
<input type="text" id="llm-model" name="llm.model" placeholder="For example: claude-3-5-sonnet">
</div>
<div class="form-group">
<label for="llm-base-url">API Base URL <span class="required-mark">*</span></label>
<input type="text" id="llm-base-url" name="llm.base_url" placeholder="For example: https://api.openai.com/v1">
</div>
<div class="form-group">
<label for="llm-api-key">API Key <span class="required-mark">*</span></label>
<input type="password" id="llm-api-key" name="llm.api_key" placeholder="Your API key, for example: sk-...">
<span class="field-help">Must be your own valid API key, not the placeholder in the example</span>
</div>
<div class="form-group">
<label for="llm-max-tokens">Max Tokens</label>
<input type="number" id="llm-max-tokens" name="llm.max_tokens" placeholder="For example: 4096">
</div>
<div class="form-group">
<label for="llm-temperature">Temperature</label>
<input type="number" id="llm-temperature" name="llm.temperature" step="0.1" placeholder="For example: 0.0">
</div>
</div>
<div class="config-section">
<h3>Server Configuration</h3>
<div class="form-group">
<label for="server-host">Host <span class="required-mark">*</span></label>
<input type="text" id="server-host" name="server.host" placeholder="For example: localhost">
</div>
<div class="form-group">
<label for="server-port">Port <span class="required-mark">*</span></label>
<input type="number" id="server-port" name="server.port" placeholder="For example: 5172">
</div>
</div>
</div>
<div class="config-modal-footer">
<p id="config-error" class="config-error"></p>
<div class="config-actions">
<button id="save-config-btn" class="primary-btn">Save Configuration</button>
</div>
</div>
</div>
</div>
<script src="/static/main.js"></script> <script src="/static/main.js"></script>
</body> </body>
</html> </html>