import SwiftUI struct OrbView: View { enum OrbState { case idle case connecting case listening case speaking case thinking case failed } let state: OrbState @State private var animating = false private var baseColor: Color { switch state { case .idle, .connecting: .blue case .listening: .green case .speaking, .thinking: .orange case .failed: .red } } private var scale: CGFloat { switch state { case .connecting, .listening, .speaking, .thinking: 1.1 default: 1.0 } } var body: some View { let shape = Circle() .fill(baseColor.gradient) .frame(width: 120, height: 120) let animated = shape .scaleEffect(animating ? scale : 0.95) .opacity(state == .idle || state == .failed ? 0.7 : 1.0) let anim: Animation? = state == .idle || state == .failed ? nil : .easeInOut(duration: 0.8).repeatForever(autoreverses: true) animated .animation(anim, value: animating) .onAppear { animating = true } .onChange(of: state) { _, _ in animating = true } } } struct TranscriptListView: View { let events: [TranscriptEvent] var body: some View { ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 8) { ForEach(events) { event in transcriptRow(event) } } .padding(.horizontal) } .onChange(of: events.count) { _, count in if let last = events.last { withAnimation { proxy.scrollTo(last.id, anchor: .bottom) } } } } } private func transcriptRow(_ event: TranscriptEvent) -> some View { let roleName = event.role == .user ? "You" : "Hope" let nameColor: Color = event.role == .user ? .secondary : .blue let rowBackground: Color = event.role == .user ? Color(.secondarySystemBackground) : Color.blue.opacity(0.08) let textOpacity: Double = event.isFinal ? 1.0 : 0.55 return VStack(alignment: .leading, spacing: 2) { Text(roleName) .font(.caption2.bold()) .foregroundStyle(nameColor) Text(stripped(event.text)) .font(.body) .opacity(textOpacity) } .frame(maxWidth: .infinity, alignment: .leading) .padding(8) .background(rowBackground) .cornerRadius(10) .id(event.id) } private func stripped(_ text: String) -> String { // Strip delivery-style markers like [appreciative], [pauses] etc. text.replacingOccurrences( of: #"\[[a-zA-Z ,'-]+\]"#, with: "", options: .regularExpression ).trimmingCharacters(in: .whitespaces) } }