Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6160a5d05b | ||
|
b63a0301dc | ||
|
d0a62e65c7 | ||
|
6243591e7f | ||
|
8439fdc7ab | ||
|
38e34219d3 | ||
|
693f6a90ad | ||
|
d847b550d6 | ||
|
5bba31db3e | ||
|
548c402316 | ||
|
c96c016feb | ||
|
728fdaffd6 | ||
|
fa2b05b658 | ||
|
7b96e12858 | ||
|
62f937c1ca |
107
app.py
107
app.py
@ -1,13 +1,22 @@
|
||||
import asyncio
|
||||
import os
|
||||
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
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
||||
from fastapi.responses import (
|
||||
FileResponse,
|
||||
HTMLResponse,
|
||||
JSONResponse,
|
||||
StreamingResponse,
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
@ -90,6 +99,14 @@ async def index(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")
|
||||
async def create_task(prompt: str = Body(..., embed=True)):
|
||||
task = task_manager.create_task(prompt)
|
||||
@ -107,7 +124,6 @@ async def run_task(task_id: str, prompt: str):
|
||||
agent = Manus(
|
||||
name="Manus",
|
||||
description="A versatile agent that can solve various tasks using multiple tools",
|
||||
max_steps=30,
|
||||
)
|
||||
|
||||
async def on_think(thought):
|
||||
@ -135,7 +151,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"
|
||||
@ -237,6 +253,61 @@ async def get_task(task_id: str):
|
||||
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)
|
||||
async def generic_exception_handler(request: Request, exc: Exception):
|
||||
return JSONResponse(
|
||||
@ -244,12 +315,34 @@ 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"
|
||||
|
||||
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__":
|
||||
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"])
|
||||
|
@ -26,6 +26,9 @@ class Manus(ToolCallAgent):
|
||||
system_prompt: str = SYSTEM_PROMPT
|
||||
next_step_prompt: str = NEXT_STEP_PROMPT
|
||||
|
||||
max_observe: int = 2000
|
||||
max_steps: int = 20
|
||||
|
||||
# Add general-purpose tools to the tool collection
|
||||
available_tools: ToolCollection = Field(
|
||||
default_factory=lambda: ToolCollection(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Any, List, Literal
|
||||
from typing import Any, List, Literal, Optional, Union
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@ -31,6 +31,7 @@ class ToolCallAgent(ReActAgent):
|
||||
tool_calls: List[ToolCall] = Field(default_factory=list)
|
||||
|
||||
max_steps: int = 30
|
||||
max_observe: Optional[Union[int, bool]] = None
|
||||
|
||||
async def think(self) -> bool:
|
||||
"""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}"
|
||||
)
|
||||
|
||||
if self.max_observe:
|
||||
result = result[: self.max_observe]
|
||||
|
||||
# Add tool response to memory
|
||||
tool_msg = Message.tool_message(
|
||||
content=result, tool_call_id=command.id, name=command.function.name
|
||||
|
@ -103,7 +103,12 @@ class BrowserUseTool(BaseTool):
|
||||
async def _ensure_browser_initialized(self) -> BrowserContext:
|
||||
"""Ensure browser and context are initialized."""
|
||||
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:
|
||||
self.context = await self.browser.new_context()
|
||||
self.dom_service = DomService(await self.context.get_current_page())
|
||||
|
@ -1,7 +1,8 @@
|
||||
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):
|
||||
|
@ -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
|
||||
|
41
run.bat
41
run.bat
@ -1,3 +1,42 @@
|
||||
@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
|
||||
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
|
||||
|
412
static/main.js
412
static/main.js
@ -1,5 +1,157 @@
|
||||
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() {
|
||||
const promptInput = document.getElementById('prompt-input');
|
||||
const prompt = promptInput.value.trim();
|
||||
@ -38,6 +190,7 @@ function createTask() {
|
||||
}
|
||||
setupSSE(data.task_id);
|
||||
loadHistory();
|
||||
promptInput.value = '';
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
@ -49,6 +202,7 @@ function setupSSE(taskId) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 2000;
|
||||
let lastResultContent = '';
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
|
||||
@ -60,16 +214,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 +258,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 +306,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 +341,14 @@ function setupSSE(taskId) {
|
||||
</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 => {
|
||||
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 +373,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 +405,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="/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.innerHTML = `
|
||||
<div class="log-line">
|
||||
@ -219,6 +486,7 @@ function createStepElement(type, content, timestamp) {
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
@ -269,14 +537,119 @@ 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', () => {
|
||||
// Check configuration status
|
||||
checkConfigStatus();
|
||||
|
||||
loadHistory();
|
||||
|
||||
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
|
||||
@ -304,4 +677,21 @@ 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');
|
||||
}
|
||||
|
||||
// Do not close the configuration pop-up, because the configuration is required
|
||||
}
|
||||
});
|
||||
});
|
||||
|
356
static/style.css
356
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;
|
||||
@ -351,3 +520,186 @@ pre {
|
||||
white-space: pre-wrap;
|
||||
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;
|
||||
}
|
||||
|
@ -34,6 +34,66 @@
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user