Detail: summary paragraph, voice chips, and copy actions consolidated into one card

This commit is contained in:
avi 2026-09-21 11:29:48 -05:00
commit 90f6f1b786

View file

@ -913,87 +913,126 @@ fun DetailScreen(state: DesktopState) {
// is already inside the report text shown below. // is already inside the report text shown below.
if (detail.transcript != null) detail.summary?.let { s -> if (detail.transcript != null) detail.summary?.let { s ->
if (s.short.isNotBlank()) { if (s.short.isNotBlank()) {
Row(verticalAlignment = Alignment.CenterVertically) { // The summary owns an elevated surface: paragraph, voice
Text("Summary", style = MaterialTheme.typography.titleMedium) // controls, and copy live in ONE card, so the chips read
// Honest tag: which voice this summary was written in // as controls ON this text instead of a loose row between
// — always shown; no persona = the neutral house voice. // content blocks (the "tone controls feel disconnected"
Spacer(Modifier.width(8.dp)) // complaint). Paragraph, chips, copy — top to bottom, the
Text("· in a ${s.tone ?: "neutral"} voice", // actual order of the usage loop.
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary)
}
Text(s.short)
// One-click copy: the summary text and the action items
// are what get pasted into notes/messages — grabbing them
// by mouse-selecting a wall of text was the old way.
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
var copiedNote by remember(detail.file.name) { SonarCard(modifier = Modifier.fillMaxWidth()) {
mutableStateOf<String?>(null) Column(
} modifier = Modifier.fillMaxWidth()
Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { .padding(Shonar.CardPadding),
ShonarGhostButton({ verticalArrangement = Arrangement.spacedBy(Shonar.Space3),
clipboard.setText(AnnotatedString(s.short)) ) {
copiedNote = "Summary copied" Row(verticalAlignment = Alignment.CenterVertically) {
}) { Text("Copy summary") } Text("Summary",
val actionItems = s.list("action_items") style = MaterialTheme.typography.titleMedium)
if (actionItems.isNotEmpty()) { // Honest tag: which voice this summary was written in
ShonarGhostButton({ // — always shown; no persona = the neutral house voice.
clipboard.setText(AnnotatedString( Spacer(Modifier.width(Shonar.Space2))
actionItems.joinToString("\n") { "- $it" })) Text("· in a ${s.tone ?: "neutral"} voice",
copiedNote = "Action items copied" style = MaterialTheme.typography.labelMedium,
}) { Text("Copy action items") } color = MaterialTheme.colorScheme.primary)
} }
} Text(s.short, style = MaterialTheme.typography.bodyLarge)
copiedNote?.let { // Voice controls directly under the paragraph —
Text(it, style = MaterialTheme.typography.labelMedium, // they act on this summary and the shared card
color = MaterialTheme.colorScheme.primary) // says so. Every time they sat further down
} // (page bottom, below the bullets) they fell
// Voice controls FIRST under the summary — before the long // off-screen and read as "missing".
// sections. Every time they sat further down (page bottom, run {
// then below the bullets) they fell off-screen and read as val report = detail.reportText
// "missing". They act on this summary; they live here. val hasTranscript = detail.transcript != null ||
run { (report != null &&
val report = detail.reportText !report.contains("_No transcript available._"))
val hasTranscript = detail.transcript != null || val remoteKnown = state.remoteIdFor(detail.file) != null
(report != null && !report.contains("_No transcript available._")) val sumBusy = detail.jobs.firstOrNull { it.jobType == "summarize" }
val remoteKnown = state.remoteIdFor(detail.file) != null ?.status in setOf("queued", "running")
val sumBusy = detail.jobs.firstOrNull { it.jobType == "summarize" } val voicesEnabled = remoteKnown && hasTranscript && !sumBusy
?.status in setOf("queued", "running") Column(verticalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
val voicesEnabled = remoteKnown && hasTranscript && !sumBusy Text("Re-summarize in the voice of",
Column(verticalArrangement = Arrangement.spacedBy(Shonar.Space1)) { style = MaterialTheme.typography.labelMedium,
Text("Re-summarize in the voice of", color = MaterialTheme.colorScheme.onSurfaceVariant)
style = MaterialTheme.typography.labelMedium, Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
color = MaterialTheme.colorScheme.onSurfaceVariant) SUMMARIZE_PERSONAS.forEach { (label, tone) ->
Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { PillChip(
SUMMARIZE_PERSONAS.forEach { (label, tone) -> selected = chipTone == tone,
PillChip( enabled = voicesEnabled,
selected = chipTone == tone, onClick = {
enabled = voicesEnabled, sumTone = tone
onClick = { state.summarize(tone)
sumTone = tone },
state.summarize(tone) label = label,
}, )
label = label, }
) }
if (!voicesEnabled && !remoteKnown) {
HelperText("Summarize unlocks once this file is uploaded (press Transcribe).")
}
} }
} }
if (!voicesEnabled && !remoteKnown) { // One-click copy: what gets pasted into
HelperText("Summarize unlocks once this file is uploaded (press Transcribe).") // notes/messages — mouse-selecting a wall of text
// was the old way.
var summaryCopied by remember(detail.file.name) {
mutableStateOf(false)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2),
) {
ShonarGhostButton({
clipboard.setText(AnnotatedString(s.short))
summaryCopied = true
}) { Text(if (summaryCopied) "Summary copied" else "Copy summary") }
} }
} }
} }
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(Shonar.Space4))
// Full structured sections (key points, decisions, action // Action items get their own card, first among the
// items, questions, details) right on screen — the saved // sections: they are the content users actually carry out
// .md report had them but the live view showed only the // of the app, and they were styled exactly like Questions.
val actionItems = s.list("action_items")
if (actionItems.isNotEmpty()) {
var itemsCopied by remember(detail.file.name) {
mutableStateOf(false)
}
SonarCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.fillMaxWidth()
.padding(Shonar.CardPadding),
verticalArrangement = Arrangement.spacedBy(Shonar.Space2),
) {
Text("Action items",
style = MaterialTheme.typography.titleMedium)
actionItems.forEach { item ->
Text("→ $item",
style = MaterialTheme.typography.bodyMedium)
}
ShonarOutlinedButton({
clipboard.setText(AnnotatedString(
actionItems.joinToString("\n") { "- $it" }))
itemsCopied = true
}) { Text(if (itemsCopied) "Copied" else "Copy action items") }
}
}
Spacer(Modifier.height(Shonar.Space4))
}
// Remaining structured sections (key points, decisions,
// questions, details) right on screen — the saved .md
// report had them but the live view showed only the
// one-liner, so "what did I have to do?" required opening // one-liner, so "what did I have to do?" required opening
// the file elsewhere. // the file elsewhere. Section headings separate with
SUMMARY_LIST_KEYS.forEach { key -> // Space4 (more above the heading than below it).
SUMMARY_LIST_KEYS.filter { it != "action_items" }.forEach { key ->
val items = s.list(key) val items = s.list(key)
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(Shonar.Space4))
Text(key.replace('_', ' ').replaceFirstChar { it.uppercase() }, Text(key.replace('_', ' ').replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.titleSmall) style = MaterialTheme.typography.titleSmall)
Spacer(Modifier.height(Shonar.Space1))
items.forEach { item -> items.forEach { item ->
Text("• $item", Text("• $item",
style = MaterialTheme.typography.bodyMedium) style = MaterialTheme.typography.bodyMedium)
@ -1001,11 +1040,12 @@ fun DetailScreen(state: DesktopState) {
} }
} }
if (s.detailed.isNotBlank()) { if (s.detailed.isNotBlank()) {
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(Shonar.Space4))
Text("Details", style = MaterialTheme.typography.titleSmall) Text("Details", style = MaterialTheme.typography.titleSmall)
Spacer(Modifier.height(Shonar.Space1))
Text(s.detailed, style = MaterialTheme.typography.bodyMedium) Text(s.detailed, style = MaterialTheme.typography.bodyMedium)
} }
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(Shonar.Space4))
} }
} }
// Re-transcribe + summarize progress (the chips themselves live // Re-transcribe + summarize progress (the chips themselves live