Add Sport/Canada/US/AI/Cloud/HomeLab pills; drop Finance
Some checks failed
CI / Build · macos-14 · Debug (push) Has been cancelled
CI / Build · macos-15 · Debug (push) Has been cancelled
CI / Build · macos-14 · Release (push) Has been cancelled
CI / Build · macos-15 · Release (push) Has been cancelled

- 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 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-06-19 17:39:45 +03:00
parent 3d40065ec7
commit 5c0f4afd80
2 changed files with 58 additions and 7 deletions

View File

@@ -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.

View File

@@ -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"),
]
}