import SwiftUI struct ContentView: View { @StateObject private var streamManager: AudioStreamManager @StateObject private var captureManager = AudioCaptureManager() @State private var isPressing = false init() { let manager = AudioStreamManager(endpoint: URL(string: "ws://192.168.68.126:8642/stream")!) _streamManager = StateObject(wrappedValue: manager) } var body: some View { VStack(spacing: 14) { Text("Hermes") .font(.headline) Text(statusText) .font(.caption2) .foregroundStyle(statusColor) .lineLimit(2) .multilineTextAlignment(.center) .frame(minHeight: 28) Circle() .fill(buttonFill) .overlay { Image(systemName: isPressing ? "waveform" : "mic.fill") .font(.system(size: 36, weight: .semibold)) .foregroundStyle(.white) } .overlay { Circle() .stroke(.white.opacity(isPressing ? 0.9 : 0.25), lineWidth: 3) } .frame(width: 112, height: 112) .scaleEffect(isPressing ? 0.94 : 1.0) .animation(.spring(response: 0.2, dampingFraction: 0.75), value: isPressing) .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in guard !isPressing else { return } isPressing = true startPTT() } .onEnded { _ in isPressing = false stopPTT() } ) Text(isPressing ? "Release to send" : "Hold to talk") .font(.caption2) .foregroundStyle(.secondary) } .padding() .onAppear { streamManager.connect() captureManager.onPCMChunk = { [streamManager] chunk in streamManager.sendPCMChunk(chunk) } } .onDisappear { captureManager.stopRecording() streamManager.disconnect() } } private var statusText: String { switch captureManager.state { case .recording: return "Listening" case .starting: return "Starting mic" case .failed(let message): return message case .idle: break } switch streamManager.playbackState { case .playing: return "Playing response" case .failed(let message): return message case .idle: break } switch streamManager.connectionState { case .connected: return "Ready" case .connecting: return "Connecting" case .disconnected: return "Disconnected" case .failed(let message): return message } } private var statusColor: Color { if case .failed = captureManager.state { return .red } if case .failed = streamManager.connectionState { return .red } if case .failed = streamManager.playbackState { return .red } if captureManager.state == .recording { return .green } if streamManager.playbackState == .playing { return .blue } return .secondary } private var buttonFill: Color { if captureManager.state == .recording { return .red } if streamManager.playbackState == .playing { return .blue } return .accentColor } private func startPTT() { streamManager.connect() streamManager.sendControlEvent("start") Task { await captureManager.startRecording() } } private func stopPTT() { captureManager.stopRecording() streamManager.sendControlEvent("stop") } } #Preview { ContentView() }