// // IntentResolver.swift // Prisme // // Author: Simon-Pierre Boucher // import Foundation /// A search engine the user can route a query through. DuckDuckGo is the /// default (privacy thesis, CLAUDE.md §1); Google is offered as an explicit /// alternative on every query — the user always sees where a search goes. enum SearchEngine: String, CaseIterable, Codable, Sendable, Identifiable { case duckDuckGo case google var id: String { rawValue } var name: String { switch self { case .duckDuckGo: "DuckDuckGo" case .google: "Google" } } var symbol: String { switch self { case .duckDuckGo: "shield.lefthalf.filled" case .google: "g.circle.fill" } } var other: SearchEngine { self == .duckDuckGo ? .google : .duckDuckGo } func searchURL(for query: String) -> URL { let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query switch self { case .duckDuckGo: return URL(string: "https://duckduckgo.com/?q=\(encoded)")! case .google: return URL(string: "https://www.google.com/search?q=\(encoded)")! } } } /// What the user meant when typing in the address bar. /// Phase 1 distinguishes URL vs search with pure heuristics (tier `none`, /// CLAUDE.md §3) — question and command tiers arrive with the intelligence /// layer. The bar never guesses silently: proposals are shown and the user /// confirms one. enum EntryIntent: Hashable { case navigate(URL) case search(String, SearchEngine) /// User-facing label (French, per product conventions). var label: String { switch self { case .navigate(let url): "Aller sur \(url.host() ?? url.absoluteString)" case .search(let query, _): "Rechercher « \(query) »" } } /// Secondary line: where the action goes. var detail: String { switch self { case .navigate: "Adresse directe" case .search(_, let engine): engine.name } } var symbol: String { switch self { case .navigate: "globe" case .search(_, let engine): engine.symbol } } /// URL to actually load. var destination: URL { switch self { case .navigate(let url): url case .search(let query, let engine): engine.searchURL(for: query) } } } /// Pure-heuristic resolver: zero AI, zero network, deterministic. enum IntentResolver { /// Ordered proposals for the given input, most likely first. The /// preferred engine leads; the other engine is always one tap away. static func propose(for input: String, preferring engine: SearchEngine) -> [EntryIntent] { let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return [] } if let url = explicitURL(from: trimmed) ?? implicitURL(from: trimmed) { return [.navigate(url), .search(trimmed, engine), .search(trimmed, engine.other)] } return [.search(trimmed, engine), .search(trimmed, engine.other)] } /// Input carrying its own scheme, e.g. "https://example.com". private static func explicitURL(from input: String) -> URL? { guard input.lowercased().hasPrefix("http://") || input.lowercased().hasPrefix("https://"), let url = URL(string: input), url.host() != nil else { return nil } return url } /// Bare host-like input, e.g. "example.com/page" or "localhost:3000". private static func implicitURL(from input: String) -> URL? { guard !input.contains(" ") else { return nil } let hostPart = input.split(separator: "/").first.map(String.init) ?? input let looksLikeHost = hostPart.contains(".") || hostPart.hasPrefix("localhost") guard looksLikeHost, let url = URL(string: "https://\(input)"), url.host() != nil else { return nil } return url } }