import asyncio import os from typing import Any, cast from nicegui import app, ui from journal.core.config import ( BACKEND_MODE, CHUNK_TOKEN_BUDGET, CLOUDAI_API_KEY, CLOUDAI_API_URL, CLOUDAI_CONNECT_TIMEOUT, CLOUDAI_TIMEOUT, CSHARP_SIDECAR_PATH, EMBEDDING_API_URL, EMBEDDING_MODEL_NAME, FASTER_WHISPER_COMPUTE_TYPE, FASTER_WHISPER_DEVICE, LLAMA_CPP_MODEL, LLAMA_CPP_TIMEOUT, LLAMA_CPP_URL, MODEL_CONTEXT_TOKENS, NLP_BACKEND, SERVER_CONTROL_FILE, SPEECH_RECOGNITION_ENGINE, WHISPER_MODEL_SIZE, ) _DEFAULT_SETTINGS: dict[str, Any] = { "backend_mode": BACKEND_MODE, "csharp_sidecar_path": CSHARP_SIDECAR_PATH, "nlp_backend": NLP_BACKEND, "speech_engine": SPEECH_RECOGNITION_ENGINE, "whisper_model": WHISPER_MODEL_SIZE, "faster_whisper_device": FASTER_WHISPER_DEVICE, "faster_whisper_compute_type": FASTER_WHISPER_COMPUTE_TYPE, "llama_cpp_url": LLAMA_CPP_URL, "llama_cpp_model": LLAMA_CPP_MODEL, "llama_cpp_timeout": LLAMA_CPP_TIMEOUT, "embedding_api_url": EMBEDDING_API_URL, "embedding_model_name": EMBEDDING_MODEL_NAME, "cloudai_api_url": CLOUDAI_API_URL, "cloudai_api_key": CLOUDAI_API_KEY, "cloudai_timeout": CLOUDAI_TIMEOUT, "cloudai_connect_timeout": CLOUDAI_CONNECT_TIMEOUT, "model_context_tokens": MODEL_CONTEXT_TOKENS, "chunk_token_budget": CHUNK_TOKEN_BUDGET, } _ENV_KEYS: dict[str, str] = { "backend_mode": "JOURNAL_BACKEND_MODE", "csharp_sidecar_path": "JOURNAL_CSHARP_SIDECAR_PATH", "nlp_backend": "JOURNAL_NLP_BACKEND", "speech_engine": "JOURNAL_SPEECH_ENGINE", "whisper_model": "JOURNAL_WHISPER_MODEL", "faster_whisper_device": "JOURNAL_FASTER_WHISPER_DEVICE", "faster_whisper_compute_type": "JOURNAL_FASTER_WHISPER_COMPUTE_TYPE", "llama_cpp_url": "JOURNAL_LLAMA_CPP_URL", "llama_cpp_model": "JOURNAL_LLAMA_CPP_MODEL", "llama_cpp_timeout": "JOURNAL_LLAMA_CPP_TIMEOUT", "embedding_api_url": "JOURNAL_EMBEDDING_API_URL", "embedding_model_name": "JOURNAL_EMBEDDING_MODEL_NAME", "cloudai_api_url": "JOURNAL_CLOUDAI_API_URL", "cloudai_api_key": "JOURNAL_CLOUDAI_API_KEY", "cloudai_timeout": "JOURNAL_CLOUDAI_TIMEOUT", "cloudai_connect_timeout": "JOURNAL_CLOUDAI_CONNECT_TIMEOUT", "model_context_tokens": "JOURNAL_MODEL_CONTEXT_TOKENS", "chunk_token_budget": "JOURNAL_CHUNK_TOKEN_BUDGET", } def _ensure_defaults() -> None: for key, value in _DEFAULT_SETTINGS.items(): app.storage.user.setdefault(key, value) def _apply_env_from_storage() -> None: for key, env_key in _ENV_KEYS.items(): value = app.storage.user.get(key, _DEFAULT_SETTINGS.get(key, "")) os.environ[env_key] = str(value).strip() def _on_text_change(user_key: str, env_key: str, label: str, restart_required: bool = False): def handler(e: Any) -> None: value = str(e.value or "").strip() app.storage.user[user_key] = value os.environ[env_key] = value suffix = " Restart server to apply." if restart_required else "" ui.notify(f"{label} updated.{suffix}") return handler def _on_int_change(user_key: str, env_key: str, label: str, restart_required: bool = False): def handler(e: Any) -> None: try: value = int(float(cast(Any, e.value))) except (TypeError, ValueError): ui.notify(f"{label} must be a valid number.", type="negative") return if value <= 0: ui.notify(f"{label} must be greater than 0.", type="negative") return app.storage.user[user_key] = value os.environ[env_key] = str(value) suffix = " Restart server to apply." if restart_required else "" ui.notify(f"{label} updated.{suffix}") return handler def settings_dialog(): """Creates a dialog for runtime/application settings.""" _ensure_defaults() _apply_env_from_storage() async def request_server_action(action: str) -> None: try: SERVER_CONTROL_FILE.parent.mkdir(parents=True, exist_ok=True) SERVER_CONTROL_FILE.write_text(action, encoding="utf-8") except Exception as error: ui.notify(f"Failed to queue server action: {error}", type="negative") return if action == "restart": ui.notify("Server restart requested...", type="warning") else: ui.notify("Server shutdown requested...", type="warning") await asyncio.sleep(0.2) app.shutdown() async def restart_server() -> None: await request_server_action("restart") async def shutdown_server() -> None: await request_server_action("shutdown") with ui.dialog() as dialog, ui.card().classes("w-full max-w-[38rem] max-h-[85vh] overflow-y-auto"): _ = ui.label("Settings").classes("text-xl font-bold") _ = ui.markdown( "Most values apply immediately. " "Some runtime/module values are marked with restart hints." ).classes("text-xs text-gray-500") _ = ui.separator().classes("my-2") _ = ui.label("Backend").classes("text-lg font-medium") _ = ( ui.select( ["csharp-hybrid", "python"], label="Backend Mode", value=cast(str, app.storage.user["backend_mode"]), on_change=_on_text_change( "backend_mode", "JOURNAL_BACKEND_MODE", "Backend mode", restart_required=True, ), ) .bind_value(app.storage.user, "backend_mode") .classes("w-full") ) _ = ( ui.input( label="C# Sidecar Path (optional override)", value=cast(str, app.storage.user["csharp_sidecar_path"]), on_change=_on_text_change( "csharp_sidecar_path", "JOURNAL_CSHARP_SIDECAR_PATH", "C# sidecar path", restart_required=True, ), ) .bind_value(app.storage.user, "csharp_sidecar_path") .classes("w-full") ) _ = ui.separator().classes("my-2") _ = ui.label("AI: Local Model Endpoints").classes("text-lg font-medium") _ = ( ui.input( label="AI Text URL (analysis)", value=cast(str, app.storage.user["llama_cpp_url"]), on_change=_on_text_change( "llama_cpp_url", "JOURNAL_LLAMA_CPP_URL", "Llama.cpp URL" ), ) .bind_value(app.storage.user, "llama_cpp_url") .classes("w-full") ) _ = ( ui.input( label="Llama.cpp Model", value=cast(str, app.storage.user["llama_cpp_model"]), on_change=_on_text_change( "llama_cpp_model", "JOURNAL_LLAMA_CPP_MODEL", "Llama.cpp model" ), ) .bind_value(app.storage.user, "llama_cpp_model") .classes("w-full") ) _ = ( ui.number( label="Llama.cpp Timeout (s)", value=float(cast(str, app.storage.user["llama_cpp_timeout"])), on_change=_on_int_change( "llama_cpp_timeout", "JOURNAL_LLAMA_CPP_TIMEOUT", "Llama timeout" ), ) .bind_value(app.storage.user, "llama_cpp_timeout") .classes("w-full") ) _ = ( ui.input( label="Embedding API URL", value=cast(str, app.storage.user["embedding_api_url"]), on_change=_on_text_change( "embedding_api_url", "JOURNAL_EMBEDDING_API_URL", "Embedding API URL" ), ) .bind_value(app.storage.user, "embedding_api_url") .classes("w-full") ) _ = ( ui.input( label="Embedding Model Name", value=cast(str, app.storage.user["embedding_model_name"]), on_change=_on_text_change( "embedding_model_name", "JOURNAL_EMBEDDING_MODEL_NAME", "Embedding model name", ), ) .bind_value(app.storage.user, "embedding_model_name") .classes("w-full") ) _ = ( ui.number( label="Model Context Tokens", value=float(cast(str, app.storage.user["model_context_tokens"])), on_change=_on_int_change( "model_context_tokens", "JOURNAL_MODEL_CONTEXT_TOKENS", "Model context tokens", ), ) .bind_value(app.storage.user, "model_context_tokens") .classes("w-full") ) _ = ( ui.number( label="Chunk Token Budget", value=float(cast(str, app.storage.user["chunk_token_budget"])), on_change=_on_int_change( "chunk_token_budget", "JOURNAL_CHUNK_TOKEN_BUDGET", "Chunk token budget", ), ) .bind_value(app.storage.user, "chunk_token_budget") .classes("w-full") ) _ = ui.separator().classes("my-2") _ = ui.label("AI: Cloud Endpoint").classes("text-lg font-medium") _ = ( ui.input( label="AI Chat URL", value=cast(str, app.storage.user["cloudai_api_url"]), on_change=_on_text_change( "cloudai_api_url", "JOURNAL_CLOUDAI_API_URL", "Cloud AI URL" ), ) .bind_value(app.storage.user, "cloudai_api_url") .classes("w-full") ) _ = ( ui.input( label="Cloud AI API Key", password=True, password_toggle_button=True, value=cast(str, app.storage.user["cloudai_api_key"]), on_change=_on_text_change( "cloudai_api_key", "JOURNAL_CLOUDAI_API_KEY", "Cloud AI API key" ), ) .bind_value(app.storage.user, "cloudai_api_key") .classes("w-full") ) _ = ( ui.number( label="Cloud AI Timeout (s)", value=float(cast(str, app.storage.user["cloudai_timeout"])), on_change=_on_int_change( "cloudai_timeout", "JOURNAL_CLOUDAI_TIMEOUT", "Cloud AI timeout" ), ) .bind_value(app.storage.user, "cloudai_timeout") .classes("w-full") ) _ = ( ui.number( label="Cloud AI Connect Timeout (s)", value=float(cast(str, app.storage.user["cloudai_connect_timeout"])), on_change=_on_int_change( "cloudai_connect_timeout", "JOURNAL_CLOUDAI_CONNECT_TIMEOUT", "Cloud AI connect timeout", ), ) .bind_value(app.storage.user, "cloudai_connect_timeout") .classes("w-full") ) _ = ui.separator().classes("my-2") _ = ui.label("NLP + Speech").classes("text-lg font-medium") _ = ( ui.select( ["auto", "spacy", "fallback"], label="NLP Backend", value=cast(str, app.storage.user["nlp_backend"]), on_change=_on_text_change( "nlp_backend", "JOURNAL_NLP_BACKEND", "NLP backend", restart_required=True, ), ) .bind_value(app.storage.user, "nlp_backend") .classes("w-full") ) _ = ( ui.select( ["faster-whisper", "whisper", "google", "sphinx"], label="Speech Recognition Engine", value=cast(str, app.storage.user["speech_engine"]), on_change=_on_text_change( "speech_engine", "JOURNAL_SPEECH_ENGINE", "Speech engine" ), ) .bind_value(app.storage.user, "speech_engine") .classes("w-full") ) _ = ( ui.select( ["tiny", "base", "small", "medium", "large"], label="Whisper Model Size", value=cast(str, app.storage.user["whisper_model"]), on_change=_on_text_change( "whisper_model", "JOURNAL_WHISPER_MODEL", "Whisper model" ), ) .bind_value(app.storage.user, "whisper_model") .classes("w-full") ) _ = ( ui.select( ["auto", "cpu", "cuda"], label="faster-whisper Device", value=cast(str, app.storage.user["faster_whisper_device"]), on_change=_on_text_change( "faster_whisper_device", "JOURNAL_FASTER_WHISPER_DEVICE", "faster-whisper device", ), ) .bind_value(app.storage.user, "faster_whisper_device") .classes("w-full") ) _ = ( ui.select( ["float32", "int8", "default", "auto", "float16", "int8_float32", "int8_float16"], label="faster-whisper Compute Type", value=cast(str, app.storage.user["faster_whisper_compute_type"]), on_change=_on_text_change( "faster_whisper_compute_type", "JOURNAL_FASTER_WHISPER_COMPUTE_TYPE", "faster-whisper compute type", ), ) .bind_value(app.storage.user, "faster_whisper_compute_type") .classes("w-full") ) _ = ui.separator().classes("my-2") _ = ui.label("Server Controls").classes("font-medium") if cast(bool, app.storage.user.get("authenticated", False)): _ = ui.markdown( "Use these when running via `run_desktop.py`.\n" "`Restart` reconnects quickly, `Shutdown` stops the server wrapper." ).classes("text-xs text-gray-500") with ui.row().classes("w-full gap-2 mt-1"): _ = ( ui.button( "Restart Server", on_click=restart_server, ) .props("color=warning") .classes("flex-1") ) _ = ( ui.button( "Shutdown Server", on_click=shutdown_server, ) .props("color=negative") .classes("flex-1") ) else: _ = ui.label("Log in to enable restart/shutdown controls.").classes( "text-xs text-gray-500" ) _ = ( ui.button("Close", on_click=dialog.close) .props("flat") .classes("mt-4 self-end") ) return dialog