a2328de4d3
The name field was the first row of the third "Transcription" section, below the fold — users couldn't find where to set their name (it's the setting that labels the mic channel and reserves the name so the LLM never assigns it to another speaker). Move it into a dedicated "Your name" section at the top of Settings, and show an orange nudge while it's still the placeholder "Me"/empty.
111 lines
4.7 KiB
Swift
111 lines
4.7 KiB
Swift
import SwiftUI
|
||
import AppKit
|
||
|
||
/// Settings panel (pushed from the menu-bar panel).
|
||
struct SettingsView: View {
|
||
@EnvironmentObject private var settings: AppSettings
|
||
|
||
var body: some View {
|
||
Form {
|
||
Section("Your name") {
|
||
TextField("Your name", text: $settings.selfName)
|
||
.textFieldStyle(.roundedBorder)
|
||
if isDefaultName {
|
||
Label("Still set to the default. Enter your real name so your own voice is labeled correctly — and so the AI never gives your name to someone else.",
|
||
systemImage: "exclamationmark.triangle.fill")
|
||
.font(.caption)
|
||
.foregroundStyle(.orange)
|
||
} else {
|
||
Text("Labels your microphone channel as you in every transcript, and reserves this name so it’s never assigned to another speaker.")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
Section("SparkControl backend") {
|
||
TextField("Base URL", text: $settings.backendBaseURL)
|
||
.textFieldStyle(.roundedBorder)
|
||
Toggle("Skip TLS verification (self-signed cert)",
|
||
isOn: $settings.skipTLSVerification)
|
||
}
|
||
|
||
Section("Call detection") {
|
||
Toggle("Auto-record when a call is detected", isOn: $settings.autoRecordOnDetection)
|
||
Text("Detects Zoom, Teams, Signal, and Google Meet (any browser).")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
Section("Transcription") {
|
||
Toggle("Auto-send recordings to backend", isOn: $settings.autoSendOnStop)
|
||
Toggle("Reconcile speakers (merge splits + name from content)", isOn: $settings.reconcileSpeakers)
|
||
Toggle("Build readable recap (topics + highlights)", isOn: $settings.recapEnabled)
|
||
HStack {
|
||
Picker("Default recap template", selection: $settings.defaultTemplateId) {
|
||
ForEach(settings.recapTemplates) { Text($0.name).tag($0.id) }
|
||
}
|
||
Button("Manage…") { TemplatesWindow.shared.show(settings: settings) }
|
||
}
|
||
Text("Auto-send transcribes on stop; the recap writes transcript.md + recap.html. Templates define the takeaways categories per meeting type.")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
Section("Output") {
|
||
HStack {
|
||
Text(settings.outputFolderPath)
|
||
.lineLimit(1)
|
||
.truncationMode(.middle)
|
||
.foregroundStyle(.secondary)
|
||
Spacer()
|
||
Button("Choose…", action: chooseFolder)
|
||
}
|
||
}
|
||
|
||
Section("Adapters") {
|
||
Text("Inert in Phase 0 — these toggles only persist for now.")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
ForEach(AppSettings.adapterKeys, id: \.key) { adapter in
|
||
Toggle(adapter.label, isOn: binding(for: adapter.key))
|
||
}
|
||
}
|
||
}
|
||
.formStyle(.grouped)
|
||
.frame(width: 320)
|
||
.navigationTitle("Settings")
|
||
}
|
||
|
||
/// True while the user still has the placeholder name — drives the inline nudge.
|
||
private var isDefaultName: Bool {
|
||
let n = settings.selfName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return n.isEmpty || n.caseInsensitiveCompare("Me") == .orderedSame
|
||
}
|
||
|
||
private func binding(for key: String) -> Binding<Bool> {
|
||
Binding(
|
||
get: { settings.adapterEnabled[key] ?? true },
|
||
set: { settings.adapterEnabled[key] = $0 }
|
||
)
|
||
}
|
||
|
||
private func chooseFolder() {
|
||
let panel = NSOpenPanel()
|
||
panel.canChooseDirectories = true
|
||
panel.canChooseFiles = false
|
||
panel.allowsMultipleSelection = false
|
||
panel.prompt = "Choose"
|
||
panel.directoryURL = settings.outputFolderURL
|
||
|
||
// The app is a menu-bar accessory and this is invoked from the transient
|
||
// MenuBarExtra(.window) popover. Use the async begin(...) API rather than
|
||
// runModal() — a nested modal loop can let the popover dismiss the panel
|
||
// out from under it. Activate first so the panel comes to the front.
|
||
NSApp.activate(ignoringOtherApps: true)
|
||
panel.begin { response in
|
||
guard response == .OK, let url = panel.url else { return }
|
||
settings.outputFolderPath = url.path
|
||
}
|
||
}
|
||
}
|