diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 47b88a0..b001d27 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -517,12 +517,17 @@ fun LibraryScreen(state: DesktopState) { /** Personalities offered beside Re-summarize: (label, tone sent to the * engine). The tone string is interpolated into "in a tone of - * voice", so it must read naturally there. null = neutral house voice. */ + * voice", so it must read naturally there. null = neutral house voice. + * CUSTOM_TONE opens a free-text box — the engine accepts any phrase. */ +private const val CUSTOM_TONE = "custom" + private val SUMMARIZE_PERSONAS = listOf( "Neutral" to null, "Sarcastic" to "sarcastic", "Funny" to "funny", "Dry wit" to "dry, witty", + "Enthusiastic" to "enthusiastic", + "Storyteller" to "storyteller", ) @androidx.compose.runtime.Composable @@ -538,6 +543,10 @@ fun DetailScreen(state: DesktopState) { var recQuery by remember { mutableStateOf("") } // Voice for the next summarize: null = Neutral (server default). var sumTone by remember(detail.file.name) { mutableStateOf(null) } + var customTone by remember(detail.file.name) { mutableStateOf("") } + var showCustomDialog by remember(detail.file.name) { mutableStateOf(false) } + val effectiveTone = if (sumTone == CUSTOM_TONE) + customTone.trim().takeIf { it.isNotBlank() } else sumTone val playingFile by state.playingFile.collectAsState() val playError by state.playError.collectAsState() val isPlaying = playingFile?.absolutePath == detail.file.absolutePath @@ -784,20 +793,93 @@ fun DetailScreen(state: DesktopState) { label = { Text(label) }, ) } + FilterChip( + selected = sumTone == CUSTOM_TONE, + onClick = { + sumTone = CUSTOM_TONE + showCustomDialog = true + }, + label = { + Text(if (sumTone == CUSTOM_TONE && + customTone.isNotBlank()) + "“${customTone.trim()}”" else "Other…") + }, + ) + } + if (showCustomDialog) { + AlertDialog( + onDismissRequest = { showCustomDialog = false }, + title = { Text("Any voice you like") }, + text = { + Column { + OutlinedTextField(customTone, { + if (it.length <= 60) customTone = it }, + label = { Text("e.g. pirate, philosopher, sports caster") }, + singleLine = true, + modifier = Modifier.fillMaxWidth()) + Text("Used as: “…in a tone of voice.”", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) + } + }, + confirmButton = { + TextButton({ + if (customTone.isNotBlank()) + showCustomDialog = false + }) { Text("Use this voice") } + }, + dismissButton = { + TextButton({ + sumTone = null + customTone = "" + showCustomDialog = false + }) { Text("Cancel") } + }, + ) } } Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { TextButton({ state.transcribe() }) { Text(if (hasTranscript) "Re-transcribe" else "Transcribe") } - TextButton({ state.summarize(sumTone) }, - enabled = remoteKnown && hasTranscript) { + TextButton({ state.summarize(effectiveTone) }, + enabled = remoteKnown && hasTranscript && + !(sumTone == CUSTOM_TONE && + customTone.isBlank())) { Text(buildString { append(if (hasSummary) "Re-summarize" else "Summarize") - sumTone?.let { append(" · $it") } + effectiveTone?.let { append(" · $it") } }) } } + // Progress right where the buttons are: the busy spinner near + // the top is off-screen once a report fills the page, which + // made a running summarize look like "nothing happens". + val sJob = detail.jobs.firstOrNull { it.jobType == "summarize" } + val sRunning = sJob?.status in setOf("queued", "running") + if (sRunning) { + val sStatus = sJob?.status + val sProg = sJob?.progress + val sAttempt = sJob?.attempt ?: 0 + if (sStatus == "running" && sProg != null && sProg > 0) { + LinearProgressIndicator( + progress = { sProg.coerceIn(0, 99) / 100f }, + modifier = Modifier.fillMaxWidth()) + } else { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + Text(buildString { + append(when { + sStatus == "queued" -> "Queued…" + sProg != null && sProg > 0 -> "Summarizing… $sProg%" + else -> "Summarizing…" + }) + effectiveTone?.let { append(" · $it") } + if (sAttempt > 0) + append(" (attempt $sAttempt/${sJob?.maxAttempts})") + }, style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary) + } if (!remoteKnown || !hasTranscript) { Text(if (!hasTranscript) "Summarize unlocks once this recording has a transcript."