Files
OSGKeyboard/OSGKeyboardShared/Services/AIHintPool.swift
Rocky 03837b0e48
CI / Validate manifests (push) Has been cancelled
CI / SwiftLint (push) Has been cancelled
CI / iOS / Extension (push) Has been cancelled
CI / macOS (push) Has been cancelled
Refine local AI hint fallbacks
Keep clipboard actions in Skills and focus offline hints on distinct daily market briefings while preventing retired cached cards from resurfacing.
2026-08-23 20:26:34 +08:00

85 lines
3.0 KiB
Swift

// AIHintPool.swift
// OSGKeyboard · Shared
//
// Builds the idle carousel pool from local + remote cards. Clipboard
// sentence cards are excluded: the 30s copy window shows skill chips.
import Foundation
public enum AIHintPool: Sendable {
public static func activeCards(
pack: AIHintPack
) -> [AIHintCard] {
let regularCards = pack.cards
.filter { !AIHintLocalCatalog.isRetired(cardID: $0.id) }
.filter { !$0.requiresClipboard30s }
.filter { $0.metadata?.soul == nil }
.filter { !isHistoricalToday($0) }
// Clipboard-conditioned sentences no longer rotate in the carousel;
// the 30s window shows skill chips instead. Keep evergreen content
// loaded so leaving the window does not flash a leftover clipboard card.
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 }
}
/// Copy-then-30s window where clipboard skill chips replace the carousel.
public static func isClipboardSkillWindowActive(
clipboardHistoryEnabled: Bool,
newestClipboard: ClipboardHistoryEntry?,
now: Date = Date()
) -> Bool {
clipboardHistoryEnabled
&& newestClipboard.map { ClipboardHistoryPolicy.isEligibleForAIHint($0, now: now) } == true
}
/// 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
}
}