diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt index 5a584c4..a93f863 100644 --- a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt @@ -22,6 +22,9 @@ class ConfigRepository(context: Context) { fun updateTts(enabled: Boolean): Boolean = client.updateConfig(ttsEnabled = enabled) + fun updateDebug(enabled: Boolean): Boolean = + client.updateConfig(debugEnabled = enabled) + fun updateSpeakerModel(modelId: String): Boolean = client.updateConfig(speakerModelId = modelId) diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaConfigClient.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaConfigClient.kt index d304d21..4570aec 100644 --- a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaConfigClient.kt +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaConfigClient.kt @@ -28,7 +28,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) { val thinkerTemperature: Float, val ragEnabled: Boolean = false, val ragThreshold: Float = 0.82f, - val ragTopK: Int = 3 + val ragTopK: Int = 3, + val debugEnabled: Boolean = false ) data class ModelInfo( @@ -57,7 +58,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) { thinkerTemperature = c.getFloat(c.getColumnIndexOrThrow("thinker_temperature")), ragEnabled = c.getColumnIndex("rag_enabled").let { if (it >= 0) c.getInt(it) == 1 else false }, ragThreshold = c.getColumnIndex("rag_threshold").let { if (it >= 0) c.getFloat(it) else 0.82f }, - ragTopK = c.getColumnIndex("rag_top_k").let { if (it >= 0) c.getInt(it) else 3 } + ragTopK = c.getColumnIndex("rag_top_k").let { if (it >= 0) c.getInt(it) else 3 }, + debugEnabled = c.getColumnIndex("debug_enabled").let { if (it >= 0) c.getInt(it) == 1 else false } ) } }.getOrNull() @@ -95,7 +97,8 @@ class KazeiaConfigClient(private val resolver: ContentResolver) { thinkerTemperature: Float? = null, ragEnabled: Boolean? = null, ragThreshold: Float? = null, - ragTopK: Int? = null + ragTopK: Int? = null, + debugEnabled: Boolean? = null ): Boolean { val v = ContentValues() cascadeEnabled?.let { v.put("cascade_enabled", it) } @@ -109,6 +112,7 @@ class KazeiaConfigClient(private val resolver: ContentResolver) { ragEnabled?.let { v.put("rag_enabled", it) } ragThreshold?.let { v.put("rag_threshold", it) } ragTopK?.let { v.put("rag_top_k", it) } + debugEnabled?.let { v.put("debug_enabled", it) } if (v.size() == 0) return true return runCatching { resolver.update(CONFIG_URI, v, null, null) > 0 diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/system/SystemScreen.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/system/SystemScreen.kt index 39d9771..b5cfe00 100644 --- a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/system/SystemScreen.kt +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/system/SystemScreen.kt @@ -138,6 +138,21 @@ fun SystemScreen() { } ) + DebugCard( + enabled = cfg.debugEnabled, + onToggle = { newValue -> + val ok = repo.updateDebug(newValue) + if (ok) config = repo.config() + scope.launch { + snackbar.showSnackbar( + if (ok) "Mode debug ${if (newValue) "activé" else "désactivé"} — appliqué immédiatement côté patient." + else "Échec : Kazeia patient injoignable.", + duration = SnackbarDuration.Short + ) + } + } + ) + ModelSection( title = "Speaker", subtitle = "LLM qui répond au patient", @@ -373,6 +388,45 @@ private fun TtsCard(enabled: Boolean, onToggle: (Boolean) -> Unit) { } } +@Composable +private fun DebugCard(enabled: Boolean, onToggle: (Boolean) -> Unit) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + shape = RoundedCornerShape(12.dp) + ) { + Column(Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text( + "Mode debug", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(Modifier.height(4.dp)) + Text( + if (enabled) + "ACTIVÉ — le bouton debug (logs + métriques CPU/GPU/NPU/RAM) est visible dans l'app patient." + else + "DÉSACTIVÉ — écran patient épuré, bouton debug masqué.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = enabled, + onCheckedChange = onToggle, + colors = SwitchDefaults.colors( + checkedThumbColor = StateReady, + checkedTrackColor = StateReady.copy(alpha = 0.4f) + ) + ) + } + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModelSection( diff --git a/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt b/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt index e68c934..47923aa 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt @@ -64,7 +64,12 @@ class ConfigStore private constructor(private val file: File) { * pertinent ~0.88). Plus haut = plus strict. Tunable depuis l'admin. */ val ragThreshold: Float = 0.82f, /** Nombre max de chunks récupérés/injectés par tour. */ - val ragTopK: Int = 3 + val ragTopK: Int = 3, + /** Mode debug : quand true, l'app patient affiche le bouton qui ouvre le + * panneau de logs + métriques système (CPU/GPU/NPU/RAM). Quand false + * (défaut prod), le bouton est masqué — l'écran reste épuré pour le + * patient. Pilotable depuis l'app admin. */ + val debugEnabled: Boolean = false ) companion object { @@ -172,6 +177,7 @@ class ConfigStore private constructor(private val file: File) { root.put("rag_enabled", cfg.ragEnabled) root.put("rag_threshold", cfg.ragThreshold.toDouble()) root.put("rag_top_k", cfg.ragTopK) + root.put("debug_enabled", cfg.debugEnabled) root.put("speaker", JSONObject().apply { put("model_id", cfg.speaker.modelId) put("system_prompt", cfg.speaker.systemPrompt) @@ -197,6 +203,7 @@ class ConfigStore private constructor(private val file: File) { ragEnabled = root.optBoolean("rag_enabled", defaults.ragEnabled), ragThreshold = root.optDouble("rag_threshold", defaults.ragThreshold.toDouble()).toFloat(), ragTopK = root.optInt("rag_top_k", defaults.ragTopK), + debugEnabled = root.optBoolean("debug_enabled", defaults.debugEnabled), speaker = root.optJSONObject("speaker")?.let { js -> ModelConfig( modelId = js.optString("model_id", defaults.speaker.modelId), diff --git a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt index cc9a605..f69be9d 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt @@ -703,6 +703,7 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think // (admin app → ContentProvider.update → ConfigStore.save → listener) val store = com.kazeia.config.ConfigStore.get(this) runtimeConfig = store.current() + _debugMode.value = runtimeConfig.debugEnabled // gate UI debug (bouton logs/métriques) store.addListener { new -> handleConfigChange(new) } registerConfigReloadReceiver() registerRagReloadReceiver() @@ -2173,6 +2174,10 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think private fun handleConfigChange(new: com.kazeia.config.ConfigStore.RuntimeConfig) { val old = runtimeConfig runtimeConfig = new + if (new.debugEnabled != old.debugEnabled) { + _debugMode.value = new.debugEnabled // affiche/masque le bouton debug côté patient + log("[CONFIG] debug ${old.debugEnabled}→${new.debugEnabled}") + } log("[CONFIG] applied : cascade ${old.cascadeEnabled}→${new.cascadeEnabled}, " + "speaker ${old.speaker.modelId}→${new.speaker.modelId}, " + "thinker ${old.thinker.modelId}→${new.thinker.modelId}") diff --git a/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt b/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt index a6ed9e5..3a6aa3a 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt @@ -256,7 +256,7 @@ class KazeiaTelemetryProvider : ContentProvider() { val cols = arrayOf( "cascade_enabled", "tts_enabled", "stt_engine", "llm_engine", "tts_engine", "rag_enabled", - "rag_threshold", "rag_top_k", + "rag_threshold", "rag_top_k", "debug_enabled", "speaker_model_id", "speaker_system_prompt", "speaker_temperature", "thinker_model_id", "thinker_system_prompt", "thinker_temperature" ) @@ -267,6 +267,7 @@ class KazeiaTelemetryProvider : ContentProvider() { cfg.sttEngine, cfg.llmEngine, cfg.ttsEngine, if (cfg.ragEnabled) 1 else 0, cfg.ragThreshold, cfg.ragTopK, + if (cfg.debugEnabled) 1 else 0, cfg.speaker.modelId, cfg.speaker.systemPrompt, cfg.speaker.temperature, cfg.thinker.modelId, cfg.thinker.systemPrompt, cfg.thinker.temperature )) @@ -361,6 +362,8 @@ class KazeiaTelemetryProvider : ContentProvider() { values.getAsBoolean("rag_enabled") else cur.ragEnabled, ragThreshold = values.getAsFloat("rag_threshold") ?: cur.ragThreshold, ragTopK = values.getAsInteger("rag_top_k") ?: cur.ragTopK, + debugEnabled = if (values.containsKey("debug_enabled")) + values.getAsBoolean("debug_enabled") else cur.debugEnabled, speaker = com.kazeia.config.ConfigStore.ModelConfig( modelId = values.getAsString("speaker_model_id") ?: cur.speaker.modelId, systemPrompt = values.getAsString("speaker_system_prompt") diff --git a/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt b/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt index d170020..2432e41 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt @@ -319,8 +319,12 @@ class ChatActivity : AppCompatActivity() { } } launch { + // debugMode (piloté par l'admin via ConfigStore.debugEnabled) gate le + // BOUTON debug. Si off : bouton masqué + panneau forcé fermé (écran + // patient épuré). Si on : bouton visible, le patient/soignant ouvre le + // panneau logs+métriques via le bouton. service.debugMode.collect { debug -> - setDebugPanelVisible(debug) + applyDebugGate(debug) } } launch { @@ -404,6 +408,19 @@ class ChatActivity : AppCompatActivity() { } } + /** Mode debug (ConfigStore.debugEnabled, piloté admin) : affiche/masque le bouton + * qui ouvre le panneau logs+métriques. Off → bouton caché + panneau forcé fermé. */ + private fun applyDebugGate(enabled: Boolean) { + runOnUiThread { + findViewById(R.id.btnDebugToggle)?.visibility = + if (enabled) View.VISIBLE else View.GONE + if (!enabled) { + debugPanelVisible = false + setDebugPanelVisible(false) + } + } + } + private var logFile: java.io.FileWriter? = null private fun appendLog(msg: String) { diff --git a/kazeia-android/app/src/main/res/layout/activity_chat.xml b/kazeia-android/app/src/main/res/layout/activity_chat.xml index 62e6ee0..21d7491 100644 --- a/kazeia-android/app/src/main/res/layout/activity_chat.xml +++ b/kazeia-android/app/src/main/res/layout/activity_chat.xml @@ -39,6 +39,7 @@ android:src="@android:drawable/ic_menu_manage" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="Debug" + android:visibility="gone" android:tint="@android:color/white" />