spb/prisme Public MIT
Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.
Swift 96.7%
JavaScript 3.3%
1//2// IntelligenceCenter.swift3// Prisme4//5// Author: Simon-Pierre Boucher <contact@spboucher.ai>6//78import Foundation9import FoundationModels10import Observation1112/// Entry point of the intelligence layer for the rest of the app.13/// Owns the router and the distiller; publishes digest state per tab.14/// Everything degrades: on devices without Foundation Models the states15/// are `.unavailable` with calm copy and the browser stays fully usable.16@MainActor17@Observable18final class IntelligenceCenter {19 let router = ModelRouter()20 private let distiller = Distiller()2122 private(set) var digestStates: [Tab.ID: DigestState] = [:]23 /// URL each digest was computed for — a navigation makes it stale.24 private var digestURLs: [Tab.ID: URL] = [:]2526 func digestState(for tab: Tab) -> DigestState {27 digestStates[tab.id] ?? .idle28 }2930 /// Digest of a page (tier `local`). User gestures preempt the31 /// automatic post-load enrichment. Cached content is returned without32 /// touching the model.33 func requestDigest(for tab: Tab, priority: InferencePriority = .userGesture) {34 switch digestState(for: tab) {35 case .working:36 return37 case .ready where digestURLs[tab.id] == tab.page.url:38 return39 case .idle, .failed, .unavailable, .ready:40 break41 }42 digestURLs[tab.id] = tab.page.url4344 router.refreshAvailability()45 guard case .available = router.local else {46 if case .unavailable(let reason) = router.local {47 digestStates[tab.id] = .unavailable(reason)48 }49 return50 }5152 digestStates[tab.id] = .working53 let queue = router.queue54 let distiller = distiller55 let page = tab.page56 let tabID = tab.id5758 Task { [weak self] in59 do {60 let content = try await page.extractContent()61 let (digest, blocks) = try await distiller.digest(62 content,63 queue: queue,64 priority: priority65 )66 self?.digestStates[tabID] = .ready(digest, blocks: blocks)67 } catch let error as LanguageModelSession.GenerationError {68 // Guardrail refusals are a normal state, not an alarm (§7).69 self?.digestStates[tabID] = .failed(Self.describe(error))70 } catch let error as Distiller.DistillerError {71 self?.digestStates[tabID] = .failed(72 error.errorDescription ?? Self.genericFailure73 )74 } catch {75 // Raw system errors never reach the user (§7); the page76 // stays fully readable either way.77 self?.digestStates[tabID] = .failed(Self.genericFailure)78 }79 }80 }8182 func invalidateDigest(for tab: Tab) {83 digestStates[tab.id] = nil84 }8586 private static let genericFailure =87 "Le modèle local n'a pas pu traiter cette page pour le moment. La page reste entièrement lisible."8889 private static func describe(_ error: LanguageModelSession.GenerationError) -> String {90 switch error {91 case .guardrailViolation:92 "Le modèle a préféré ne pas résumer cette page. La page reste entièrement lisible."93 case .exceededContextWindowSize:94 "La page est trop longue pour le modèle local."95 default:96 "Le résumé n'a pas abouti. La page reste entièrement lisible."97 }98 }99}100