feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// FlowSessionBridge+Transcription.swift
|
||||
// OSGKeyboard · Shared
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FlowSessionBridge {
|
||||
// MARK: - Recording signals (keyboard → host)
|
||||
|
||||
public static func setRecordingState(
|
||||
_ state: FlowSessionKeys.RecordingState,
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
store.set(state.rawValue, forKey: FlowSessionKeys.keyboardRecordingState)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
}
|
||||
|
||||
public static func recordingState(
|
||||
defaults: UserDefaults? = nil
|
||||
) -> FlowSessionKeys.RecordingState {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
let raw = store.string(forKey: FlowSessionKeys.keyboardRecordingState) ?? FlowSessionKeys.RecordingState.idle.rawValue
|
||||
return FlowSessionKeys.RecordingState(rawValue: raw) ?? .idle
|
||||
}
|
||||
|
||||
public static func setTranscriptionLanguage(
|
||||
_ localeId: String,
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
store.set(localeId, forKey: FlowSessionKeys.transcriptionLanguage)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
}
|
||||
|
||||
// MARK: - Results (host → keyboard)
|
||||
|
||||
public static func storeTranscriptionResult(
|
||||
_ text: String,
|
||||
polishWarning: String? = nil,
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
store.set(trimmed, forKey: FlowSessionKeys.transcriptionResult)
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionError)
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionPartial)
|
||||
if let polishWarning, !polishWarning.isEmpty {
|
||||
store.set(polishWarning, forKey: FlowSessionKeys.transcriptionPolishWarning)
|
||||
} else {
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionPolishWarning)
|
||||
}
|
||||
setRecordingState(.idle, defaults: store)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
FlowSessionDarwin.postTranscriptionChanged()
|
||||
}
|
||||
|
||||
/// Host app: publish pipelined ASR partial while recording or finalizing.
|
||||
public static func storeTranscriptionPartial(
|
||||
_ text: String,
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
if trimmed.isEmpty {
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionPartial)
|
||||
} else {
|
||||
store.set(trimmed, forKey: FlowSessionKeys.transcriptionPartial)
|
||||
}
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
FlowSessionDarwin.postTranscriptionChanged()
|
||||
}
|
||||
|
||||
/// Keyboard: read the latest partial without clearing it.
|
||||
public static func transcriptionPartial(defaults: UserDefaults? = nil) -> String? {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
guard let text = store.string(forKey: FlowSessionKeys.transcriptionPartial),
|
||||
!text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
public static func storeTranscriptionError(
|
||||
_ message: String,
|
||||
kind: FlowSessionKeys.TranscriptionErrorKind = .generic,
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
store.set(message, forKey: FlowSessionKeys.transcriptionError)
|
||||
store.set(kind.rawValue, forKey: FlowSessionKeys.transcriptionErrorKind)
|
||||
setRecordingState(.idle, defaults: store)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
FlowSessionDarwin.postTranscriptionChanged()
|
||||
}
|
||||
|
||||
/// Returns and clears a pending transcription result, if any.
|
||||
public static func consumeTranscriptionResult(defaults: UserDefaults? = nil) -> String? {
|
||||
consumeTranscriptionDelivery(defaults: defaults)?.text
|
||||
}
|
||||
|
||||
/// Returns and clears a pending transcription delivery (text + optional
|
||||
/// polish warning), if any.
|
||||
public static func consumeTranscriptionDelivery(
|
||||
defaults: UserDefaults? = nil
|
||||
) -> TranscriptionDelivery? {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
guard let text = store.string(forKey: FlowSessionKeys.transcriptionResult), !text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
let warning = store.string(forKey: FlowSessionKeys.transcriptionPolishWarning)
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionResult)
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionPolishWarning)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
return TranscriptionDelivery(text: text, polishWarning: warning)
|
||||
}
|
||||
|
||||
/// Returns and clears a pending transcription error, if any.
|
||||
public static func consumeTranscriptionError(defaults: UserDefaults? = nil) -> FlowTranscriptionError? {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
guard let message = store.string(forKey: FlowSessionKeys.transcriptionError), !message.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
let kindRaw = store.string(forKey: FlowSessionKeys.transcriptionErrorKind)
|
||||
let kind = FlowSessionKeys.TranscriptionErrorKind(rawValue: kindRaw ?? "") ?? .generic
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionError)
|
||||
store.removeObject(forKey: FlowSessionKeys.transcriptionErrorKind)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
return FlowTranscriptionError(message: message, kind: kind)
|
||||
}
|
||||
|
||||
public static func audioLevels(defaults: UserDefaults? = nil) -> [Float] {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
if let levels = store.array(forKey: FlowSessionKeys.audioLevels) as? [Double], !levels.isEmpty {
|
||||
return levels.map { Float($0) }
|
||||
}
|
||||
if let levels = store.array(forKey: FlowSessionKeys.audioLevels) as? [NSNumber], !levels.isEmpty {
|
||||
return levels.map { $0.floatValue }
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Host app: publish waveform bars for the keyboard (main thread only).
|
||||
public static func storeAudioLevels(
|
||||
_ levels: [Float],
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
let store = FlowSessionBridgeStorage.resolvedDefaults(defaults)
|
||||
store.set(levels.map { Double($0) }, forKey: FlowSessionKeys.audioLevels)
|
||||
FlowSessionBridgeStorage.flush(store)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user