9f308fadd2
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.
85 lines
3.0 KiB
Swift
85 lines
3.0 KiB
Swift
// 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
|
|
}
|
|
}
|