2025-12-08 16:29:13 +01:00
|
|
|
|
import AppKit
|
2025-12-14 05:04:58 +00:00
|
|
|
|
import Observation
|
2025-12-08 16:29:13 +01:00
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
|
|
/// Lightweight, borderless panel that shows the current voice wake transcript near the menu bar.
|
|
|
|
|
|
@MainActor
|
2025-12-14 05:04:58 +00:00
|
|
|
|
@Observable
|
|
|
|
|
|
final class VoiceWakeOverlayController {
|
2025-12-08 16:29:13 +01:00
|
|
|
|
static let shared = VoiceWakeOverlayController()
|
|
|
|
|
|
|
2026-01-04 14:32:47 +00:00
|
|
|
|
let logger = Logger(subsystem: "com.clawdbot", category: "voicewake.overlay")
|
2025-12-24 20:09:56 +01:00
|
|
|
|
let enableUI: Bool
|
2025-12-09 03:18:05 +01:00
|
|
|
|
|
2026-01-04 14:32:47 +00:00
|
|
|
|
/// Keep the voice wake overlay above any other Clawdbot windows, but below the system’s pop-up menus.
|
2025-12-12 22:09:14 +00:00
|
|
|
|
/// (Menu bar menus typically live at `.popUpMenu`.)
|
2025-12-24 20:09:56 +01:00
|
|
|
|
static let preferredWindowLevel = NSWindow.Level(rawValue: NSWindow.Level.popUpMenu.rawValue - 4)
|
2025-12-12 22:09:14 +00:00
|
|
|
|
|
2025-12-09 04:29:34 +01:00
|
|
|
|
enum Source: String { case wakeWord, pushToTalk }
|
|
|
|
|
|
|
2025-12-24 20:17:01 +01:00
|
|
|
|
var model = Model()
|
2025-12-09 05:02:03 +01:00
|
|
|
|
var isVisible: Bool { self.model.isVisible }
|
2025-12-08 16:29:13 +01:00
|
|
|
|
|
|
|
|
|
|
struct Model {
|
|
|
|
|
|
var text: String = ""
|
|
|
|
|
|
var isFinal: Bool = false
|
|
|
|
|
|
var isVisible: Bool = false
|
|
|
|
|
|
var forwardEnabled: Bool = false
|
2025-12-08 16:32:38 +01:00
|
|
|
|
var isSending: Bool = false
|
2025-12-09 04:42:32 +01:00
|
|
|
|
var attributed: NSAttributedString = .init(string: "")
|
2025-12-08 18:50:14 +01:00
|
|
|
|
var isOverflowing: Bool = false
|
2025-12-08 19:11:59 +01:00
|
|
|
|
var isEditing: Bool = false
|
2025-12-08 22:28:49 +01:00
|
|
|
|
var level: Double = 0 // normalized 0...1 speech level for UI
|
2025-12-08 16:29:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 20:09:56 +01:00
|
|
|
|
var window: NSPanel?
|
|
|
|
|
|
var hostingView: NSHostingView<VoiceWakeOverlayView>?
|
|
|
|
|
|
var autoSendTask: Task<Void, Never>?
|
|
|
|
|
|
var autoSendToken: UUID?
|
|
|
|
|
|
var activeToken: UUID?
|
|
|
|
|
|
var activeSource: Source?
|
|
|
|
|
|
var lastLevelUpdate: TimeInterval = 0
|
|
|
|
|
|
|
|
|
|
|
|
let width: CGFloat = 360
|
|
|
|
|
|
let padding: CGFloat = 10
|
|
|
|
|
|
let buttonWidth: CGFloat = 36
|
|
|
|
|
|
let spacing: CGFloat = 8
|
|
|
|
|
|
let verticalPadding: CGFloat = 8
|
|
|
|
|
|
let maxHeight: CGFloat = 400
|
|
|
|
|
|
let minHeight: CGFloat = 48
|
2025-12-09 03:18:05 +01:00
|
|
|
|
let closeOverflow: CGFloat = 10
|
2025-12-24 20:09:56 +01:00
|
|
|
|
let levelUpdateInterval: TimeInterval = 1.0 / 12.0
|
2025-12-08 22:28:49 +01:00
|
|
|
|
|
2025-12-08 16:29:13 +01:00
|
|
|
|
enum DismissReason { case explicit, empty }
|
2025-12-08 16:49:58 +01:00
|
|
|
|
enum SendOutcome { case sent, empty }
|
2025-12-10 00:47:49 +01:00
|
|
|
|
enum GuardOutcome { case accept, dropMismatch, dropNoActive }
|
2025-12-09 19:51:51 +01:00
|
|
|
|
|
2025-12-24 20:09:56 +01:00
|
|
|
|
init(enableUI: Bool = true) {
|
|
|
|
|
|
self.enableUI = enableUI
|
2025-12-24 17:42:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|