// // IntelligenceCenter.swift // Prisme // // Author: Simon-Pierre Boucher // import Foundation import FoundationModels import Observation /// Entry point of the intelligence layer for the rest of the app. /// Owns the router and the distiller; publishes digest state per tab. /// Everything degrades: on devices without Foundation Models the states /// are `.unavailable` with calm copy and the browser stays fully usable. @MainActor @Observable final class IntelligenceCenter { let router = ModelRouter() private let distiller = Distiller() private(set) var digestStates: [Tab.ID: DigestState] = [:] /// URL each digest was computed for — a navigation makes it stale. private var digestURLs: [Tab.ID: URL] = [:] func digestState(for tab: Tab) -> DigestState { digestStates[tab.id] ?? .idle } /// Digest of a page (tier `local`). User gestures preempt the /// automatic post-load enrichment. Cached content is returned without /// touching the model. func requestDigest(for tab: Tab, priority: InferencePriority = .userGesture) { switch digestState(for: tab) { case .working: return case .ready where digestURLs[tab.id] == tab.page.url: return case .idle, .failed, .unavailable, .ready: break } digestURLs[tab.id] = tab.page.url router.refreshAvailability() guard case .available = router.local else { if case .unavailable(let reason) = router.local { digestStates[tab.id] = .unavailable(reason) } return } digestStates[tab.id] = .working let queue = router.queue let distiller = distiller let page = tab.page let tabID = tab.id Task { [weak self] in do { let content = try await page.extractContent() let (digest, blocks) = try await distiller.digest( content, queue: queue, priority: priority ) self?.digestStates[tabID] = .ready(digest, blocks: blocks) } catch let error as LanguageModelSession.GenerationError { // Guardrail refusals are a normal state, not an alarm (§7). self?.digestStates[tabID] = .failed(Self.describe(error)) } catch let error as Distiller.DistillerError { self?.digestStates[tabID] = .failed( error.errorDescription ?? Self.genericFailure ) } catch { // Raw system errors never reach the user (§7); the page // stays fully readable either way. self?.digestStates[tabID] = .failed(Self.genericFailure) } } } func invalidateDigest(for tab: Tab) { digestStates[tab.id] = nil } private static let genericFailure = "Le modèle local n'a pas pu traiter cette page pour le moment. La page reste entièrement lisible." private static func describe(_ error: LanguageModelSession.GenerationError) -> String { switch error { case .guardrailViolation: "Le modèle a préféré ne pas résumer cette page. La page reste entièrement lisible." case .exceededContextWindowSize: "La page est trop longue pour le modèle local." default: "Le résumé n'a pas abouti. La page reste entièrement lisible." } } }