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:
Rocky
2026-08-13 01:00:51 +08:00
parent fd6e0d3e7e
commit 9f308fadd2
202 changed files with 10897 additions and 5962 deletions
@@ -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)
}
}