Tool Call Card Alignment
Nucleic-Session: FFDE42E0-22AB-4478-B2E2-FF69B2753CEE Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import NucleicProtocol
|
||||||
|
|
||||||
|
/// Pretty rendering of a *completed* `AskUserQuestion` tool call — the phone echo of the Mac's
|
||||||
|
/// `AskUserQuestionResultCard`. Once the user has answered, the raw "AskUserQuestion {…}" card reads
|
||||||
|
/// as opaque JSON; instead we show each question with the option the user chose (and that option's
|
||||||
|
/// description when the answer matches a known label — free-text "Other" answers won't, so they show
|
||||||
|
/// none). The answers are recovered from the persisted tool result (`AskUserQuestion.answers(in:for:)`),
|
||||||
|
/// so the card survives transcript replay. Mirrors the answer picker's look — the prompt set quiet,
|
||||||
|
/// the chosen answer emphasized — so the answered card reads as the settled form of the picker.
|
||||||
|
struct AskUserQuestionResultCard: View {
|
||||||
|
let questions: [AskUserQuestion.Question]
|
||||||
|
/// Chosen option labels keyed by question text (more than one for a multi-select).
|
||||||
|
let answers: [String: [String]]
|
||||||
|
|
||||||
|
private var accent: Color { Palette.accent }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "questionmark.bubble")
|
||||||
|
.font(.caption).foregroundStyle(accent).frame(width: 15)
|
||||||
|
Text("You answered")
|
||||||
|
.font(.callout.weight(.medium))
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
ForEach(Array(questions.enumerated()), id: \.offset) { index, question in
|
||||||
|
if index > 0 { Divider().overlay(accent.opacity(0.12)) }
|
||||||
|
questionRow(question)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12).padding(.vertical, 10)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(accent.opacity(0.06))
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(accent.opacity(0.16), lineWidth: 1))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func questionRow(_ question: AskUserQuestion.Question) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
if let header = question.header, !header.isEmpty {
|
||||||
|
Text(header.uppercased())
|
||||||
|
.font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Text(question.question)
|
||||||
|
.font(.callout).foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
if let chosen = answers[question.question], !chosen.isEmpty {
|
||||||
|
ForEach(Array(chosen.enumerated()), id: \.offset) { _, label in
|
||||||
|
answerRow(label, description: description(of: label, in: question))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// An answered card only shows for questions that parsed an answer; this is the
|
||||||
|
// honest fallback if one question in a multi-question ask somehow didn't.
|
||||||
|
Text("No answer recorded")
|
||||||
|
.font(.callout).italic().foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func answerRow(_ label: String, description: String?) -> some View {
|
||||||
|
HStack(alignment: .top, spacing: 7) {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.caption).foregroundStyle(accent)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(label)
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
if let description, !description.isEmpty {
|
||||||
|
Text(description)
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The chosen option's description, when the answer matches a known option label.
|
||||||
|
private func description(of label: String, in question: AskUserQuestion.Question) -> String? {
|
||||||
|
question.options.first { $0.label == label }?.description
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user