diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index a990ad2..847402a 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -913,87 +913,126 @@ fun DetailScreen(state: DesktopState) { // is already inside the report text shown below. if (detail.transcript != null) detail.summary?.let { s -> if (s.short.isNotBlank()) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Summary", style = MaterialTheme.typography.titleMedium) - // Honest tag: which voice this summary was written in - // — always shown; no persona = the neutral house voice. - Spacer(Modifier.width(8.dp)) - Text("· in a ${s.tone ?: "neutral"} voice", - 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. + // 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 - var copiedNote by remember(detail.file.name) { - mutableStateOf(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 { - val report = detail.reportText - val hasTranscript = detail.transcript != null || - (report != null && !report.contains("_No transcript available._")) - val remoteKnown = state.remoteIdFor(detail.file) != null - val sumBusy = detail.jobs.firstOrNull { it.jobType == "summarize" } - ?.status in setOf("queued", "running") - val voicesEnabled = remoteKnown && hasTranscript && !sumBusy - Column(verticalArrangement = Arrangement.spacedBy(Shonar.Space1)) { - Text("Re-summarize in the voice of", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant) - Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { - SUMMARIZE_PERSONAS.forEach { (label, tone) -> - PillChip( - selected = chipTone == tone, - enabled = voicesEnabled, - onClick = { - sumTone = tone - state.summarize(tone) - }, - label = label, - ) + SonarCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.fillMaxWidth() + .padding(Shonar.CardPadding), + verticalArrangement = Arrangement.spacedBy(Shonar.Space3), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Summary", + style = MaterialTheme.typography.titleMedium) + // Honest tag: which voice this summary was written in + // — always shown; no persona = the neutral house voice. + Spacer(Modifier.width(Shonar.Space2)) + Text("· in a ${s.tone ?: "neutral"} voice", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary) + } + Text(s.short, style = MaterialTheme.typography.bodyLarge) + // Voice controls directly under the paragraph — + // they act on this summary and the shared card + // says so. Every time they sat further down + // (page bottom, below the bullets) they fell + // off-screen and read as "missing". + run { + val report = detail.reportText + val hasTranscript = detail.transcript != null || + (report != null && + !report.contains("_No transcript available._")) + val remoteKnown = state.remoteIdFor(detail.file) != null + val sumBusy = detail.jobs.firstOrNull { it.jobType == "summarize" } + ?.status in setOf("queued", "running") + val voicesEnabled = remoteKnown && hasTranscript && !sumBusy + Column(verticalArrangement = Arrangement.spacedBy(Shonar.Space2)) { + Text("Re-summarize in the voice of", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant) + Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { + SUMMARIZE_PERSONAS.forEach { (label, tone) -> + PillChip( + selected = chipTone == tone, + enabled = voicesEnabled, + onClick = { + sumTone = tone + state.summarize(tone) + }, + label = label, + ) + } + } + if (!voicesEnabled && !remoteKnown) { + HelperText("Summarize unlocks once this file is uploaded (press Transcribe).") + } } } - if (!voicesEnabled && !remoteKnown) { - HelperText("Summarize unlocks once this file is uploaded (press Transcribe).") + // One-click copy: what gets pasted into + // 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)) - // Full structured sections (key points, decisions, action - // items, questions, details) right on screen — the saved - // .md report had them but the live view showed only the + 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 - // the file elsewhere. - SUMMARY_LIST_KEYS.forEach { key -> + // the file elsewhere. Section headings separate with + // Space4 (more above the heading than below it). + SUMMARY_LIST_KEYS.filter { it != "action_items" }.forEach { key -> val items = s.list(key) if (items.isNotEmpty()) { - Spacer(Modifier.height(6.dp)) + Spacer(Modifier.height(Shonar.Space4)) Text(key.replace('_', ' ').replaceFirstChar { it.uppercase() }, style = MaterialTheme.typography.titleSmall) + Spacer(Modifier.height(Shonar.Space1)) items.forEach { item -> Text("• $item", style = MaterialTheme.typography.bodyMedium) @@ -1001,11 +1040,12 @@ fun DetailScreen(state: DesktopState) { } } if (s.detailed.isNotBlank()) { - Spacer(Modifier.height(6.dp)) + Spacer(Modifier.height(Shonar.Space4)) Text("Details", style = MaterialTheme.typography.titleSmall) + Spacer(Modifier.height(Shonar.Space1)) 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