diff --git a/apps/server/api/routes/system.py b/apps/server/api/routes/system.py index df4d6a0c..87e73cd3 100644 --- a/apps/server/api/routes/system.py +++ b/apps/server/api/routes/system.py @@ -1190,6 +1190,13 @@ async def get_settings(request: Request): settings["mqttEnabled"] = mqtt_env.lower() == "true" elif "mqttEnabled" not in settings: settings["mqttEnabled"] = True + + # Gemini model (env var takes precedence over stored setting) + gemini_model_env = os.environ.get("GEMINI_MODEL", "").strip() + if gemini_model_env: + settings["geminiModel"] = gemini_model_env + elif "geminiModel" not in settings: + settings["geminiModel"] = "gemini-2.5-flash" return settings @@ -1249,6 +1256,11 @@ async def save_settings_endpoint(request: Request): for bool_key in ("autoSync", "autoSyncAiDescription"): if bool_key in body: current_settings[bool_key] = bool(body[bool_key]) + + # Gemini model selection + if "geminiModel" in body: + model_value = str(body["geminiModel"]).strip() + current_settings["geminiModel"] = model_value if model_value else "gemini-2.5-flash" # For IP and API key changes, also update .env file env_updated = False @@ -1425,6 +1437,16 @@ async def save_settings_endpoint(request: Request): logger.info("MQTT enabled=%s", mqtt_enabled, extra={"request_id": request_id}) + # Hot-reload Gemini model into process environment + if "geminiModel" in body: + new_model = str(body["geminiModel"]).strip() + if new_model: + os.environ["GEMINI_MODEL"] = new_model + _update_s6_env("GEMINI_MODEL", new_model, request_id) + services_restarted.append("gemini_model") + logger.info("Updated GEMINI_MODEL to %s", new_model, + extra={"request_id": request_id}) + return { "status": "success", "message": "Settings saved successfully", diff --git a/apps/server/main.py b/apps/server/main.py index a6c26869..bd236f76 100644 --- a/apps/server/main.py +++ b/apps/server/main.py @@ -140,6 +140,7 @@ async def lifespan(app: FastAPI): stored = load_settings() _ENV_SETTINGS_MAP = { "geminiApiKey": "GEMINI_API_KEY", + "geminiModel": "GEMINI_MODEL", "meticulousIp": "METICULOUS_IP", "authorName": "AUTHOR_NAME", } diff --git a/apps/server/services/settings_service.py b/apps/server/services/settings_service.py index f73ad44b..f8b9a042 100644 --- a/apps/server/services/settings_service.py +++ b/apps/server/services/settings_service.py @@ -14,6 +14,7 @@ _DEFAULT_SETTINGS = { "geminiApiKey": "", + "geminiModel": "gemini-2.5-flash", "meticulousIp": "", "serverIp": "", "authorName": "", diff --git a/apps/server/test_main.py b/apps/server/test_main.py index eb0879eb..c68bab3a 100644 --- a/apps/server/test_main.py +++ b/apps/server/test_main.py @@ -7927,6 +7927,46 @@ def test_save_settings_empty_body(self, client): # Should handle gracefully assert response.status_code in [200, 400, 500] + def test_save_gemini_model_persists_and_returns(self, client, monkeypatch, tmp_path): + """POST geminiModel then GET and verify it is returned.""" + import services.settings_service as settings_svc + + settings_file = tmp_path / "settings.json" + settings_file.write_text("{}") + monkeypatch.setattr(settings_svc, "SETTINGS_FILE", settings_file) + monkeypatch.delenv("GEMINI_MODEL", raising=False) + + resp = client.post("/api/settings", json={"geminiModel": "gemini-2.5-pro"}) + assert resp.status_code == 200 + + resp = client.get("/api/settings") + assert resp.status_code == 200 + assert resp.json()["geminiModel"] == "gemini-2.5-pro" + + def test_save_gemini_model_empty_falls_back_to_default(self, client, monkeypatch, tmp_path): + """POST empty geminiModel falls back to the default model.""" + import services.settings_service as settings_svc + + settings_file = tmp_path / "settings.json" + settings_file.write_text("{}") + monkeypatch.setattr(settings_svc, "SETTINGS_FILE", settings_file) + monkeypatch.delenv("GEMINI_MODEL", raising=False) + + resp = client.post("/api/settings", json={"geminiModel": " "}) + assert resp.status_code == 200 + + resp = client.get("/api/settings") + assert resp.status_code == 200 + assert resp.json()["geminiModel"] == "gemini-2.5-flash" + + def test_get_settings_includes_gemini_model(self, client, monkeypatch): + """GET /api/settings always includes geminiModel.""" + monkeypatch.delenv("GEMINI_MODEL", raising=False) + + resp = client.get("/api/settings") + assert resp.status_code == 200 + assert "geminiModel" in resp.json() + class TestDecompressShotData: """Test shot data decompression.""" diff --git a/apps/web/public/locales/de/translation.json b/apps/web/public/locales/de/translation.json index c657605e..d28f82c2 100644 --- a/apps/web/public/locales/de/translation.json +++ b/apps/web/public/locales/de/translation.json @@ -423,6 +423,8 @@ "authorName": "Autorenname", "authorNamePlaceholder": "MeticAI", "authorNameDescription": "Dein Name für das Autorenfeld im generierten Profil-JSON. Standardmäßig \"MeticAI\", wenn leer gelassen.", + "geminiModel": "Gemini-Modell", + "geminiModelDescription": "Wähle das Gemini-Modell für KI-Funktionen aus.", "mqttEnabled": "MQTT-Brücke", "mqttEnabledDescription": "Echtzeit-Maschinentelemetrie über MQTT. Betreibt das Live-Dashboard.", "saveSettings": "Einstellungen speichern", @@ -537,7 +539,10 @@ "runsOn": "Runs on", "andCaffeine": "and caffeine ☕", "betaChannelFailed": "Fehler beim Wechsel des Betakanals", - "feedbackFailed": "Fehler beim Senden des Feedbacks" + "feedbackFailed": "Fehler beim Senden des Feedbacks", + "geminiModel25Flash": "Gemini 2.5 Flash (Empfohlen)", + "geminiModel25Pro": "Gemini 2.5 Pro", + "geminiModel20Flash": "Gemini 2.0 Flash" }, "profileBreakdown": { "title": "Profildetails", @@ -860,7 +865,13 @@ "followSystemTheme": "Follow system theme", "followSystemDescription": "Automatically match your system's dark/light mode preference", "backgroundAnimations": "Background animations", - "animationsDescription": "Show animated background effects" + "animationsDescription": "Show animated background effects", + "platformTheme": "Plattform-Design", + "platformThemeDescription": "Visuellen Stil an Ihr Gerät anpassen", + "platformThemeAuto": "Automatisch", + "platformThemeIos": "iOS", + "platformThemeMaterial": "Material (Android)", + "platformThemeNone": "Standard" }, "advanced": { "detailedKnowledge": "Detailliertes KI-Wissen", diff --git a/apps/web/public/locales/en/translation.json b/apps/web/public/locales/en/translation.json index 0d720abf..6d164f2f 100644 --- a/apps/web/public/locales/en/translation.json +++ b/apps/web/public/locales/en/translation.json @@ -423,6 +423,8 @@ "authorName": "Author Name", "authorNamePlaceholder": "MeticAI", "authorNameDescription": "Your name for the author field in generated profile JSON. Defaults to \"MeticAI\" if left empty.", + "geminiModel": "Gemini Model", + "geminiModelDescription": "Select which Gemini model to use for AI features.", "mqttEnabled": "MQTT Bridge", "mqttEnabledDescription": "Real-time machine telemetry via MQTT. Powers the live Control Center dashboard.", "addToHomeAssistant": "Add to Home Assistant", @@ -537,7 +539,10 @@ "version": "Version", "footer": "MeticAI • Built with ❤️ for coffee lovers", "feedbackFailed": "Failed to submit feedback", - "betaChannelFailed": "Failed to switch beta channel" + "betaChannelFailed": "Failed to switch beta channel", + "geminiModel25Flash": "Gemini 2.5 Flash (Recommended)", + "geminiModel25Pro": "Gemini 2.5 Pro", + "geminiModel20Flash": "Gemini 2.0 Flash" }, "profileBreakdown": { "title": "Profile Details", @@ -865,7 +870,13 @@ "followSystemTheme": "Follow system theme", "followSystemDescription": "Automatically match your system's dark/light mode preference", "backgroundAnimations": "Background animations", - "animationsDescription": "Show animated background effects" + "animationsDescription": "Show animated background effects", + "platformTheme": "Platform theme", + "platformThemeDescription": "Adjust the visual style to match your device", + "platformThemeAuto": "Auto-detect", + "platformThemeIos": "iOS", + "platformThemeMaterial": "Material (Android)", + "platformThemeNone": "Default" }, "pourOver": { "title": "Pour-over", diff --git a/apps/web/public/locales/es/translation.json b/apps/web/public/locales/es/translation.json index cbaf03d9..c1f9e5c2 100644 --- a/apps/web/public/locales/es/translation.json +++ b/apps/web/public/locales/es/translation.json @@ -423,6 +423,8 @@ "authorName": "Nombre del autor", "authorNamePlaceholder": "MeticAI", "authorNameDescription": "Tu nombre para el campo de autor en el JSON de perfil generado. Por defecto \"MeticAI\" si se deja vacío.", + "geminiModel": "Modelo Gemini", + "geminiModelDescription": "Selecciona qué modelo Gemini usar para las funciones de IA.", "mqttEnabled": "Puente MQTT", "mqttEnabledDescription": "Telemetría en tiempo real de la máquina vía MQTT. Alimenta el panel de control en vivo.", "saveSettings": "Guardar configuración", @@ -537,7 +539,10 @@ "runsOn": "Runs on", "andCaffeine": "and caffeine ☕", "betaChannelFailed": "Error al cambiar el canal beta", - "feedbackFailed": "Error al enviar comentarios" + "feedbackFailed": "Error al enviar comentarios", + "geminiModel25Flash": "Gemini 2.5 Flash (Recomendado)", + "geminiModel25Pro": "Gemini 2.5 Pro", + "geminiModel20Flash": "Gemini 2.0 Flash" }, "profileBreakdown": { "title": "Detalles del perfil", @@ -860,7 +865,13 @@ "followSystemTheme": "Follow system theme", "followSystemDescription": "Automatically match your system's dark/light mode preference", "backgroundAnimations": "Background animations", - "animationsDescription": "Show animated background effects" + "animationsDescription": "Show animated background effects", + "platformTheme": "Tema de plataforma", + "platformThemeDescription": "Ajusta el estilo visual según tu dispositivo", + "platformThemeAuto": "Detectar automáticamente", + "platformThemeIos": "iOS", + "platformThemeMaterial": "Material (Android)", + "platformThemeNone": "Predeterminado" }, "advanced": { "detailedKnowledge": "Conocimiento detallado de IA", diff --git a/apps/web/public/locales/fr/translation.json b/apps/web/public/locales/fr/translation.json index 87d90de1..5807e76d 100644 --- a/apps/web/public/locales/fr/translation.json +++ b/apps/web/public/locales/fr/translation.json @@ -423,6 +423,8 @@ "authorName": "Nom de l'auteur", "authorNamePlaceholder": "MeticAI", "authorNameDescription": "Votre nom pour le champ auteur dans le JSON du profil généré. Par défaut \"MeticAI\" si laissé vide.", + "geminiModel": "Modèle Gemini", + "geminiModelDescription": "Sélectionnez le modèle Gemini à utiliser pour les fonctions IA.", "mqttEnabled": "Pont MQTT", "mqttEnabledDescription": "Télémétrie machine en temps réel via MQTT. Alimente le tableau de bord en direct.", "saveSettings": "Enregistrer les paramètres", @@ -537,7 +539,10 @@ "runsOn": "Runs on", "andCaffeine": "and caffeine ☕", "betaChannelFailed": "Échec du changement de canal bêta", - "feedbackFailed": "Échec de l'envoi du commentaire" + "feedbackFailed": "Échec de l'envoi du commentaire", + "geminiModel25Flash": "Gemini 2.5 Flash (Recommandé)", + "geminiModel25Pro": "Gemini 2.5 Pro", + "geminiModel20Flash": "Gemini 2.0 Flash" }, "profileBreakdown": { "title": "Détails du profil", @@ -860,7 +865,13 @@ "followSystemTheme": "Follow system theme", "followSystemDescription": "Automatically match your system's dark/light mode preference", "backgroundAnimations": "Background animations", - "animationsDescription": "Show animated background effects" + "animationsDescription": "Show animated background effects", + "platformTheme": "Thème de plateforme", + "platformThemeDescription": "Adapter le style visuel à votre appareil", + "platformThemeAuto": "Détection automatique", + "platformThemeIos": "iOS", + "platformThemeMaterial": "Material (Android)", + "platformThemeNone": "Par défaut" }, "advanced": { "detailedKnowledge": "Connaissances IA détaillées", diff --git a/apps/web/public/locales/it/translation.json b/apps/web/public/locales/it/translation.json index 535acc44..749648c8 100644 --- a/apps/web/public/locales/it/translation.json +++ b/apps/web/public/locales/it/translation.json @@ -423,6 +423,8 @@ "authorName": "Nome autore", "authorNamePlaceholder": "MeticAI", "authorNameDescription": "Il tuo nome per il campo autore nel JSON del profilo generato. Predefinito \"MeticAI\" se lasciato vuoto.", + "geminiModel": "Modello Gemini", + "geminiModelDescription": "Seleziona quale modello Gemini utilizzare per le funzioni IA.", "mqttEnabled": "Bridge MQTT", "mqttEnabledDescription": "Telemetria macchina in tempo reale via MQTT. Alimenta la dashboard di controllo dal vivo.", "saveSettings": "Salva impostazioni", @@ -537,7 +539,10 @@ "runsOn": "Runs on", "andCaffeine": "and caffeine ☕", "betaChannelFailed": "Impossibile cambiare il canale beta", - "feedbackFailed": "Impossibile inviare il feedback" + "feedbackFailed": "Impossibile inviare il feedback", + "geminiModel25Flash": "Gemini 2.5 Flash (Consigliato)", + "geminiModel25Pro": "Gemini 2.5 Pro", + "geminiModel20Flash": "Gemini 2.0 Flash" }, "profileBreakdown": { "title": "Dettagli profilo", @@ -860,7 +865,13 @@ "followSystemTheme": "Follow system theme", "followSystemDescription": "Automatically match your system's dark/light mode preference", "backgroundAnimations": "Background animations", - "animationsDescription": "Show animated background effects" + "animationsDescription": "Show animated background effects", + "platformTheme": "Tema piattaforma", + "platformThemeDescription": "Adatta lo stile visivo al tuo dispositivo", + "platformThemeAuto": "Rilevamento automatico", + "platformThemeIos": "iOS", + "platformThemeMaterial": "Material (Android)", + "platformThemeNone": "Predefinito" }, "advanced": { "detailedKnowledge": "Conoscenza IA dettagliata", diff --git a/apps/web/public/locales/sv/translation.json b/apps/web/public/locales/sv/translation.json index 6f76e2a5..5b2fa0d3 100644 --- a/apps/web/public/locales/sv/translation.json +++ b/apps/web/public/locales/sv/translation.json @@ -423,6 +423,8 @@ "authorName": "Författarnamn", "authorNamePlaceholder": "MeticAI", "authorNameDescription": "Ditt namn för författarfältet i genererad profil-JSON. Standardvärde \"MeticAI\" om det lämnas tomt.", + "geminiModel": "Gemini-modell", + "geminiModelDescription": "Välj vilken Gemini-modell som ska användas för AI-funktioner.", "mqttEnabled": "MQTT-brygga", "mqttEnabledDescription": "Realtidstelemetri från maskinen via MQTT. Driver den direkta kontrollpanelen.", "saveSettings": "Spara inställningar", @@ -537,7 +539,10 @@ "runsOn": "Runs on", "andCaffeine": "and caffeine ☕", "betaChannelFailed": "Kunde inte byta betakanal", - "feedbackFailed": "Kunde inte skicka feedback" + "feedbackFailed": "Kunde inte skicka feedback", + "geminiModel25Flash": "Gemini 2.5 Flash (Rekommenderad)", + "geminiModel25Pro": "Gemini 2.5 Pro", + "geminiModel20Flash": "Gemini 2.0 Flash" }, "profileBreakdown": { "title": "Profildetaljer", @@ -865,7 +870,13 @@ "followSystemTheme": "Följ systemtema", "followSystemDescription": "Matcha automatiskt ditt systems mörkt-/ljust-läge", "backgroundAnimations": "Bakgrundsanimationer", - "animationsDescription": "Visa animerade bakgrundseffekter" + "animationsDescription": "Visa animerade bakgrundseffekter", + "platformTheme": "Plattformstema", + "platformThemeDescription": "Anpassa utseendet efter din enhet", + "platformThemeAuto": "Automatisk", + "platformThemeIos": "iOS", + "platformThemeMaterial": "Material (Android)", + "platformThemeNone": "Standard" }, "pourOver": { "title": "Pour Over", diff --git a/apps/web/src/components/SettingsView.tsx b/apps/web/src/components/SettingsView.tsx index 4e587588..b5c127d6 100644 --- a/apps/web/src/components/SettingsView.tsx +++ b/apps/web/src/components/SettingsView.tsx @@ -54,6 +54,7 @@ interface Settings { geminiApiKey: string meticulousIp: string authorName: string + geminiModel?: string mqttEnabled?: boolean geminiApiKeyMasked?: boolean geminiApiKeyConfigured?: boolean @@ -105,6 +106,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo geminiApiKey: '', meticulousIp: '', authorName: '', + geminiModel: 'gemini-2.5-flash', mqttEnabled: true }) const [isSaving, setIsSaving] = useState(false) @@ -201,6 +203,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo geminiApiKey: localStorage.getItem(STORAGE_KEYS.GEMINI_API_KEY) || '', meticulousIp: window.location.hostname, authorName: localStorage.getItem(STORAGE_KEYS.AUTHOR_NAME) || '', + geminiModel: localStorage.getItem(STORAGE_KEYS.GEMINI_MODEL) || 'gemini-2.5-flash', mqttEnabled: true, geminiApiKeyMasked: false, geminiApiKeyConfigured: Boolean(localStorage.getItem(STORAGE_KEYS.GEMINI_API_KEY)?.trim()), @@ -217,6 +220,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo geminiApiKey: data.geminiApiKey || '', meticulousIp: data.meticulousIp || '', authorName: data.authorName || '', + geminiModel: data.geminiModel || 'gemini-2.5-flash', mqttEnabled: data.mqttEnabled !== false, geminiApiKeyMasked: data.geminiApiKeyMasked || false, geminiApiKeyConfigured: data.geminiApiKeyConfigured || false @@ -362,6 +366,9 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo if (settings.authorName) { localStorage.setItem(STORAGE_KEYS.AUTHOR_NAME, settings.authorName) } + if (settings.geminiModel) { + localStorage.setItem(STORAGE_KEYS.GEMINI_MODEL, settings.geminiModel) + } setSaveStatus('success') setTimeout(() => setSaveStatus('idle'), 3000) return @@ -374,6 +381,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo authorName: settings.authorName, meticulousIp: settings.meticulousIp, mqttEnabled: settings.mqttEnabled, + geminiModel: settings.geminiModel, } // Only send API key if user actually typed a new value (not the masked stars) @@ -966,6 +974,26 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
+ {/* Gemini Model */} ++ {t('settings.geminiModelDescription')} +
+