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,54 +913,45 @@ 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()) {
// The summary owns an elevated surface: paragraph, voice
// controls, and copy live in ONE card, so the chips read
// as controls ON this text instead of a loose row between
// content blocks (the "tone controls feel disconnected"
// complaint). Paragraph, chips, copy — top to bottom, the
// actual order of the usage loop.
val clipboard = LocalClipboardManager.current
SonarCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.fillMaxWidth()
.padding(Shonar.CardPadding),
verticalArrangement = Arrangement.spacedBy(Shonar.Space3),
) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Summary", style = MaterialTheme.typography.titleMedium) Text("Summary",
style = MaterialTheme.typography.titleMedium)
// Honest tag: which voice this summary was written in // Honest tag: which voice this summary was written in
// — always shown; no persona = the neutral house voice. // — always shown; no persona = the neutral house voice.
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(Shonar.Space2))
Text("· in a ${s.tone ?: "neutral"} voice", Text("· in a ${s.tone ?: "neutral"} voice",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary) color = MaterialTheme.colorScheme.primary)
} }
Text(s.short) Text(s.short, style = MaterialTheme.typography.bodyLarge)
// One-click copy: the summary text and the action items // Voice controls directly under the paragraph —
// are what get pasted into notes/messages — grabbing them // they act on this summary and the shared card
// by mouse-selecting a wall of text was the old way. // says so. Every time they sat further down
val clipboard = LocalClipboardManager.current // (page bottom, below the bullets) they fell
var copiedNote by remember(detail.file.name) { // off-screen and read as "missing".
mutableStateOf<String?>(null)
}
Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
ShonarGhostButton({
clipboard.setText(AnnotatedString(s.short))
copiedNote = "Summary copied"
}) { Text("Copy summary") }
val actionItems = s.list("action_items")
if (actionItems.isNotEmpty()) {
ShonarGhostButton({
clipboard.setText(AnnotatedString(
actionItems.joinToString("\n") { "- $it" }))
copiedNote = "Action items copied"
}) { Text("Copy action items") }
}
}
copiedNote?.let {
Text(it, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary)
}
// Voice controls FIRST under the summary — before the long
// sections. Every time they sat further down (page bottom,
// then below the bullets) they fell off-screen and read as
// "missing". They act on this summary; they live here.
run { run {
val report = detail.reportText val report = detail.reportText
val hasTranscript = detail.transcript != null || val hasTranscript = detail.transcript != null ||
(report != null && !report.contains("_No transcript available._")) (report != null &&
!report.contains("_No transcript available._"))
val remoteKnown = state.remoteIdFor(detail.file) != null val remoteKnown = state.remoteIdFor(detail.file) != null
val sumBusy = detail.jobs.firstOrNull { it.jobType == "summarize" } val sumBusy = detail.jobs.firstOrNull { it.jobType == "summarize" }
?.status in setOf("queued", "running") ?.status in setOf("queued", "running")
val voicesEnabled = remoteKnown && hasTranscript && !sumBusy val voicesEnabled = remoteKnown && hasTranscript && !sumBusy
Column(verticalArrangement = Arrangement.spacedBy(Shonar.Space1)) { Column(verticalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
Text("Re-summarize in the voice of", Text("Re-summarize in the voice of",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant) color = MaterialTheme.colorScheme.onSurfaceVariant)
@ -982,18 +973,66 @@ fun DetailScreen(state: DesktopState) {
} }
} }
} }
Spacer(Modifier.height(4.dp)) // One-click copy: what gets pasted into
// Full structured sections (key points, decisions, action // notes/messages — mouse-selecting a wall of text
// items, questions, details) right on screen — the saved // was the old way.
// .md report had them but the live view showed only the 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(Shonar.Space4))
// Action items get their own card, first among the
// sections: they are the content users actually carry out
// 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