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,84 @@
// AIHintPool.swift
// OSGKeyboard · Shared
//
// Builds the idle carousel pool: 100% clipboard cards while eligible,
// otherwise a shuffled mix of non-clipboard local + remote cards.
import Foundation
public enum AIHintPool: Sendable {
public static func activeCards(
pack: AIHintPack,
clipboardHistoryEnabled: Bool,
newestClipboard: ClipboardHistoryEntry?,
now: Date = Date()
) -> [AIHintCard] {
let clipboardEligible = clipboardHistoryEnabled
&& newestClipboard.map { ClipboardHistoryPolicy.isEligibleForAIHint($0, now: now) } == true
let clipboardCards = pack.cards.filter(\.requiresClipboard30s)
let regularCards = pack.cards.filter { !$0.requiresClipboard30s }
.filter { !isHistoricalToday($0) }
// Within 30s: only clipboard-related sentences.
if clipboardEligible {
let pool = clipboardCards.isEmpty
? AIHintLocalCatalog.cards(locale: pack.locale).filter(\.requiresClipboard30s)
: clipboardCards
return pool.sorted { $0.priority > $1.priority }
}
// Otherwise: drop clipboard-conditioned cards entirely.
var merged = regularCards
let localRegular = AIHintLocalCatalog.cards(locale: pack.locale)
.filter { !$0.requiresClipboard30s }
for card in localRegular where !merged.contains(where: { $0.id == card.id }) {
merged.append(card)
}
return merged.sorted { $0.priority > $1.priority }
}
/// Prompt for a tapped card. Clipboard cards fail closed so an expired
/// window can never send an instruction without its material.
public static func resolvePrompt(
for card: AIHintCard,
clipboardText: String?
) -> AIClipboardPrompt.Resolution {
guard card.requiresClipboard30s else {
return .ready(AIClipboardPrompt.strippingPlaceholder(card.prompt))
}
return AIClipboardPrompt.resolve(
instruction: card.prompt,
material: clipboardText
)
}
private static func isHistoricalToday(_ card: AIHintCard) -> Bool {
let haystack = (card.displayText + " " + card.prompt)
return haystack.contains("历史上的今天") || haystack.localizedCaseInsensitiveContains("on this day")
}
}
/// Shuffle-bag rotator for the idle carousel.
public struct AIHintCarouselBag: Sendable {
private var bag: [AIHintCard] = []
private var sourceFingerprint: Int = 0
public init() {}
public mutating func next(from cards: [AIHintCard]) -> AIHintCard? {
guard !cards.isEmpty else { return nil }
let fingerprint = cards.map(\.id).joined(separator: "|").hashValue
if bag.isEmpty || fingerprint != sourceFingerprint {
sourceFingerprint = fingerprint
bag = cards.shuffled()
}
if bag.isEmpty { return nil }
return bag.removeFirst()
}
public mutating func reset() {
bag = []
sourceFingerprint = 0
}
}