SPB Git

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%
3.5 KB · 100 lines swift
Raw Blame History
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