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.6 KB · 106 lines swift
Raw Blame History
1//2//  WebPageProxy.swift3//  Prisme4//5//  Author: Simon-Pierre Boucher <contact@spboucher.ai>6//78import Foundation9import Observation10import WebKit1112/// The only surface through which the rest of the app talks to a web page.13/// `WKWebView` itself never escapes `Browser/Engine` (CLAUDE.md §9): chrome14/// and tabs read observable state here and issue commands through it.15@MainActor16@Observable17final class WebPageProxy {18    /// The pooled web view currently rendering this page, if any.19    /// Set by the engine while the owning tab is on screen.20    @ObservationIgnored weak var webView: WKWebView?2122    /// Fired by the engine when a main-frame load finishes. The app layer23    /// hooks history recording here; the engine stays ignorant of it.24    @ObservationIgnored var onDidFinishLoad: ((URL) -> Void)?2526    /// Fired when the user picks "Sauver l'extrait" in the selection menu.27    @ObservationIgnored var onExcerptRequested: (() -> Void)?2829    private(set) var url: URL?30    private(set) var title: String = ""31    private(set) var estimatedProgress: Double = 032    private(set) var isLoading = false33    private(set) var canGoBack = false34    private(set) var canGoForward = false3536    // MARK: Commands3738    func load(_ url: URL) {39        webView?.load(URLRequest(url: url))40    }4142    func goBack() { webView?.goBack() }43    func goForward() { webView?.goForward() }44    func reload() { webView?.reload() }45    func stopLoading() { webView?.stopLoading() }4647    // MARK: Content extraction (Distiller steps 1–2, in-page)4849    enum PageError: LocalizedError {50        case notAttached51        case extractionFailed5253        var errorDescription: String? {54            switch self {55            case .notAttached: "L'onglet n'est pas affiché."56            case .extractionFailed: "Le contenu de la page n'a pas pu être lu."57            }58        }59    }6061    /// Runs the injected extractor and returns the typed block list.62    /// Never returns raw HTML (CLAUDE.md §4).63    func extractContent() async throws -> ExtractedContent {64        guard let webView else { throw PageError.notAttached }65        let result = try await webView.evaluateJavaScript("window.__prismeExtract(400)")66        guard let json = result as? String, let data = json.data(using: .utf8) else {67            throw PageError.extractionFailed68        }69        return try JSONDecoder().decode(ExtractedContent.self, from: data)70    }7172    /// The user's current text selection, with source anchor and section73    /// context. Nil when nothing is selected.74    func captureSelection() async throws -> SelectionExcerpt? {75        guard let webView else { throw PageError.notAttached }76        let result = try await webView.evaluateJavaScript("window.__prismeSelection()")77        guard let json = result as? String, let data = json.data(using: .utf8) else {78            return nil79        }80        return try? JSONDecoder().decode(SelectionExcerpt.self, from: data)81    }8283    /// Scrolls the page to the DOM element a digest item was derived from —84    /// the anchor that keeps generated content honest (§7).85    func scrollToBlock(domPath: String) {86        let escaped = domPath.replacingOccurrences(of: "'", with: "")87        webView?.evaluateJavaScript("window.__prismeScrollTo('\(escaped)')")88    }8990    // MARK: Engine-side state sync9192    func sync(from webView: WKWebView) {93        url = webView.url94        title = webView.title ?? ""95        estimatedProgress = webView.estimatedProgress96        isLoading = webView.isLoading97        canGoBack = webView.canGoBack98        canGoForward = webView.canGoForward99    }100101    func resetTransientState() {102        estimatedProgress = 0103        isLoading = false104    }105}106