mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
Add TTS toggle button to voice controls
Voice toolbar now shows three controls when voice is enabled: - Mic toggle (voice on/off) - TTS toggle (speaker icon, sends /voice tts) - Push to Talk (waveform, sends Ctrl+B) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@ final class ChatViewModel {
|
|||||||
var sessionPreviews: [String: String] = [:]
|
var sessionPreviews: [String: String] = [:]
|
||||||
var terminalView: LocalProcessTerminalView?
|
var terminalView: LocalProcessTerminalView?
|
||||||
var hasActiveProcess = false
|
var hasActiveProcess = false
|
||||||
|
var voiceEnabled = false
|
||||||
|
var ttsEnabled = false
|
||||||
|
var isRecording = false
|
||||||
private var coordinator: Coordinator?
|
private var coordinator: Coordinator?
|
||||||
|
|
||||||
var hermesBinaryExists: Bool {
|
var hermesBinaryExists: Bool {
|
||||||
@@ -17,14 +20,23 @@ final class ChatViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startNewSession() {
|
func startNewSession() {
|
||||||
|
voiceEnabled = false
|
||||||
|
ttsEnabled = false
|
||||||
|
isRecording = false
|
||||||
launchTerminal(arguments: ["chat"])
|
launchTerminal(arguments: ["chat"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func resumeSession(_ sessionId: String) {
|
func resumeSession(_ sessionId: String) {
|
||||||
|
voiceEnabled = false
|
||||||
|
ttsEnabled = false
|
||||||
|
isRecording = false
|
||||||
launchTerminal(arguments: ["chat", "--resume", sessionId])
|
launchTerminal(arguments: ["chat", "--resume", sessionId])
|
||||||
}
|
}
|
||||||
|
|
||||||
func continueLastSession() {
|
func continueLastSession() {
|
||||||
|
voiceEnabled = false
|
||||||
|
ttsEnabled = false
|
||||||
|
isRecording = false
|
||||||
launchTerminal(arguments: ["chat", "--continue"])
|
launchTerminal(arguments: ["chat", "--continue"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +54,37 @@ final class ChatViewModel {
|
|||||||
return session.id
|
return session.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleVoice() {
|
||||||
|
guard let tv = terminalView else { return }
|
||||||
|
if voiceEnabled {
|
||||||
|
sendToTerminal(tv, text: "/voice off\r")
|
||||||
|
voiceEnabled = false
|
||||||
|
isRecording = false
|
||||||
|
} else {
|
||||||
|
sendToTerminal(tv, text: "/voice on\r")
|
||||||
|
voiceEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleTTS() {
|
||||||
|
guard let tv = terminalView, voiceEnabled else { return }
|
||||||
|
sendToTerminal(tv, text: "/voice tts\r")
|
||||||
|
ttsEnabled.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushToTalk() {
|
||||||
|
guard let tv = terminalView, voiceEnabled else { return }
|
||||||
|
// Ctrl+B = ASCII 0x02
|
||||||
|
let ctrlB: [UInt8] = [0x02]
|
||||||
|
tv.send(source: tv, data: ctrlB[0..<1])
|
||||||
|
isRecording.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendToTerminal(_ tv: LocalProcessTerminalView, text: String) {
|
||||||
|
let bytes = Array(text.utf8)
|
||||||
|
tv.send(source: tv, data: bytes[0..<bytes.count])
|
||||||
|
}
|
||||||
|
|
||||||
private func launchTerminal(arguments: [String]) {
|
private func launchTerminal(arguments: [String]) {
|
||||||
if let existing = terminalView {
|
if let existing = terminalView {
|
||||||
existing.terminate()
|
existing.terminate()
|
||||||
@@ -55,6 +98,8 @@ final class ChatViewModel {
|
|||||||
|
|
||||||
let coord = Coordinator(onTerminated: { [weak self] in
|
let coord = Coordinator(onTerminated: { [weak self] in
|
||||||
self?.hasActiveProcess = false
|
self?.hasActiveProcess = false
|
||||||
|
self?.voiceEnabled = false
|
||||||
|
self?.isRecording = false
|
||||||
})
|
})
|
||||||
terminal.processDelegate = coord
|
terminal.processDelegate = coord
|
||||||
self.coordinator = coord
|
self.coordinator = coord
|
||||||
|
|||||||
@@ -105,12 +105,26 @@ struct ChatView: View {
|
|||||||
.help("Toggle voice mode (/voice)")
|
.help("Toggle voice mode (/voice)")
|
||||||
|
|
||||||
if viewModel.voiceEnabled {
|
if viewModel.voiceEnabled {
|
||||||
|
Button {
|
||||||
|
viewModel.toggleTTS()
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: viewModel.ttsEnabled ? "speaker.wave.2.fill" : "speaker.slash")
|
||||||
|
.foregroundStyle(viewModel.ttsEnabled ? .green : .secondary)
|
||||||
|
Text(viewModel.ttsEnabled ? "TTS On" : "TTS Off")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(viewModel.ttsEnabled ? .primary : .secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Toggle text-to-speech (/voice tts)")
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.pushToTalk()
|
viewModel.pushToTalk()
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: viewModel.isRecording ? "waveform.circle.fill" : "waveform.circle")
|
Image(systemName: viewModel.isRecording ? "waveform.circle.fill" : "waveform.circle")
|
||||||
.foregroundStyle(viewModel.isRecording ? .red : .accentColor)
|
.foregroundStyle(viewModel.isRecording ? .red : Color.accentColor)
|
||||||
.symbolEffect(.pulse, isActive: viewModel.isRecording)
|
.symbolEffect(.pulse, isActive: viewModel.isRecording)
|
||||||
Text(viewModel.isRecording ? "Recording..." : "Push to Talk")
|
Text(viewModel.isRecording ? "Recording..." : "Push to Talk")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
Reference in New Issue
Block a user