From a20c1d73c7ac976f9626268d2ec99b5fdc6e1a62 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:09:19 +0000 Subject: [PATCH 01/11] Refactor configuration management in wizard and ChronicleSetup - Updated wizard.py to read Obsidian/Neo4j configuration from config.yml, enhancing flexibility and error handling. - Refactored ChronicleSetup to utilize ConfigManager for loading and verifying config.yml, ensuring a single source of truth. - Improved user feedback for missing configuration files and streamlined the setup process for memory and transcription providers. --- backends/advanced/init.py | 91 ++++++++++++++------------------------- wizard.py | 20 +++++---- 2 files changed, 44 insertions(+), 67 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index f093bf4d..f8231db8 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -33,22 +33,21 @@ def __init__(self, args=None): self.config: Dict[str, Any] = {} self.args = args or argparse.Namespace() self.config_yml_path = Path("../../config/config.yml") # Main config at config/config.yml - self.config_yml_data = None # Check if we're in the right directory if not Path("pyproject.toml").exists() or not Path("src").exists(): self.console.print("[red][ERROR][/red] Please run this script from the backends/advanced directory") sys.exit(1) - # Initialize ConfigManager + # Initialize ConfigManager (single source of truth for config.yml) self.config_manager = ConfigManager(service_path="backends/advanced") self.console.print(f"[blue][INFO][/blue] Using config.yml at: {self.config_manager.config_yml_path}") - # Load existing config or create default structure - self.config_yml_data = self.config_manager.get_full_config() - if not self.config_yml_data: - self.console.print("[yellow][WARNING][/yellow] config.yml not found, will create default structure") - self.config_yml_data = self._get_default_config_structure() + # Verify config.yml exists - fail fast if missing + if not self.config_manager.config_yml_path.exists(): + self.console.print("[red][ERROR][/red] config.yml not found at {self.config_manager.config_yml_path}") + self.console.print("[red][ERROR][/red] Run wizard.py from project root to create config.yml") + sys.exit(1) def print_header(self, title: str): """Print a colorful header""" @@ -138,28 +137,6 @@ def mask_api_key(self, key: str, show_chars: int = 5) -> str: return f"{key_clean[:show_chars]}{'*' * min(15, len(key_clean) - show_chars * 2)}{key_clean[-show_chars:]}" - def _get_default_config_structure(self) -> Dict[str, Any]: - """Return default config.yml structure if file doesn't exist""" - return { - "defaults": { - "llm": "openai-llm", - "embedding": "openai-embed", - "stt": "stt-deepgram", - "tts": "tts-http", - "vector_store": "vs-qdrant" - }, - "models": [], - "memory": { - "provider": "chronicle", - "timeout_seconds": 1200, - "extraction": { - "enabled": True, - "prompt": "Extract important information from this conversation and return a JSON object with an array named \"facts\"." - } - } - } - - def setup_authentication(self): """Configure authentication settings""" self.print_section("Authentication Setup") @@ -208,7 +185,6 @@ def setup_transcription(self): # Update config.yml to use Deepgram self.config_manager.update_config_defaults({"stt": "stt-deepgram"}) - self.config_yml_data = self.config_manager.get_full_config() # Reload self.console.print("[green][SUCCESS][/green] Deepgram configured in config.yml and .env") self.console.print("[blue][INFO][/blue] Set defaults.stt: stt-deepgram") @@ -224,7 +200,6 @@ def setup_transcription(self): # Update config.yml to use Parakeet self.config_manager.update_config_defaults({"stt": "stt-parakeet-batch"}) - self.config_yml_data = self.config_manager.get_full_config() # Reload self.console.print("[green][SUCCESS][/green] Parakeet configured in config.yml and .env") self.console.print("[blue][INFO][/blue] Set defaults.stt: stt-parakeet-batch") @@ -266,7 +241,6 @@ def setup_llm(self): self.config["OPENAI_API_KEY"] = api_key # Update config.yml to use OpenAI models self.config_manager.update_config_defaults({"llm": "openai-llm", "embedding": "openai-embed"}) - self.config_yml_data = self.config_manager.get_full_config() # Reload to stay in sync self.console.print("[green][SUCCESS][/green] OpenAI configured in config.yml") self.console.print("[blue][INFO][/blue] Set defaults.llm: openai-llm") self.console.print("[blue][INFO][/blue] Set defaults.embedding: openai-embed") @@ -277,7 +251,6 @@ def setup_llm(self): self.console.print("[blue][INFO][/blue] Ollama selected") # Update config.yml to use Ollama models self.config_manager.update_config_defaults({"llm": "local-llm", "embedding": "local-embed"}) - self.config_yml_data = self.config_manager.get_full_config() # Reload to stay in sync self.console.print("[green][SUCCESS][/green] Ollama configured in config.yml") self.console.print("[blue][INFO][/blue] Set defaults.llm: local-llm") self.console.print("[blue][INFO][/blue] Set defaults.embedding: local-embed") @@ -287,7 +260,6 @@ def setup_llm(self): self.console.print("[blue][INFO][/blue] Skipping LLM setup - memory extraction disabled") # Disable memory extraction in config.yml self.config_manager.update_memory_config({"extraction": {"enabled": False}}) - self.config_yml_data = self.config_manager.get_full_config() # Reload to stay in sync def setup_memory(self): """Configure memory provider - updates config.yml""" @@ -309,7 +281,6 @@ def setup_memory(self): # Update config.yml (also updates .env automatically) self.config_manager.update_memory_config({"provider": "chronicle"}) - self.config_yml_data = self.config_manager.get_full_config() # Reload to stay in sync self.console.print("[green][SUCCESS][/green] Chronicle memory provider configured in config.yml and .env") elif choice == "2": @@ -330,7 +301,6 @@ def setup_memory(self): "timeout": int(timeout) } }) - self.config_yml_data = self.config_manager.get_full_config() # Reload to stay in sync self.console.print("[green][SUCCESS][/green] OpenMemory MCP configured in config.yml and .env") self.console.print("[yellow][WARNING][/yellow] Remember to start OpenMemory: cd ../../extras/openmemory-mcp && docker compose up -d") @@ -348,7 +318,6 @@ def setup_memory(self): "timeout": int(timeout) } }) - self.config_yml_data = self.config_manager.get_full_config() # Reload to stay in sync self.console.print("[green][SUCCESS][/green] Mycelia memory provider configured in config.yml and .env") self.console.print("[yellow][WARNING][/yellow] Make sure Mycelia is running at the configured URL") @@ -405,21 +374,19 @@ def setup_obsidian(self): neo4j_password = self.prompt_password("Neo4j password (min 8 chars)") if enable_obsidian: - # Update .env with credentials - self.config["OBSIDIAN_ENABLED"] = "true" + # Update .env with credentials only (secrets, not feature flags) self.config["NEO4J_HOST"] = "neo4j-mem0" self.config["NEO4J_USER"] = "neo4j" self.config["NEO4J_PASSWORD"] = neo4j_password - # Update config.yml with feature flag - if "memory" not in self.config_yml_data: - self.config_yml_data["memory"] = {} - if "obsidian" not in self.config_yml_data["memory"]: - self.config_yml_data["memory"]["obsidian"] = {} - - self.config_yml_data["memory"]["obsidian"]["enabled"] = True - self.config_yml_data["memory"]["obsidian"]["neo4j_host"] = "neo4j-mem0" - self.config_yml_data["memory"]["obsidian"]["timeout"] = 30 + # Update config.yml with feature flag (source of truth) - auto-saves via ConfigManager + self.config_manager.update_memory_config({ + "obsidian": { + "enabled": True, + "neo4j_host": "neo4j-mem0", + "timeout": 30 + } + }) self.console.print("[green][SUCCESS][/green] Obsidian/Neo4j configured") self.console.print("[blue][INFO][/blue] Neo4j will start automatically with --profile obsidian") @@ -585,28 +552,32 @@ def show_summary(self): self.console.print(f"✅ Admin Account: {self.config.get('ADMIN_EMAIL', 'Not configured')}") + # Get current config from ConfigManager (single source of truth) + config_yml = self.config_manager.get_full_config() + # Show transcription from config.yml - stt_default = self.config_yml_data.get("defaults", {}).get("stt", "not set") + stt_default = config_yml.get("defaults", {}).get("stt", "not set") stt_model = next( - (m for m in self.config_yml_data.get("models", []) if m.get("name") == stt_default), + (m for m in config_yml.get("models", []) if m.get("name") == stt_default), None ) stt_provider = stt_model.get("model_provider", "unknown") if stt_model else "not configured" self.console.print(f"✅ Transcription: {stt_provider} ({stt_default}) - config.yml") # Show LLM config from config.yml - llm_default = self.config_yml_data.get("defaults", {}).get("llm", "not set") - embedding_default = self.config_yml_data.get("defaults", {}).get("embedding", "not set") + llm_default = config_yml.get("defaults", {}).get("llm", "not set") + embedding_default = config_yml.get("defaults", {}).get("embedding", "not set") self.console.print(f"✅ LLM: {llm_default} (config.yml)") self.console.print(f"✅ Embedding: {embedding_default} (config.yml)") # Show memory provider from config.yml - memory_provider = self.config_yml_data.get("memory", {}).get("provider", "chronicle") + memory_provider = config_yml.get("memory", {}).get("provider", "chronicle") self.console.print(f"✅ Memory Provider: {memory_provider} (config.yml)") - # Show Obsidian/Neo4j status - if self.config.get('OBSIDIAN_ENABLED') == 'true': - neo4j_host = self.config.get('NEO4J_HOST', 'not set') + # Show Obsidian/Neo4j status (read from config.yml) + obsidian_config = config_yml.get("memory", {}).get("obsidian", {}) + if obsidian_config.get("enabled", False): + neo4j_host = obsidian_config.get("neo4j_host", "not set") self.console.print(f"✅ Obsidian/Neo4j: Enabled ({neo4j_host})") # Auto-determine URLs based on HTTPS configuration @@ -625,9 +596,13 @@ def show_next_steps(self): self.print_section("Next Steps") self.console.print() + # Get current config from ConfigManager (single source of truth) + config_yml = self.config_manager.get_full_config() + self.console.print("1. Start the main services:") - # Include --profile obsidian if Obsidian is enabled - if self.config.get('OBSIDIAN_ENABLED') == 'true': + # Include --profile obsidian if Obsidian is enabled (read from config.yml) + obsidian_enabled = config_yml.get("memory", {}).get("obsidian", {}).get("enabled", False) + if obsidian_enabled: self.console.print(" [cyan]docker compose --profile obsidian up --build -d[/cyan]") self.console.print(" [dim](Includes Neo4j for Obsidian integration)[/dim]") else: diff --git a/wizard.py b/wizard.py index d78a910c..a2e2b2f7 100755 --- a/wizard.py +++ b/wizard.py @@ -9,6 +9,7 @@ import sys from datetime import datetime from pathlib import Path +import yaml from dotenv import get_key from rich import print as rprint @@ -449,17 +450,18 @@ def main(): else: failed_services.append(service) - # Check for Obsidian/Neo4j configuration + # Check for Obsidian/Neo4j configuration (read from config.yml) obsidian_enabled = False if 'advanced' in selected_services and 'advanced' not in failed_services: - backend_env_path = Path('backends/advanced/.env') - if backend_env_path.exists(): - neo4j_host = read_env_value(str(backend_env_path), 'NEO4J_HOST') - obsidian_enabled_flag = read_env_value(str(backend_env_path), 'OBSIDIAN_ENABLED') - if neo4j_host and not is_placeholder(neo4j_host, 'your-neo4j-host-here', 'your_neo4j_host_here'): - obsidian_enabled = True - elif obsidian_enabled_flag == 'true': - obsidian_enabled = True + config_yml_path = Path('config/config.yml') + if config_yml_path.exists(): + try: + with open(config_yml_path, 'r') as f: + config_data = yaml.safe_load(f) + obsidian_config = config_data.get('memory', {}).get('obsidian', {}) + obsidian_enabled = obsidian_config.get('enabled', False) + except Exception as e: + console.print(f"[yellow]Warning: Could not read config.yml: {e}[/yellow]") # Final Summary console.print(f"\n🎊 [bold green]Setup Complete![/bold green]") From ad4b1f95ff71c923bf5c37de0def7da188c6b75a Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:26:38 +0000 Subject: [PATCH 02/11] Fix string formatting for error message in ChronicleSetup --- backends/advanced/init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index f8231db8..fe04fd15 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -45,7 +45,7 @@ def __init__(self, args=None): # Verify config.yml exists - fail fast if missing if not self.config_manager.config_yml_path.exists(): - self.console.print("[red][ERROR][/red] config.yml not found at {self.config_manager.config_yml_path}") + self.console.print(f"[red][ERROR][/red] config.yml not found at {self.config_manager.config_yml_path}") self.console.print("[red][ERROR][/red] Run wizard.py from project root to create config.yml") sys.exit(1) From ff061e02ff3d0da026da269bd0ec3447c8048858 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:03:27 +0000 Subject: [PATCH 03/11] Enhance chat configuration management and UI integration - Updated `services.py` to allow service restart with an option to recreate containers, addressing WSL2 bind mount issues. - Added new chat configuration management functions in `system_controller.py` for loading, saving, and validating chat prompts. - Introduced `ChatSettings` component in the web UI for admin users to manage chat configurations easily. - Updated API service methods in `api.ts` to support chat configuration endpoints. - Integrated chat settings into the system management page for better accessibility. --- .../src/advanced_omi_backend/chat_service.py | 37 +++- .../controllers/system_controller.py | 100 +++++++++ .../routers/modules/system_routes.py | 50 ++++- .../webui/src/components/ChatSettings.tsx | 195 ++++++++++++++++++ backends/advanced/webui/src/pages/System.tsx | 6 + backends/advanced/webui/src/services/api.ts | 11 + services.py | 33 ++- tests/endpoints/system_admin_tests.robot | 57 +++++ 8 files changed, 475 insertions(+), 14 deletions(-) create mode 100644 backends/advanced/webui/src/components/ChatSettings.tsx diff --git a/backends/advanced/src/advanced_omi_backend/chat_service.py b/backends/advanced/src/advanced_omi_backend/chat_service.py index de92a4b9..647fa7d1 100644 --- a/backends/advanced/src/advanced_omi_backend/chat_service.py +++ b/backends/advanced/src/advanced_omi_backend/chat_service.py @@ -22,6 +22,7 @@ from advanced_omi_backend.database import get_database from advanced_omi_backend.llm_client import get_llm_client +from advanced_omi_backend.model_registry import get_models_registry from advanced_omi_backend.services.memory import get_memory_service from advanced_omi_backend.services.memory.base import MemoryEntry from advanced_omi_backend.services.obsidian_service import ( @@ -133,7 +134,7 @@ def from_dict(cls, data: Dict) -> "ChatSession": class ChatService: """Service for managing chat sessions and memory-enhanced conversations.""" - + def __init__(self): self.db = None self.sessions_collection: Optional[AsyncIOMotorCollection] = None @@ -142,6 +143,32 @@ def __init__(self): self.memory_service = None self._initialized = False + def _get_system_prompt(self) -> str: + """ + Get system prompt from config with fallback to default. + + Returns: + str: System prompt for chat interactions + """ + try: + reg = get_models_registry() + if reg and hasattr(reg, 'config'): + chat_config = reg.config.get('chat', {}) + prompt = chat_config.get('system_prompt') + if prompt: + logger.debug("Loaded chat system prompt from config") + return prompt + except Exception as e: + logger.warning(f"Failed to load chat system prompt from config: {e}") + + # Fallback to default + logger.debug("Using default chat system prompt") + return """You are a helpful AI assistant with access to the user's personal memories and conversation history. + +Use the provided memories and conversation context to give personalized, contextual responses. If memories are relevant, reference them naturally in your response. Be conversational and helpful. + +If no relevant memories are available, respond normally based on the conversation context.""" + async def initialize(self): """Initialize the chat service with database connections.""" if self._initialized: @@ -392,12 +419,8 @@ async def generate_response_stream( "timestamp": time.time() } - # Create system prompt - system_prompt = """You are a helpful AI assistant with access to the user's personal memories and conversation history. - -Use the provided memories and conversation context to give personalized, contextual responses. If memories are relevant, reference them naturally in your response. Be conversational and helpful. - -If no relevant memories are available, respond normally based on the conversation context.""" + # Get system prompt from config + system_prompt = self._get_system_prompt() # Prepare full prompt full_prompt = f"{system_prompt}\n\n{context}" diff --git a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py index 17b9cbcf..aced763f 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py @@ -455,3 +455,103 @@ async def set_memory_provider(provider: str): except Exception as e: logger.exception("Error setting memory provider") raise e + + +# Chat Configuration Management Functions + +async def get_chat_config_yaml() -> str: + """Get chat system prompt as plain text.""" + try: + config_path = _find_config_path() + + default_prompt = """You are a helpful AI assistant with access to the user's personal memories and conversation history. + +Use the provided memories and conversation context to give personalized, contextual responses. If memories are relevant, reference them naturally in your response. Be conversational and helpful. + +If no relevant memories are available, respond normally based on the conversation context.""" + + if not os.path.exists(config_path): + return default_prompt + + with open(config_path, 'r') as f: + full_config = yaml.safe_load(f) or {} + + chat_config = full_config.get('chat', {}) + system_prompt = chat_config.get('system_prompt', default_prompt) + + # Return just the prompt text, not the YAML structure + return system_prompt + + except Exception as e: + logger.error(f"Error loading chat config: {e}") + raise + + +async def save_chat_config_yaml(prompt_text: str) -> dict: + """Save chat system prompt from plain text.""" + try: + config_path = _find_config_path() + + # Validate plain text prompt + if not prompt_text or not isinstance(prompt_text, str): + raise ValueError("Prompt must be a non-empty string") + + prompt_text = prompt_text.strip() + if len(prompt_text) < 10: + raise ValueError("Prompt too short (minimum 10 characters)") + if len(prompt_text) > 10000: + raise ValueError("Prompt too long (maximum 10000 characters)") + + # Create chat config dict + chat_config = {'system_prompt': prompt_text} + + # Load full config + if os.path.exists(config_path): + with open(config_path, 'r') as f: + full_config = yaml.safe_load(f) or {} + else: + full_config = {} + + # Backup existing config + if os.path.exists(config_path): + backup_path = str(config_path) + '.backup' + shutil.copy2(config_path, backup_path) + logger.info(f"Created config backup at {backup_path}") + + # Update chat section + full_config['chat'] = chat_config + + # Save + with open(config_path, 'w') as f: + yaml.dump(full_config, f, default_flow_style=False, allow_unicode=True) + + # Reload config in memory (hot-reload) + load_models_config(force_reload=True) + + logger.info("Chat configuration updated successfully") + + return {"success": True, "message": "Chat configuration updated successfully"} + + except Exception as e: + logger.error(f"Error saving chat config: {e}") + raise + + +async def validate_chat_config_yaml(prompt_text: str) -> dict: + """Validate chat system prompt plain text.""" + try: + # Validate plain text prompt + if not isinstance(prompt_text, str): + return {"valid": False, "error": "Prompt must be a string"} + + prompt_text = prompt_text.strip() + if len(prompt_text) < 10: + return {"valid": False, "error": "Prompt too short (minimum 10 characters)"} + if len(prompt_text) > 10000: + return {"valid": False, "error": "Prompt too long (maximum 10000 characters)"} + + return {"valid": True, "message": "Configuration is valid"} + + except Exception as e: + logger.error(f"Error validating chat config: {e}") + return {"valid": False, "error": f"Validation error: {str(e)}"} diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py index ead61ffa..e2b49676 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py @@ -7,7 +7,8 @@ import logging from typing import Optional -from fastapi import APIRouter, Body, Depends, Request +from fastapi import APIRouter, Body, Depends, HTTPException, Request +from fastapi.responses import Response from pydantic import BaseModel from advanced_omi_backend.auth import current_active_user, current_superuser @@ -128,6 +129,53 @@ async def delete_all_user_memories(current_user: User = Depends(current_active_u return await system_controller.delete_all_user_memories(current_user) +# Chat Configuration Management Endpoints + +@router.get("/admin/chat/config", response_class=Response) +async def get_chat_config(current_user: User = Depends(current_superuser)): + """Get chat configuration as YAML. Admin only.""" + try: + yaml_content = await system_controller.get_chat_config_yaml() + return Response(content=yaml_content, media_type="text/plain") + except Exception as e: + logger.error(f"Failed to get chat config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/admin/chat/config") +async def save_chat_config( + request: Request, + current_user: User = Depends(current_superuser) +): + """Save chat configuration from YAML. Admin only.""" + try: + yaml_content = await request.body() + yaml_str = yaml_content.decode('utf-8') + result = await system_controller.save_chat_config_yaml(yaml_str) + return result + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Failed to save chat config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/admin/chat/config/validate") +async def validate_chat_config( + request: Request, + current_user: User = Depends(current_superuser) +): + """Validate chat configuration YAML. Admin only.""" + try: + yaml_content = await request.body() + yaml_str = yaml_content.decode('utf-8') + result = await system_controller.validate_chat_config_yaml(yaml_str) + return result + except Exception as e: + logger.error(f"Failed to validate chat config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/streaming/status") async def get_streaming_status(request: Request, current_user: User = Depends(current_superuser)): """Get status of active streaming sessions and Redis Streams health. Admin only.""" diff --git a/backends/advanced/webui/src/components/ChatSettings.tsx b/backends/advanced/webui/src/components/ChatSettings.tsx new file mode 100644 index 00000000..1acad362 --- /dev/null +++ b/backends/advanced/webui/src/components/ChatSettings.tsx @@ -0,0 +1,195 @@ +import { useState, useEffect } from 'react' +import { MessageSquare, RefreshCw, CheckCircle, Save, RotateCcw, AlertCircle } from 'lucide-react' +import { systemApi } from '../services/api' +import { useAuth } from '../contexts/AuthContext' + +interface ChatSettingsProps { + className?: string +} + +export default function ChatSettings({ className }: ChatSettingsProps) { + const [configYaml, setConfigYaml] = useState('') + const [loading, setLoading] = useState(false) + const [validating, setValidating] = useState(false) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + const [error, setError] = useState('') + const { isAdmin } = useAuth() + + useEffect(() => { + loadChatConfig() + }, []) + + const loadChatConfig = async () => { + setLoading(true) + setError('') + setMessage('') + + try { + const response = await systemApi.getChatConfigRaw() + setConfigYaml(response.data.config_yaml || response.data) + setMessage('Configuration loaded successfully') + setTimeout(() => setMessage(''), 3000) + } catch (err: any) { + const status = err.response?.status + if (status === 401) { + setError('Unauthorized: admin privileges required') + } else { + setError(err.response?.data?.error || 'Failed to load configuration') + } + } finally { + setLoading(false) + } + } + + const validateConfig = async () => { + if (!configYaml.trim()) { + setError('Configuration cannot be empty') + return + } + + setValidating(true) + setError('') + setMessage('') + + try { + const response = await systemApi.validateChatConfig(configYaml) + if (response.data.valid) { + setMessage('✅ Configuration is valid') + } else { + setError(response.data.error || 'Validation failed') + } + setTimeout(() => setMessage(''), 3000) + } catch (err: any) { + setError(err.response?.data?.error || 'Validation failed') + } finally { + setValidating(false) + } + } + + const saveConfig = async () => { + if (!configYaml.trim()) { + setError('Configuration cannot be empty') + return + } + + setSaving(true) + setError('') + setMessage('') + + try { + await systemApi.updateChatConfigRaw(configYaml) + setMessage('✅ Configuration saved successfully') + setTimeout(() => setMessage(''), 5000) + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to save configuration') + } finally { + setSaving(false) + } + } + + const resetConfig = () => { + loadChatConfig() + setMessage('Configuration reset to file version') + setTimeout(() => setMessage(''), 3000) + } + + if (!isAdmin) { + return null + } + + return ( +
+
+ {/* Header */} +
+
+ +

+ Chat System Prompt +

+
+
+ + +
+
+ + {/* Messages */} + {message && ( +
+ +

{message}

+
+ )} + + {error && ( +
+ +

{error}

+
+ )} + + {/* Editor */} +
+