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
+19 -48
View File
@@ -118,13 +118,13 @@ public final class KeyboardState: ObservableObject {
/// keyboard never assumes the audio-uploading engine before the App
/// Group config has been read.
@Published public var engineMode: String = "local"
/// v0.2.1 follow-up: derived translation is on iff a target
/// Derived: translation is on iff a target
/// locale has been selected (mirrors `ProviderConfig.translationEnabled`
/// so the chip / pipeline read the same source of truth).
public var translationEnabled: Bool {
translationTargetLocaleId != TranslationLanguageCatalog.offLocaleId
}
/// v0.2.1: target locale id the translate-and-polish prompt should
/// Target locale id the translate-and-polish prompt should
/// produce (e.g. `"en"`, `"ja"`). Mirrored from `ProviderConfig`.
/// Defaults to `offLocaleId` so the keyboard boots in the "off"
/// state on first install.
@@ -142,6 +142,10 @@ public final class KeyboardState: ObservableObject {
@Published public var clipboardCandidateBarEnabled: Bool = false
/// Host field is a password / secure entry never read pasteboard.
@Published public var isSecureTextEntry: Bool = false
/// Secure fields hide every clipboard-history entry point.
public var canShowClipboardEntry: Bool {
!isSecureTextEntry
}
/// Full-keyboard clipboard overlay (enable guide or history list).
@Published public var clipboardOverlay: ClipboardKeyboardOverlay = .none
/// Suggestion strip above keys (newest clipboard item).
@@ -175,7 +179,7 @@ public final class KeyboardState: ObservableObject {
@Published public var cutAvailable: Bool = false
/// Closed state machine for long-press editing of the last insertion.
@Published public var editSession: EditSessionState = .inactive
/// Temporary AI conversation UI state. The host owns the actual messages.
/// AI conversation UI state for the keyboard surface. The host owns the actual messages.
@Published public var aiSession: AISessionState = .inactive
@Published public var editCanReplaceOriginal: Bool = false
/// Short idle feedback (availability, expiry, missing LLM).
@@ -187,12 +191,18 @@ public final class KeyboardState: ObservableObject {
translationEnabled
}
/// Whether the keyboard top-bar translation chip should render.
public var isTranslationChipVisible: Bool { true }
/// Convenience shorthand used by the pipeline and views.
public var isLocalEngine: Bool { engineMode == "local" }
/// Applies the non-persistent secure-field UI policy immediately.
public func setSecureTextEntry(_ isSecure: Bool) {
isSecureTextEntry = isSecure
guard isSecure else { return }
clipboardSuggestionText = nil
clipboardSuggestionChangeCount = nil
clipboardOverlay = .none
}
// MARK: - Host-app onboarding gate
/// Mirrored from App Group / Keychain. Setup UI lives only in the host
@@ -212,47 +222,6 @@ public final class KeyboardState: ObservableObject {
}
}
// MARK: - Temporary Flow debug (remove after orange-mic investigation)
/// Mirrored from `KeyboardFlowCoordinator` for the on-screen debug panel.
@Published public var debugPendingFlowStart: Bool = false
@Published public var debugFlowRecording: Bool = false
@Published public var debugAwaitingFlowResult: Bool = false
@Published public var debugHasFullAccess: Bool = false
/// Snapshot for the keyboard debug panel.
public func makeFlowDebugRows(hasFullAccess: Bool) -> [FlowDebugRow] {
debugHasFullAccess = hasFullAccess
let micLabel: String = {
switch micVoiceAvailability {
case .ready: return "ready"
case .recording: return "recording"
case .processing: return "processing"
case .unavailable(let reason):
switch reason {
case .hostNotReady: return "unavailable(hostNotReady)"
case .preparingSession: return "unavailable(preparingSession)"
case .noFullAccess: return "unavailable(noFullAccess)"
case .appGroupUnavailable: return "unavailable(appGroupUnavailable)"
case .missingAPIKey: return "unavailable(missingAPIKey)"
case .onboardingIncomplete: return "unavailable(onboardingIncomplete)"
}
}
}()
let localRows: [FlowDebugRow] = [
FlowDebugRow("mic", micLabel),
FlowDebugRow("phase", String(describing: phase)),
FlowDebugRow("pendingStart", debugPendingFlowStart ? "1" : "0"),
FlowDebugRow("kb.recording", debugFlowRecording ? "1" : "0"),
FlowDebugRow("kb.awaiting", debugAwaitingFlowResult ? "1" : "0"),
FlowDebugRow("fullAccess", hasFullAccess ? "1" : "0"),
FlowDebugRow("micDisabled", micDisabled ? "1" : "0"),
FlowDebugRow("flowSessionPub", flowSessionActive ? "1" : "0"),
FlowDebugRow("engine", engineMode)
]
return localRows + FlowDebugAppGroupSnapshot.rows()
}
// Action hooks injected by the view controller at install time.
public var beginRecording: () -> Void = {}
public var endRecording: () -> Void = {}
@@ -268,6 +237,8 @@ public final class KeyboardState: ObservableObject {
public var tapAIMic: () -> Void = {}
public var cancelAIInput: () -> Void = {}
public var sendAIAnswer: () -> Void = {}
/// Sends a tapped idle hint card as the AI question (skip microphone).
public var submitAIHint: (AIHintCard) -> Void = { _ in }
public var openSettings: () -> Void = {}
/// Opens the host app straight to input-resource deployment. Used by the
/// typing surface when Rime resources have not been deployed yet.
@@ -291,7 +262,7 @@ public final class KeyboardState: ObservableObject {
public var setMode: (InputMode) -> Void = { _ in }
public var setLocale: (String) -> Void = { _ in }
public var setEngineMode: (String) -> Void = { _ in }
/// v0.2.1 follow-up: only the locale picker remains `enabled`
/// Only the locale picker remains; `enabled`
/// is derived from the locale id, so there's no separate toggle to
/// persist. Wired in `KeyboardViewController.installStateActions`.
public var setTranslationTargetLocaleId: (String) -> Void = { _ in }