Compare commits

...

22 Commits

Author SHA1 Message Date
mannaandpoem 3a1065f8cc Merge pull request #996 from Feige-cn/front-end
Pre-commit checks / pre-commit-check (push) Failing after 1s
Add multilingual switchingFront end
2025-03-31 21:29:50 +08:00
Feige-cn f9b36d8406 Merge branch 'front-end' of https://github.com/Feige-cn/OpenManus into front-end 2025-03-31 21:21:31 +08:00
Feige-cn d75a59aeb8 Add multilingual switching 2025-03-31 21:21:15 +08:00
Sheng Fan 456868c01c Merge pull request #974 from Feige-cn/front-end
Set configuration button for Front end
2025-03-29 13:39:37 +08:00
Feige-cn 594b38f905 Set configuration button 2025-03-29 13:28:46 +08:00
Feige-cn 7bd742eafc Merge branch 'front-end' of https://github.com/Feige-cn/OpenManus into front-end 2025-03-29 13:25:26 +08:00
Feige-cn 5ea799d939 Set configuration button 2025-03-29 13:24:56 +08:00
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
11 changed files with 1419 additions and 65 deletions
+105 -7
View File
@@ -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,66 @@ 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():
try:
with open(config_path, "rb") as f:
current_config = tomllib.load(f)
return {"status": "exists", "config": current_config}
except Exception as e:
return {"status": "error", "message": str(e)}
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 +320,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"])
+3
View File
@@ -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(
+5 -1
View File
@@ -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
+6 -1
View File
@@ -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())
+2 -1
View File
@@ -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):
+5
View File
@@ -20,3 +20,8 @@ temperature = 0.0
model = "claude-3-5-sonnet"
base_url = "https://api.openai.com/v1"
api_key = "sk-..."
# Server configuration
[server]
host = "localhost"
port = 5172
+40 -1
View File
@@ -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
+221
View File
@@ -0,0 +1,221 @@
const i18n = {
en: {
welcome: {
title: "Welcome to OpenManus Local Version",
subtitle: "Please enter a task prompt to start a new task"
},
config: {
title: "System Configuration",
subtitle: "Please fill in the necessary configuration information to continue using the system",
note: "Please ensure that the following configuration information is correct.",
apiKeyNote: "If you do not have an API key, please obtain one from the corresponding AI service provider.",
llmConfig: "LLM Configuration",
serverConfig: "Server Configuration",
save: "Save Configuration",
cancel: "Cancel",
required: "Required"
},
input: {
placeholder: "Enter task prompt...",
button: "Create Task"
},
history: {
title: "History Tasks"
},
status: {
running: "Task running",
completed: "Task completed",
failed: "Task failed",
error: "Error"
},
buttons: {
configSetting: "Configuration"
}
},
zh: {
welcome: {
title: "欢迎使用 OpenManus 本地版",
subtitle: "请输入任务提示以开始新任务"
},
config: {
title: "系统配置",
subtitle: "请填写必要的配置信息以继续使用系统",
note: "请确保以下配置信息正确无误。",
apiKeyNote: "如果您没有API密钥,请从相应的AI服务提供商获取。",
llmConfig: "LLM 配置",
serverConfig: "服务器配置",
save: "保存配置",
cancel: "取消",
required: "必填"
},
input: {
placeholder: "输入任务提示...",
button: "创建任务"
},
history: {
title: "历史任务"
},
status: {
running: "任务运行中",
completed: "任务完成",
failed: "任务失败",
error: "错误"
},
buttons: {
configSetting: "系统配置"
}
},
ja: {
welcome: {
title: "OpenManus ローカル版へようこそ",
subtitle: "タスクを開始するにはプロンプトを入力してください"
},
config: {
title: "システム設定",
subtitle: "システムを使用するために必要な設定情報を入力してください",
note: "以下の設定情報が正しいことを確認してください。",
apiKeyNote: "APIキーをお持ちでない場合は、対応するAIサービスプロバイダーから取得してください。",
llmConfig: "LLM 設定",
serverConfig: "サーバー設定",
save: "設定を保存",
cancel: "キャンセル",
required: "必須"
},
input: {
placeholder: "タスクプロンプトを入力...",
button: "タスク作成"
},
history: {
title: "タスク履歴"
},
status: {
running: "タスク実行中",
completed: "タスク完了",
failed: "タスク失敗",
error: "エラー"
},
buttons: {
configSetting: "システム設定"
}
},
ko: {
welcome: {
title: "OpenManus 로컬 버전에 오신 것을 환영합니다",
subtitle: "새 작업을 시작하려면 프롬프트를 입력하세요"
},
config: {
title: "시스템 설정",
subtitle: "시스템을 계속 사용하려면 필요한 구성 정보를 입력하세요",
note: "다음 구성 정보가 올바른지 확인하세요.",
apiKeyNote: "API 키가 없는 경우 해당 AI 서비스 제공업체에서 얻으세요.",
llmConfig: "LLM 설정",
serverConfig: "서버 설정",
save: "설정 저장",
cancel: "취소",
required: "필수"
},
input: {
placeholder: "작업 프롬프트 입력...",
button: "작업 생성"
},
history: {
title: "작업 기록"
},
status: {
running: "작업 실행 중",
completed: "작업 완료",
failed: "작업 실패",
error: "오류"
},
buttons: {
configSetting: "시스템 설정"
}
},
de: {
welcome: {
title: "Willkommen bei OpenManus Lokale Version",
subtitle: "Bitte geben Sie eine Aufgabenanweisung ein, um eine neue Aufgabe zu starten"
},
config: {
title: "Systemkonfiguration",
subtitle: "Bitte füllen Sie die erforderlichen Konfigurationsinformationen aus",
note: "Bitte stellen Sie sicher, dass die folgenden Konfigurationsinformationen korrekt sind.",
apiKeyNote: "Wenn Sie keinen API-Schlüssel haben, erhalten Sie diesen vom entsprechenden KI-Dienstanbieter.",
llmConfig: "LLM Konfiguration",
serverConfig: "Server Konfiguration",
save: "Konfiguration speichern",
cancel: "Abbrechen",
required: "Erforderlich"
},
input: {
placeholder: "Aufgabenanweisung eingeben...",
button: "Aufgabe erstellen"
},
history: {
title: "Aufgabenverlauf"
},
status: {
running: "Aufgabe wird ausgeführt",
completed: "Aufgabe abgeschlossen",
failed: "Aufgabe fehlgeschlagen",
error: "Fehler"
},
buttons: {
configSetting: "Systemeinstellungen"
}
}
};
function getCurrentLanguage() {
return localStorage.getItem('language') || 'en';
}
function setLanguage(lang) {
localStorage.setItem('language', lang);
updatePageContent(lang);
}
function updatePageContent(lang) {
const texts = i18n[lang];
document.querySelector('.welcome-message h1').textContent = texts.welcome.title;
document.querySelector('.welcome-message p').textContent = texts.welcome.subtitle;
document.getElementById('config-button').value = texts.buttons.configSetting;
document.getElementById('config-button').title = texts.buttons.configSetting;
document.querySelector('.config-modal-header h2').textContent = texts.config.title;
document.querySelector('.config-modal-header p').textContent = texts.config.subtitle;
document.querySelector('.note-box p:first-child').textContent = texts.config.note;
document.querySelector('.note-box p:last-child').textContent = texts.config.apiKeyNote;
document.querySelectorAll('.config-section h3').forEach(h3 => {
if (h3.textContent.includes('LLM')) {
h3.textContent = texts.config.llmConfig;
} else if (h3.textContent.includes('Server')) {
h3.textContent = texts.config.serverConfig;
}
});
document.getElementById('save-config-btn').textContent = texts.config.save;
document.getElementById('cancel-config-btn').textContent = texts.config.cancel;
document.getElementById('prompt-input').placeholder = texts.input.placeholder;
document.querySelector('#input-container button').textContent = texts.input.button;
document.querySelector('.history-panel h2').textContent = texts.history.title;
document.querySelectorAll('.required-mark').forEach(mark => {
mark.textContent = texts.config.required;
});
}
document.addEventListener('DOMContentLoaded', () => {
const currentLang = getCurrentLanguage();
document.getElementById('language-select').value = currentLang;
updatePageContent(currentLang);
document.getElementById('language-select').addEventListener('change', (e) => {
setLanguage(e.target.value);
});
});
+492 -47
View File
@@ -1,5 +1,179 @@
let currentEventSource = null;
let exampleApiKey = '';
function checkConfigStatus() {
fetch('/config/status')
.then(response => response.json())
.then(data => {
const inputContainer = document.getElementById('input-container');
if (data.status === 'missing') {
showConfigModal(data.example_config);
inputContainer.classList.add('disabled');
} else if (data.status === 'no_example') {
alert('Error: Missing configuration example file! Please ensure that the config/config.example.toml file exists.');
inputContainer.classList.add('disabled');
} else {
inputContainer.classList.remove('disabled');
}
})
.catch(error => {
console.error('Configuration check failed:', error);
document.getElementById('input-container').classList.add('disabled');
});
}
// Display configuration pop-up and fill in sample configurations
function showConfigModal(config) {
const configModal = document.getElementById('config-modal');
if (!configModal) return;
configModal.classList.add('active');
if (config) {
fillConfigForm(config);
}
const closeBtn = configModal.querySelector('.close-modal');
const cancelBtn = document.getElementById('cancel-config-btn');
function closeConfigModal() {
configModal.classList.remove('active');
document.getElementById('config-error').textContent = '';
document.querySelectorAll('.form-group.error').forEach(group => {
group.classList.remove('error');
});
}
if (closeBtn) {
closeBtn.onclick = closeConfigModal;
}
if (cancelBtn) {
cancelBtn.onclick = closeConfigModal;
}
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');
document.getElementById('input-container').classList.remove('disabled');
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 +212,7 @@ function createTask() {
}
setupSSE(data.task_id);
loadHistory();
promptInput.value = '';
})
.catch(error => {
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
@@ -49,6 +224,7 @@ function setupSSE(taskId) {
let retryCount = 0;
const maxRetries = 3;
const retryDelay = 2000;
let lastResultContent = '';
const container = document.getElementById('task-container');
@@ -60,16 +236,15 @@ function setupSSE(taskId) {
container.innerHTML += '<div class="ping">·</div>';
}, 5000);
const pollInterval = setInterval(() => {
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Polling failed:', error);
});
}, 10000);
// Initial polling
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Initial status fetch failed:', error);
});
const handleEvent = (event, type) => {
clearInterval(heartbeatTimer);
@@ -105,20 +280,35 @@ function setupSSE(taskId) {
eventSource.addEventListener('complete', (event) => {
clearInterval(heartbeatTimer);
clearInterval(pollInterval);
container.innerHTML += `
<div class="complete">
<div>✅ Task completed</div>
<pre>${lastResultContent}</pre>
</div>
`;
eventSource.close();
currentEventSource = null;
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,24 +328,49 @@ function setupSSE(taskId) {
console.error('SSE connection error:', err);
clearInterval(heartbeatTimer);
clearInterval(pollInterval);
eventSource.close();
if (retryCount < maxRetries) {
retryCount++;
container.innerHTML += `
<div class="warning">
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
</div>
`;
setTimeout(connect, retryDelay);
} else {
container.innerHTML += `
<div class="error">
⚠ Connection lost, please try refreshing the page
</div>
`;
}
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">
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
</div>
`;
setTimeout(connect, retryDelay);
} else {
container.innerHTML += `
<div class="error">
⚠ Connection lost, please try refreshing the page
</div>
`;
}
})
.catch(error => {
console.error('Task status check failed:', error);
if (retryCount < maxRetries) {
retryCount++;
setTimeout(connect, retryDelay);
}
});
};
}
@@ -167,7 +382,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 +395,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,13 +427,88 @@ function formatStepContent(data, eventType) {
function createStepElement(type, content, timestamp) {
const step = document.createElement('div');
step.className = `step-item ${type}`;
step.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
</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">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
</div>
`;
}
return step;
}
@@ -269,16 +559,147 @@ function updateTaskStatus(task) {
if (task.status === 'completed') {
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else if (task.status === 'failed') {
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else {
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
}
}
// Display full screen image
function showFullImage(imageSrc) {
const modal = document.getElementById('image-modal');
if (!modal) {
const modalDiv = document.createElement('div');
modalDiv.id = 'image-modal';
modalDiv.className = 'image-modal';
modalDiv.innerHTML = `
<span class="close-modal">&times;</span>
<img src="${imageSrc}" class="modal-content" id="full-image">
`;
document.body.appendChild(modalDiv);
const closeBtn = modalDiv.querySelector('.close-modal');
closeBtn.addEventListener('click', () => {
modalDiv.classList.remove('active');
});
modalDiv.addEventListener('click', (e) => {
if (e.target === modalDiv) {
modalDiv.classList.remove('active');
}
});
setTimeout(() => modalDiv.classList.add('active'), 10);
} else {
document.getElementById('full-image').src = imageSrc;
modal.classList.add('active');
}
}
// Simulate running Python files
function simulateRunPython(filePath) {
let modal = document.getElementById('python-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'python-modal';
modal.className = 'python-modal';
modal.innerHTML = `
<div class="python-console">
<div class="close-modal">&times;</div>
<div class="python-output">Loading Python file contents...</div>
</div>
`;
document.body.appendChild(modal);
const closeBtn = modal.querySelector('.close-modal');
closeBtn.addEventListener('click', () => {
modal.classList.remove('active');
});
}
modal.classList.add('active');
// Load Python file content
fetch(filePath)
.then(response => response.text())
.then(code => {
const outputDiv = modal.querySelector('.python-output');
outputDiv.innerHTML = '';
const codeElement = document.createElement('pre');
codeElement.textContent = code;
codeElement.style.marginBottom = '20px';
codeElement.style.padding = '10px';
codeElement.style.borderBottom = '1px solid #444';
outputDiv.appendChild(codeElement);
// Add simulation run results
const resultElement = document.createElement('div');
resultElement.innerHTML = `
<div style="color: #4CAF50; margin-top: 10px; margin-bottom: 10px;">
> Simulated operation output:</div>
<pre style="color: #f8f8f8;">
#This is the result of Python code simulation run
#The actual operational results may vary
# Running ${filePath.split('/').pop()}...
print("Hello from Python Simulated environment!")
# Code execution completed
</pre>
`;
outputDiv.appendChild(resultElement);
})
.catch(error => {
console.error('Error loading Python file:', error);
const outputDiv = modal.querySelector('.python-output');
outputDiv.innerHTML = `Error loading file: ${error.message}`;
});
}
document.addEventListener('DOMContentLoaded', () => {
// Check configuration status on startup
checkConfigStatus();
loadHistory();
const configButton = document.getElementById('config-button');
if (configButton) {
configButton.addEventListener('click', () => {
fetch('/config/status')
.then(response => response.json())
.then(data => {
console.log(data);
if (data.status === 'exists' && data.config) {
showConfigModal(data.config);
} else if (data.status === 'missing' && data.example_config) {
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 error: ' + data.message);
} else {
alert('Unable to load configuration. Please check if config.example.toml exists.');
}
})
.catch(error => {
console.error('Failed to fetch configuration:', error);
alert('Failed to load configuration. Please check if the server is running and try again.');
});
});
}
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -304,4 +725,28 @@ 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');
}
const configModal = document.getElementById('config-modal');
if (configModal && configModal.classList.contains('active') && !isConfigRequired()) {
configModal.classList.remove('active');
}
}
});
});
function isConfigRequired() {
return false;
}
+462 -3
View File
@@ -42,8 +42,69 @@ body {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.history-panel {
.side-panel {
width: 300px;
display: flex;
flex-direction: column;
gap: 20px;
min-height: calc(100vh - 40px);
}
.history-panel {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.set-config-panel {
display: flex;
gap: 10px;
align-items: center;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
#language-select {
flex: 0 0 120px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
background-color: var(--bg-color);
cursor: pointer;
transition: border-color 0.3s ease;
}
#language-select:hover {
border-color: var(--primary-color);
}
#language-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
#config-button {
flex: 1;
white-space: nowrap;
padding: 12px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
#config-button:hover {
background-color: var(--primary-hover);
}
.main-panel {
@@ -102,6 +163,25 @@ body {
gap: 10px;
}
.input-container.disabled {
opacity: 0.6;
pointer-events: none;
}
.input-container.disabled::after {
content: "Please configure the system first";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 0.9rem;
white-space: nowrap;
}
#prompt-input {
flex: 1;
padding: 12px;
@@ -181,7 +261,6 @@ button:hover {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
width: 100%;
max-height: calc(100vh - 200px);
overflow-y: auto;
@@ -270,7 +349,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 +379,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 +600,213 @@ 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;
position: relative;
}
.config-modal-header h2 {
margin-bottom: 10px;
font-size: 1.8rem;
}
.config-modal-header .close-modal {
position: absolute;
right: 20px;
top: 20px;
font-size: 24px;
color: white;
cursor: pointer;
transition: color 0.2s;
}
.config-modal-header .close-modal:hover {
color: #ddd;
}
.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;
justify-content: flex-end;
}
.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);
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
.secondary-btn:hover {
background-color: #555;
}
.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;
}
+78 -4
View File
@@ -8,11 +8,22 @@
</head>
<body>
<div class="container">
<div class="history-panel">
<h2>History Tasks</h2>
<div id="task-list" class="task-list"></div>
<div class="side-panel">
<div class="history-panel">
<h2>History Tasks</h2>
<div id="task-list" class="task-list"></div>
</div>
<div class="set-config-panel">
<select id="language-select" aria-label="Select language">
<option value="en">English</option>
<option value="zh">中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
<option value="de">Deutsch</option>
</select>
<input type="button" id="config-button" value="Configuration Setting" title="Configuration Settings">
</div>
</div>
<div class="main-panel">
<div id="task-container" class="task-container">
<div class="welcome-message">
@@ -34,6 +45,69 @@
</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>
<span class="close-modal">&times;</span>
</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="cancel-config-btn" class="secondary-btn">Cancel</button>
<button id="save-config-btn" class="primary-btn">Save Configuration</button>
</div>
</div>
</div>
</div>
<script src="/static/main.js"></script>
<script src="/static/i18n.js"></script>
</body>
</html>