Detail: inline summarize progress + more voices + custom voice
- Progress bar + 'Summarizing… · <voice> (attempt n/3)' right under the
Re-summarize button: the top spinner hides behind a long report, which
made a running job look like nothing happened.
- Voices: + Enthusiastic, + Storyteller, + 'Other…' free-text (any
phrase, 60 chars; dialog shows how it reads in the prompt).
- Verified end-to-end on BOTS: sarcastic summarize succeeded, summary
v2 stored with tone=sarcastic ('…a heroic inability to monetize
anything'). BUILD SUCCESSFUL + 39 tests.
This commit is contained in:
parent
994dbc3690
commit
7d3e0e886d
1 changed files with 86 additions and 4 deletions
|
|
@ -517,12 +517,17 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
|
|
||||||
/** Personalities offered beside Re-summarize: (label, tone sent to the
|
/** Personalities offered beside Re-summarize: (label, tone sent to the
|
||||||
* engine). The tone string is interpolated into "in a <tone> tone of
|
* engine). The tone string is interpolated into "in a <tone> 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(
|
private val SUMMARIZE_PERSONAS = listOf(
|
||||||
"Neutral" to null,
|
"Neutral" to null,
|
||||||
"Sarcastic" to "sarcastic",
|
"Sarcastic" to "sarcastic",
|
||||||
"Funny" to "funny",
|
"Funny" to "funny",
|
||||||
"Dry wit" to "dry, witty",
|
"Dry wit" to "dry, witty",
|
||||||
|
"Enthusiastic" to "enthusiastic",
|
||||||
|
"Storyteller" to "storyteller",
|
||||||
)
|
)
|
||||||
|
|
||||||
@androidx.compose.runtime.Composable
|
@androidx.compose.runtime.Composable
|
||||||
|
|
@ -538,6 +543,10 @@ fun DetailScreen(state: DesktopState) {
|
||||||
var recQuery by remember { mutableStateOf("") }
|
var recQuery by remember { mutableStateOf("") }
|
||||||
// Voice for the next summarize: null = Neutral (server default).
|
// Voice for the next summarize: null = Neutral (server default).
|
||||||
var sumTone by remember(detail.file.name) { mutableStateOf<String?>(null) }
|
var sumTone by remember(detail.file.name) { mutableStateOf<String?>(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 playingFile by state.playingFile.collectAsState()
|
||||||
val playError by state.playError.collectAsState()
|
val playError by state.playError.collectAsState()
|
||||||
val isPlaying = playingFile?.absolutePath == detail.file.absolutePath
|
val isPlaying = playingFile?.absolutePath == detail.file.absolutePath
|
||||||
|
|
@ -784,20 +793,93 @@ fun DetailScreen(state: DesktopState) {
|
||||||
label = { Text(label) },
|
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 <your words> 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)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
TextButton({ state.transcribe() }) {
|
TextButton({ state.transcribe() }) {
|
||||||
Text(if (hasTranscript) "Re-transcribe" else "Transcribe")
|
Text(if (hasTranscript) "Re-transcribe" else "Transcribe")
|
||||||
}
|
}
|
||||||
TextButton({ state.summarize(sumTone) },
|
TextButton({ state.summarize(effectiveTone) },
|
||||||
enabled = remoteKnown && hasTranscript) {
|
enabled = remoteKnown && hasTranscript &&
|
||||||
|
!(sumTone == CUSTOM_TONE &&
|
||||||
|
customTone.isBlank())) {
|
||||||
Text(buildString {
|
Text(buildString {
|
||||||
append(if (hasSummary) "Re-summarize" else "Summarize")
|
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) {
|
if (!remoteKnown || !hasTranscript) {
|
||||||
Text(if (!hasTranscript)
|
Text(if (!hasTranscript)
|
||||||
"Summarize unlocks once this recording has a transcript."
|
"Summarize unlocks once this recording has a transcript."
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue