spb/zyquo-atlas Public License
The AI-native macOS web browser — every surface, intelligent.
Swift 75.2%
JavaScript 22%
Shell 2%
Makefile 0.9%
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