From 5c0f4afd807084b31222fde45652477a8d5bed4b Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Fri, 19 Jun 2026 17:39:45 +0300 Subject: [PATCH] Add Sport/Canada/US/AI/Cloud/HomeLab pills; drop Finance - New Sport pill: detect sports by clear headline terms or majority sports-outlet sources, and exclude sports (and F1) from All/Tech/regions so World Cup/soccer no longer pollutes Finance/Tech. - New client keyword pills: Canada, US, AI, Cloud, HomeLab (headline-matched). - Remove the Finance pill; keep Tech (backend topic). Co-Authored-By: Claude Opus 4.8 --- Jarvis/Views/Home/SignalFeedView.swift | 14 ++++--- Jarvis/Views/Shared/Theme.swift | 51 +++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/Jarvis/Views/Home/SignalFeedView.swift b/Jarvis/Views/Home/SignalFeedView.swift index 550a5dd..fbc223e 100644 --- a/Jarvis/Views/Home/SignalFeedView.swift +++ b/Jarvis/Views/Home/SignalFeedView.swift @@ -51,13 +51,17 @@ struct SignalFeedView: View { } else { base = [] } + // Sports (incl. F1) live only under their own pills — the backend mis-tags + // them into finance/tech/politics, so exclude them everywhere else. + let isSport: (StorySummary) -> Bool = { Interest.isFormula1($0) || Interest.isSports($0) } if let interest = selectedInterest { - if interest.id == "f1" { return base.filter(interest.matches) } - // Other pills (incl. Tech/Finance) exclude F1 (backend mis-tags it). - return base.filter { interest.matches($0) && !Interest.f1.matches($0) } + if interest.id == "f1" || interest.id == "sport" { + return base.filter(interest.matches) + } + return base.filter { interest.matches($0) && !isSport($0) } } - // "All" is news-only: F1 lives solely under its own pill. - return base.filter { !Interest.f1.matches($0) } + // "All" is news-only: no sports. + return base.filter { !isSport($0) } } /// The main feed: unread stories, with already-seen ones sunk below fresh ones. diff --git a/Jarvis/Views/Shared/Theme.swift b/Jarvis/Views/Shared/Theme.swift index bd3cf1b..f8096e6 100644 --- a/Jarvis/Views/Shared/Theme.swift +++ b/Jarvis/Views/Shared/Theme.swift @@ -190,6 +190,7 @@ struct Interest: Identifiable, Hashable { var topicSlug: String? = nil func matches(_ s: StorySummary) -> Bool { if id == "f1" { return Interest.isFormula1(s) } + if id == "sport" { return Interest.isSports(s) } if let topicSlug { return s.topic.caseInsensitiveCompare(topicSlug) == .orderedSame } @@ -217,6 +218,26 @@ struct Interest: Identifiable, Hashable { return Double(n) / Double(srcs.count) >= 0.5 } + /// Sport (non-F1) = a clear sports headline, or a majority of sports-outlet + /// sources. The backend mis-tags World Cup / soccer into finance & tech, so + /// this pulls them out. F1 has its own pill and is excluded here. + static func isSports(_ s: StorySummary) -> Bool { + if isFormula1(s) { return false } + let h = s.headline.lowercased() + let terms = ["world cup", "fifa", "premier league", "la liga", "champions league", + "uefa", "ballon d'or", "knockout stage", "quarterfinal", "semifinal", + "nba ", "nfl ", "nhl ", "mlb ", "super bowl", "playoff", "wimbledon", + "grand slam", "olympic", "test match", "six nations", "messi", "ronaldo", + "striker", "midfielder", "goalkeeper", "world cup match"] + if terms.contains(where: { h.contains($0) }) { return true } + let feeds = ["the athletic", "fox sports", "sportsnet", "espn", "sky sports", + "bbc sport", "goal", "bleacher report", "sports illustrated", "sporting news"] + let srcs = s.sources + guard !srcs.isEmpty else { return false } + let n = srcs.filter { src in feeds.contains { src.name.lowercased().contains($0) } }.count + return Double(n) / Double(srcs.count) >= 0.5 + } + // Region keywords are place/person names found in the story text — never // outlet names (which are unreliable due to backend over-clustering). private static let ugandaKeywords = ["uganda", "ugandan", "kampala", "museveni", @@ -225,15 +246,41 @@ struct Interest: Identifiable, Hashable { "cape town", "pretoria", "durban", "ramaphosa", "eskom", "gauteng", "western cape", "soweto", "drakensberg", "mbalula", "zuma"] + private static let canadaKeywords = ["canada", "canadian", "toronto", "ottawa", "vancouver", + "montreal", "quebec", "alberta", "ontario", "manitoba", + "calgary", "winnipeg", "trudeau", "carney"] + private static let usKeywords = ["united states", "u.s.", "americans", "trump", "white house", + "washington", "congress", "pentagon", "biden", "kamala", + "harris", "wall street", "new york", "california", "texas", + "florida", "supreme court", "senate", "capitol"] + // Content topics — strong, unambiguous terms (no bare "ai"/"aws" substring traps). + private static let aiKeywords = ["artificial intelligence", "machine learning", "deep learning", + "generative ai", "chatgpt", "openai", "anthropic", "claude", + "llm", "gpt", "gemini", "hugging face", "nvidia", "copilot", + "agentic", "midjourney", "a.i."] + private static let cloudKeywords = ["cloud", "azure", "google cloud", "kubernetes", "docker", + "serverless", "cloudflare", "data center", "datacenter", + "devops", "openstack", "terraform"] + private static let homelabKeywords = ["homelab", "home lab", "self-host", "self-hosted", + "selfhosted", "proxmox", "raspberry pi", "truenas", + "unraid", "synology", "jellyfin", "home server", "nas ", + "home assistant", "docker compose", "tailscale", "k3s", + "mini pc"] - static let f1 = Interest(id: "f1", label: "F1") // matching handled by isFormula1 + static let f1 = Interest(id: "f1", label: "F1") // matching via isFormula1 + static let sport = Interest(id: "sport", label: "Sport") // matching via isSports /// Pinned filter pills, in order. static let pinned: [Interest] = [ f1, + sport, Interest(id: "uganda", label: "Uganda", keywords: ugandaKeywords), Interest(id: "south-africa", label: "South Africa", keywords: southAfricaKeywords), - Interest(id: "finance", label: "Finance", topicSlug: "finance"), + Interest(id: "canada", label: "Canada", keywords: canadaKeywords), + Interest(id: "us", label: "US", keywords: usKeywords), + Interest(id: "ai", label: "AI", keywords: aiKeywords), + Interest(id: "cloud", label: "Cloud", keywords: cloudKeywords), + Interest(id: "homelab", label: "HomeLab", keywords: homelabKeywords), Interest(id: "tech", label: "Tech", topicSlug: "tech"), ] }