SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
5.2 KB · 118 lines swift
Raw Blame History
1//2//  PageContext.swift3//  Zyquo Atlas4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Normalized, model-ready representation of a web page: clean markdown, key9//  metadata, the user's current selection, and a heading index for chunking and10//  citations. Produced by ContentExtractor from the injected AtlasExtractor.js;11//  chunked by Chunker for long pages / multi-tab reasoning. This is the ONLY12//  page representation the AI layer sees — WKWebView/DOM guts never leak past it.13//1415import Foundation1617/// Extraction fidelity, so the UI can be honest when a page couldn't be read18/// cleanly (docs/AI-BROWSER-RESEARCH.md §2: extraction failure is AI failure).19enum ExtractionQuality: String, Codable {20    case reader   // Readability main-content21    case rawText  // visible structured-text fallback (app pages)22}2324struct PageHeading: Codable, Hashable {25    let level: Int26    let title: String27    /// Character offset into `markdown`.28    let offset: Int29}3031struct PageSelection: Codable, Hashable {32    let text: String33    /// Surrounding block text for grounding "explain this".34    let context: String?35}3637struct PageContext: Codable, Hashable, Identifiable {38    var id: String { canonical.isEmpty ? url : canonical }3940    let url: String41    let canonical: String42    let title: String43    let byline: String?44    let description: String?45    let siteName: String?46    let lang: String?47    let published: String?48    let favicon: String?49    /// Canonical model-facing representation.50    let markdown: String51    let quality: ExtractionQuality52    let headings: [PageHeading]53    let selection: PageSelection?54    let wordCount: Int55    let truncated: Bool5657    /// Rough token estimate (chars/4) for the stuff-vs-map-reduce budget rule.58    var estimatedTokens: Int { markdown.count / 4 }5960    private enum CodingKeys: String, CodingKey {61        case url, canonical, title, byline, description, siteName, lang62        case published, favicon, markdown, quality, headings, selection63        case wordCount, truncated64    }6566    /// Full initializer (the Decodable init handles the JS bridge separately).67    init(url: String, canonical: String, title: String, byline: String? = nil,68         description: String? = nil, siteName: String? = nil, lang: String? = nil,69         published: String? = nil, favicon: String? = nil, markdown: String,70         quality: ExtractionQuality = .reader, headings: [PageHeading] = [],71         selection: PageSelection? = nil, wordCount: Int = 0, truncated: Bool = false) {72        self.url = url; self.canonical = canonical; self.title = title; self.byline = byline73        self.description = description; self.siteName = siteName; self.lang = lang74        self.published = published; self.favicon = favicon; self.markdown = markdown75        self.quality = quality; self.headings = headings; self.selection = selection76        self.wordCount = wordCount; self.truncated = truncated77    }7879    /// Copies another context, replacing the markdown (used by the verifier to80    /// trim the sweep context so many model calls stay cheap but grounded).81    init(cloning other: PageContext, markdown: String) {82        self.init(url: other.url, canonical: other.canonical, title: other.title,83                  byline: other.byline, description: other.description, siteName: other.siteName,84                  lang: other.lang, published: other.published, favicon: other.favicon,85                  markdown: markdown, quality: other.quality, headings: other.headings,86                  selection: other.selection, wordCount: markdown.split(separator: " ").count,87                  truncated: other.truncated)88    }8990    /// A minimal baked context (verifier fallback if extraction fails entirely).91    init(bakedTitle: String, url: String, markdown: String) {92        self.init(url: url, canonical: url, title: bakedTitle, markdown: markdown,93                  quality: .reader, wordCount: markdown.split(separator: " ").count)94    }9596    // Tolerate a missing/unknown `quality` from JS by defaulting to rawText.97    init(from decoder: Decoder) throws {98        let c = try decoder.container(keyedBy: CodingKeys.self)99        url = try c.decode(String.self, forKey: .url)100        canonical = (try? c.decode(String.self, forKey: .canonical)) ?? url101        title = (try? c.decode(String.self, forKey: .title)) ?? ""102        byline = try? c.decodeIfPresent(String.self, forKey: .byline)103        description = try? c.decodeIfPresent(String.self, forKey: .description)104        siteName = try? c.decodeIfPresent(String.self, forKey: .siteName)105        lang = try? c.decodeIfPresent(String.self, forKey: .lang)106        published = try? c.decodeIfPresent(String.self, forKey: .published)107        favicon = try? c.decodeIfPresent(String.self, forKey: .favicon)108        markdown = (try? c.decode(String.self, forKey: .markdown)) ?? ""109        quality = ExtractionQuality(110            rawValue: (try? c.decode(String.self, forKey: .quality)) ?? "rawText"111        ) ?? .rawText112        headings = (try? c.decode([PageHeading].self, forKey: .headings)) ?? []113        selection = try? c.decodeIfPresent(PageSelection.self, forKey: .selection)114        wordCount = (try? c.decode(Int.self, forKey: .wordCount)) ?? 0115        truncated = (try? c.decode(Bool.self, forKey: .truncated)) ?? false116    }117}118