- Voices.swift: 13 Azure voices, default en-US-Ava - SettingsStore: persist voiceID/modelID, fallback to default voice - VoiceSession: sync set_voice/set_model on agent_ready, selectVoice/selectModel, track models + model_changed - SettingsView: voice and model picker sheets - ControlChannel: publish via publish(data:) reliable data channel instead of sendText DataStream (agent listens on legacy data_received; DataStream text was dropped)
150 lines
4.9 KiB
Swift
150 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
@ObservedObject var settings: SettingsStore
|
|
var session: VoiceSession?
|
|
@State private var urlText: String = ""
|
|
@State private var showVoicePicker = false
|
|
@State private var showModelPicker = false
|
|
|
|
init(settings: SettingsStore, session: VoiceSession? = nil) {
|
|
self.settings = settings
|
|
self.session = session
|
|
}
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("Server") {
|
|
TextField("https://192.168.86.2:9178", text: $urlText)
|
|
.keyboardType(.URL)
|
|
.autocorrectionDisabled()
|
|
.onAppear { urlText = settings.serverURL }
|
|
.onChange(of: urlText) { _, newValue in
|
|
settings.serverURL = newValue
|
|
}
|
|
}
|
|
Section("Voice") {
|
|
Button {
|
|
showVoicePicker = true
|
|
} label: {
|
|
LabeledContent("Voice", value: Voices.display(for: settings.voiceID) ?? settings.voiceID)
|
|
}
|
|
}
|
|
Section("Model") {
|
|
if let session, !session.models.isEmpty {
|
|
Button {
|
|
showModelPicker = true
|
|
} label: {
|
|
LabeledContent("Model", value: session.selectedModel ?? settings.modelID ?? "—")
|
|
}
|
|
} else {
|
|
Text(session != nil ? "Loading available models…" : "Start a conversation to load the model list.")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Section("Interaction") {
|
|
Toggle("Text mode", isOn: $settings.textMode)
|
|
}
|
|
Section {
|
|
NavigationLink("Connection test") {
|
|
ConnectionTestView(settings: settings)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
.sheet(isPresented: $showVoicePicker) {
|
|
VoicePickerView(settings: settings, session: session)
|
|
}
|
|
.sheet(isPresented: $showModelPicker) {
|
|
ModelPickerView(settings: settings, session: session)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct VoicePickerView: View {
|
|
@ObservedObject var settings: SettingsStore
|
|
var session: VoiceSession?
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
ForEach(Voices.all, id: \.id) { voice in
|
|
Button {
|
|
settings.voiceID = voice.id
|
|
Task { await session?.selectVoice(voice.id) }
|
|
dismiss()
|
|
} label: {
|
|
HStack {
|
|
Text(voice.displayName)
|
|
if settings.voiceID == voice.id {
|
|
Spacer()
|
|
Image(systemName: "checkmark")
|
|
.foregroundStyle(.blue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Voice")
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ModelPickerView: View {
|
|
@ObservedObject var settings: SettingsStore
|
|
var session: VoiceSession?
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
private var activeModelID: String? {
|
|
guard let session else { return settings.modelID }
|
|
if !session.models.isEmpty,
|
|
let selected = session.selectedModel ?? settings.modelID,
|
|
session.models.contains(where: { $0.id == selected }) {
|
|
return selected
|
|
}
|
|
return session.models.first?.id
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
ForEach(session?.models ?? [], id: \.id) { model in
|
|
Button {
|
|
settings.modelID = model.id
|
|
Task { await session?.selectModel(model.id) }
|
|
dismiss()
|
|
} label: {
|
|
HStack {
|
|
Text(model.name ?? model.id)
|
|
if activeModelID == model.id {
|
|
Spacer()
|
|
Image(systemName: "checkmark")
|
|
.foregroundStyle(.blue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Model")
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
SettingsView(settings: SettingsStore())
|
|
}
|
|
}
|