Redesign Hope iOS UI, harden playback, and document project handoff

This commit is contained in:
snowspeeder committed 2026-09-09 20:23:44 -04:00
1 parent 40aaf4fea7
commit 24f95639dc
24 files changed
+1470 -594

No files matched your search

@@ -26,7 +26,7 @@ struct ConnectionTestView: View {
if isBusy {
ProgressView()
} else {
Text("Get token")
Text("Test connection")
}
Spacer()
}
@@ -44,7 +44,7 @@ struct ConnectionTestView: View {
LabeledContent("User") {
Text(response.user.displayName ?? response.user.id)
}
Label("Token endpoint and TLS are working.", systemImage: "checkmark.circle")
Label("Connected to your server. Start a conversation to check Hope’s voice and replies.", systemImage: "checkmark.circle")
.foregroundStyle(.green)
}
case .failure(let error):
@@ -57,6 +57,7 @@ struct ConnectionTestView: View {
}
}
.navigationTitle("Connection test")
.navigationBarTitleDisplayMode(.inline)
}
private func runTest() {
+44 -32
View File
@@ -30,18 +30,48 @@ struct SettingsView: View {
var body: some View {
Form {
Section("Server") {
Section {
Button {
showVoicePicker = true
} label: {
LabeledContent("Speaking voice", value: Voices.display(for: settings.voiceID) ?? settings.voiceID)
}
Picker("Conversation mode", selection: $settings.textMode) {
Text("Voice").tag(false)
Text("Type · Microphone off").tag(true)
}
} header: {
Text("Make it yours")
} footer: {
Text("Voice and typing preferences are saved on this iPhone. You can switch modes from the Talk tab.")
}
Section {
if session.isReady, !session.models.isEmpty {
Button {
showModelPicker = true
} label: {
LabeledContent("Model", value: session.selectedModel ?? settings.modelID ?? "Server default")
}
} else {
LabeledContent("Model", value: settings.modelID ?? "Server default")
Text("Connect from Talk to see the models available on your server.")
.font(.footnote).foregroundStyle(.secondary)
}
} header: { Text("Intelligence") }
Section {
TextField(SettingsStore.defaultServerURL, text: $urlText)
.keyboardType(.URL)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.accessibilityLabel("Server URL")
.onAppear { urlText = settings.serverURL }
if let urlError {
Text(urlError)
.font(.footnote)
.foregroundStyle(.red)
}
Button("Apply server URL") { applyServerURL() }
.disabled(!canApplyServerURL || urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
Button("Save server address") { applyServerURL() }
.disabled(!canApplyServerURL || urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || urlText == settings.serverURL)
if !canApplyServerURL {
Text(session.state == .tearingDown
? "Waiting for failure cleanup to finish…"
@@ -49,37 +79,17 @@ struct SettingsView: View {
.font(.footnote)
.foregroundStyle(.secondary)
}
}
Section("Voice") {
Button {
showVoicePicker = true
} label: {
LabeledContent("Voice", value: Voices.display(for: settings.voiceID) ?? settings.voiceID)
}
}
Section("Model") {
if session.isReady, !session.models.isEmpty {
Button {
showModelPicker = true
} label: {
LabeledContent("Model", value: session.selectedModel ?? settings.modelID ?? "—")
}
} else {
Text("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)
}
} header: {
Text("Connection")
} footer: {
Text("Hope runs on your own server. Your iPhone needs to be on the same network or a network that can reach it.")
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showVoicePicker) {
VoicePickerView(settings: settings, session: session)
}
@@ -115,7 +125,7 @@ struct SettingsView: View {
struct VoicePickerView: View {
@ObservedObject var settings: SettingsStore
var session: VoiceSession
@ObservedObject var session: VoiceSession
@Environment(\.dismiss) private var dismiss
var body: some View {
@@ -132,10 +142,11 @@ struct VoicePickerView: View {
if settings.voiceID == voice.id {
Spacer()
Image(systemName: "checkmark")
.foregroundStyle(.blue)
.foregroundStyle(HopeStyle.tint)
}
}
}
.accessibilityAddTraits(settings.voiceID == voice.id ? .isSelected : [])
}
}
.navigationTitle("Voice")
@@ -150,7 +161,7 @@ struct VoicePickerView: View {
struct ModelPickerView: View {
@ObservedObject var settings: SettingsStore
var session: VoiceSession
@ObservedObject var session: VoiceSession
@Environment(\.dismiss) private var dismiss
private var activeModelID: String? {
@@ -176,10 +187,11 @@ struct ModelPickerView: View {
if activeModelID == model.id {
Spacer()
Image(systemName: "checkmark")
.foregroundStyle(.blue)
.foregroundStyle(HopeStyle.tint)
}
}
}
.accessibilityAddTraits(activeModelID == model.id ? .isSelected : [])
}
}
.navigationTitle("Model")