Files
clawdbot/apps/macos/Sources/Clawdis/VoiceWakeForwardSection.swift
2025-12-07 23:04:51 +01:00

120 lines
4.8 KiB
Swift

import SwiftUI
enum VoiceWakeForwardStatus: Equatable {
case idle
case checking
case ok
case failed(String)
}
struct VoiceWakeForwardSection: View {
@Binding var enabled: Bool
@Binding var target: String
@Binding var identity: String
@Binding var command: String
@Binding var showAdvanced: Bool
@Binding var status: VoiceWakeForwardStatus
let onTest: () -> Void
let onChange: () -> Void
var showToggle: Bool = true
var title: String = "Forward wake to host (SSH)"
var subtitle: String = "Send wake transcripts to a remote Clawdis host."
var body: some View {
VStack(alignment: .leading, spacing: 8) {
if self.showToggle {
Toggle(isOn: self.$enabled) {
Text(self.title)
}
} else {
Text(self.title)
.font(.callout.weight(.semibold))
Text(self.subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
if self.enabled {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 10) {
Text("SSH")
.font(.callout.weight(.semibold))
.frame(width: 40, alignment: .leading)
TextField("steipete@peters-mac-studio-1", text: self.$target)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: .infinity)
.onChange(of: self.target) { _, _ in
self.onChange()
}
self.statusIcon
.frame(width: 16, height: 16, alignment: .center)
Button("Test") { self.onTest() }
.disabled(self.target.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
if case let .failed(message) = self.status {
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(5)
}
DisclosureGroup(isExpanded: self.$showAdvanced) {
VStack(alignment: .leading, spacing: 10) {
LabeledContent("Identity file") {
TextField(
"/Users/you/.ssh/voicewake_ed25519",
text: self.$identity)
.textFieldStyle(.roundedBorder)
.frame(width: 320)
.onChange(of: self.identity) { _, _ in
self.onChange()
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Remote command template")
.font(.callout.weight(.semibold))
TextField(
"clawdis-mac agent --message \"${text}\" --thinking low",
text: self.$command,
axis: .vertical)
.textFieldStyle(.roundedBorder)
.onChange(of: self.command) { _, _ in
self.onChange()
}
Text(
"${text} is replaced with the transcript."
+ "\nIt is also piped to stdin if you prefer $(cat).")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(.top, 4)
} label: {
Text("Advanced")
.font(.callout.weight(.semibold))
}
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
}
private var statusIcon: some View {
Group {
switch self.status {
case .idle:
Image(systemName: "circle.dashed").foregroundStyle(.secondary)
case .checking:
ProgressView().controlSize(.mini)
case .ok:
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
case .failed:
Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.yellow)
}
}
}
}