updating transcript row changes'
This commit is contained in:
@@ -111,15 +111,7 @@ struct MessageBubble: View {
|
||||
|
||||
var body: some View {
|
||||
if role == .user {
|
||||
HStack {
|
||||
Spacer(minLength: 48)
|
||||
MarkdownText(markdown: text)
|
||||
.textSelection(.enabled)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
.background(Palette.accent.opacity(0.15),
|
||||
in: RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
}
|
||||
UserMessageBubble(text: text)
|
||||
} else {
|
||||
MarkdownText(markdown: text)
|
||||
.textSelection(.enabled)
|
||||
@@ -136,6 +128,93 @@ struct MessageBubble: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The user's own message: a right-aligned accent bubble that collapses a long message to a
|
||||
/// capped height with a "Show more"/"Show less" toggle — the phone echo of the Mac's
|
||||
/// `UserMessageBubble`. The Mac clamps a plain `Text` by line count; here the bubble renders
|
||||
/// through `MarkdownText` (a stack of per-line/-block `Text`s that `.lineLimit` can't reach), so
|
||||
/// we clamp by height instead. A hidden full-height copy of the bubble — laid out at the visible
|
||||
/// bubble's width but with no height limit — measures the natural content height, so the toggle
|
||||
/// appears only when collapsing actually hides something. The last visible line fades when
|
||||
/// collapsed so the cut reads as "more below" rather than a glyph sliced in half.
|
||||
private struct UserMessageBubble: View {
|
||||
let text: String
|
||||
@State private var expanded = false
|
||||
@State private var fullHeight: CGFloat = 0
|
||||
/// Collapsed bubbles show at most this much prose (~12 wrapped lines at the base size, matching
|
||||
/// the Mac's 12-line clamp) before a "Show more". A `@ScaledMetric` so it grows with the user's
|
||||
/// text-size setting alongside `MarkdownText`'s own scaled prose size.
|
||||
@ScaledMetric(relativeTo: .body) private var collapsedMaxHeight: CGFloat = 288
|
||||
/// Collapsing only hides something — and the toggle is only meaningful — when the full,
|
||||
/// unconstrained content is taller than the collapsed cap. The +1 absorbs sub-point rounding.
|
||||
private var isTruncated: Bool { fullHeight > collapsedMaxHeight + 1 }
|
||||
private var collapsed: Bool { isTruncated && !expanded }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
HStack {
|
||||
Spacer(minLength: 48)
|
||||
MarkdownText(markdown: text)
|
||||
.textSelection(.enabled)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
// Cap the height whenever collapsed; harmless for a short message (the cap sits
|
||||
// above its natural height, so nothing clips). `.clipped` cuts the overflow.
|
||||
.frame(maxHeight: expanded ? nil : collapsedMaxHeight, alignment: .top)
|
||||
.clipped()
|
||||
.mask {
|
||||
if collapsed { collapseFade } else { Color.black }
|
||||
}
|
||||
// The bubble fill is added after the mask, so only the prose fades — not the
|
||||
// accent behind it, which stays a solid rounded rect.
|
||||
.background(alignment: .top) { heightProbe }
|
||||
.background(Palette.accent.opacity(0.15),
|
||||
in: RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
}
|
||||
if isTruncated {
|
||||
Button { expanded.toggle() } label: {
|
||||
Text(expanded ? "Show less" : "Show more")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(Palette.accent)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A hidden copy of the bubble laid out at the visible bubble's width but with no height limit,
|
||||
/// so its measured height is the full content height regardless of the collapse cap. Attached as
|
||||
/// a background so it inherits the visible bubble's width and wraps identically.
|
||||
private var heightProbe: some View {
|
||||
MarkdownText(markdown: text)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.background(GeometryReader { geo in
|
||||
Color.clear.preference(key: FullHeightKey.self, value: geo.size.height)
|
||||
})
|
||||
.hidden()
|
||||
.onPreferenceChange(FullHeightKey.self) { fullHeight = $0 }
|
||||
}
|
||||
|
||||
/// Solid for all but the bottom sliver, then fades to clear — so a collapsed bubble's last
|
||||
/// visible line dissolves instead of being clipped mid-glyph.
|
||||
private var collapseFade: some View {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .black, location: 0),
|
||||
.init(color: .black, location: 0.86),
|
||||
.init(color: .clear, location: 1),
|
||||
],
|
||||
startPoint: .top, endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
private struct FullHeightKey: PreferenceKey {
|
||||
static let defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) }
|
||||
}
|
||||
|
||||
/// The agent's reasoning, set apart from its prose by a quiet left rule and a `sparkles` glyph,
|
||||
/// italic and muted — the phone echo of the Mac's `thinkingRow`. Renders nothing for an
|
||||
/// empty/redacted thinking block (matching the desktop), so a placeholder row never shows.
|
||||
|
||||
Reference in New Issue
Block a user