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// 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