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 } } private var label: String { switch state { case .idle: "Idle" case .connecting: "Connecting" case .listening: "Listening" case .speaking: "Speaking" case .thinking: "Thinking" case .failed: "Connection failed" } } 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 } .accessibilityElement() .accessibilityLabel("Conversation status") .accessibilityValue(label) } } struct TranscriptListView: View { let events: [TranscriptEvent] @ObservedObject var history: TranscriptHistory var onClear: () -> Void private static let deliveryMarkers = #"\[(?:appreciative|calm|concerned|curious|encouraging|excited|happy|joking|quiet|reassuring|reflective|sad|serious|surprised|sympathetic|urgent|whispering|laughter|sighing|breathing|neutral)\]\s*"# private static let timestampFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .none formatter.timeStyle = .short return formatter }() var body: some View { ScrollViewReader { proxy in VStack(alignment: .leading, spacing: 0) { HStack { Text("Transcript") .font(.caption.bold()) .foregroundStyle(.secondary) Spacer() Button(role: .destructive, action: onClear) { Label("Clear", systemImage: "trash") .font(.caption) } } .padding(.horizontal) .padding(.bottom, 4) 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) { HStack { Text(roleName) .font(.caption2.bold()) .foregroundStyle(nameColor) Spacer() Text(Self.timestampFormatter.string(from: event.timestamp)) .font(.caption2) .foregroundStyle(.tertiary) } 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 { text.replacingOccurrences( of: Self.deliveryMarkers, with: "", options: .regularExpression ).trimmingCharacters(in: .whitespaces) } }