From 8439fdc7ab7d0ef5f37ae1df367f227ffbb47b8e Mon Sep 17 00:00:00 2001 From: Feige-cn Date: Tue, 18 Mar 2025 00:54:47 +0800 Subject: [PATCH] Add page LLM configuration settings --- app.py | 67 ++++++++++++++-- static/main.js | 163 ++++++++++++++++++++++++++++++++++++++ static/style.css | 183 +++++++++++++++++++++++++++++++++++++++++++ templates/index.html | 60 ++++++++++++++ 4 files changed, 468 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index d314508..059600f 100644 --- a/app.py +++ b/app.py @@ -253,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( @@ -268,18 +323,20 @@ 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: - raise RuntimeError( - "Configuration file not found, please check if config/fig.toml exists" - ) + return {"host": "localhost", "port": 5172} except KeyError as e: - raise RuntimeError( - f"The configuration file is missing necessary fields: {str(e)}" + print( + f"The configuration file is missing necessary fields: {str(e)}, use default configuration" ) + return {"host": "localhost", "port": 5172} if __name__ == "__main__": diff --git a/static/main.js b/static/main.js index 4fd8518..2da35f1 100644 --- a/static/main.js +++ b/static/main.js @@ -1,5 +1,163 @@ 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); + // Save example API key for later verification + 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`; + // Highlight the API key input box + document.getElementById('llm-api-key').parentElement.classList.add('error'); + return; + } else { + document.getElementById('llm-api-key').parentElement.classList.remove('error'); + } + + // Send configuration to server + fetch('/config/save', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(configData) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + // Close pop-up + document.getElementById('config-modal').classList.remove('active'); + + // Show success message + alert('Configuration saved successfully! The application will use the new configuration on next startup.'); + + // Refresh page + 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') + } + }; + + // Add optional fields + 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(); @@ -495,6 +653,9 @@ print("Hello from Python Simulated environment!") } document.addEventListener('DOMContentLoaded', () => { + // Check configuration status + checkConfigStatus(); + loadHistory(); document.getElementById('prompt-input').addEventListener('keydown', (e) => { @@ -535,6 +696,8 @@ document.addEventListener('DOMContentLoaded', () => { if (pythonModal && pythonModal.classList.contains('active')) { pythonModal.classList.remove('active'); } + + // Do not close the configuration pop-up, because the configuration is required } }); }); diff --git a/static/style.css b/static/style.css index 51c60ee..3f12cd2 100644 --- a/static/style.css +++ b/static/style.css @@ -520,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; +} diff --git a/templates/index.html b/templates/index.html index d3245d9..8c98ee8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -34,6 +34,66 @@ +
+
+
+

System Configuration

+

Please fill in the necessary configuration information to continue using the system

+
+ +
+
+

⚠️ Please ensure that the following configuration information is correct.

+

If you do not have an API key, please obtain one from the corresponding AI service provider.

+
+ +
+

LLM Configuration

+
+ + +
+
+ + +
+
+ + + Must be your own valid API key, not the placeholder in the example +
+
+ + +
+
+ + +
+
+ +
+

Server Configuration

+
+ + +
+
+ + +
+
+
+ + +
+
+