import { useState, useEffect, useRef, useCallback } from 'react'
import { fetchCategories, fetchPosts, fetchStatus } from './api'
const CAT_LABELS = {
government: '\u{1F3DB}\uFE0F Government',
conglomerate: '\u{1F3E2} Media Conglomerate',
private_equity: '\u{1F4B0} Private Equity',
wealthy_private: '\u{1F464} Wealthy Private Owner',
corporate: '\u{1F4CA} Corporate',
independent: '\u2705 Independent',
}
function groupByDate(clusters) {
const getDate = (c) => c.articles[0]?.published_iso || ''
const getDisplay = (c) => c.articles[0]?.published || 'Unknown'
const map = {}
for (const c of clusters) {
const key = getDate(c)
if (!map[key]) map[key] = { display: getDisplay(c), items: [] }
map[key].items.push(c)
}
const sorted = Object.entries(map).sort((a, b) => {
if (!a[0]) return 1
if (!b[0]) return -1
return b[0].localeCompare(a[0])
})
return sorted.map(([, group]) => ({
date: group.display,
items: group.items.sort((a, b) => (b.final_score || 0) - (a.final_score || 0)),
}))
}
function SafeImage({ src, className, wrapClass }) {
const [failed, setFailed] = useState(false)
if (!src || failed) {
return
{'\u{1F4F0}'}
}
return (

setFailed(true)} />
)
}
function ScoreBar({ score }) {
const barRef = useRef(null)
const animated = useRef(false)
useEffect(() => {
if (animated.current) return
const el = barRef.current
if (!el) return
const reveal = () => {
if (animated.current) return
animated.current = true
const target = score * 100
el.style.width = '0%'
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.style.width = target + '%'
})
})
}
const parent = el.closest('.card')
if (parent && parent.classList.contains('revealed')) {
reveal()
} else {
const obs = new MutationObserver(() => {
if (parent && parent.classList.contains('revealed')) {
reveal()
obs.disconnect()
}
})
if (parent) obs.observe(parent, { attributes: true, attributeFilter: ['class'] })
return () => obs.disconnect()
}
}, [score])
return (
)
}
export default function App() {
const [categories, setCategories] = useState([])
const [allClusters, setAllClusters] = useState([])
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState(0)
const [status, setStatus] = useState('starting')
const [total, setTotal] = useState(0)
const [sponsorInfo, setSponsorInfo] = useState(null)
const tabsRef = useRef(null)
const underlineRef = useRef(null)
const observerRef = useRef(null)
useEffect(() => {
Promise.all([fetchCategories(), fetchPosts(), fetchStatus()])
.then(([catRes, postRes, statusRes]) => {
setCategories(catRes.categories)
setAllClusters(postRes.clusters)
setTotal(postRes.meta.total)
setStatus(statusRes.pipeline_status)
})
.catch(() => {})
.finally(() => setLoading(false))
}, [])
useEffect(() => {
if (!underlineRef.current || !tabsRef.current) return
const tab = tabsRef.current.children[activeTab]
if (!tab) return
underlineRef.current.style.left = tab.offsetLeft + 'px'
underlineRef.current.style.width = tab.offsetWidth + 'px'
}, [activeTab, categories])
useEffect(() => {
observerRef.current = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('revealed')
observerRef.current.unobserve(entry.target)
}
}
}, { threshold: 0.1, rootMargin: '0px 0px 40px 0px' })
return () => observerRef.current?.disconnect()
}, [])
useEffect(() => {
if (loading) return
const timer = setTimeout(() => {
const tabKey = categories[activeTab]
if (!tabKey) return
const tc = document.getElementById('tab-' + tabKey.replace(/[\s/]+/g, '_'))
if (!tc) return
tc.querySelectorAll('.card:not(.revealed)').forEach(c => {
observerRef.current?.observe(c)
})
}, 50)
return () => clearTimeout(timer)
}, [activeTab, loading, categories])
useEffect(() => {
if (loading || total === 0) return
const el = document.getElementById('count-total')
if (!el) return
let current = 0
const step = Math.max(1, Math.floor(total / 20))
const interval = setInterval(() => {
current += step
if (current >= total) {
current = total
clearInterval(interval)
}
el.textContent = current
}, 40)
return () => clearInterval(interval)
}, [loading, total])
useEffect(() => {
const handleKey = (e) => {
if (e.key === 'Escape') setSponsorInfo(null)
}
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [])
const tabId = (name) => name.replace(/[\s/]+/g, '_')
const tabKey = categories[activeTab]
const filtered = tabKey ? allClusters.filter(c => c.category === tabKey) : []
const dateGroups = groupByDate(filtered)
return (
<>
News Digest
0
{' stories across '}{categories.length}{' topics'}
{status}
{loading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : total === 0 ? (
No news yet
Run python webapp.py to fetch and analyze articles
) : (
<>
{categories.map((cat, i) => (
))}
{categories.map((cat, i) => (
{dateGroups.length === 0 ? (
No {cat} stories found
) : (
dateGroups.map(group => (
{group.date}
{group.items.map((c, idx) => {
const a = c.articles[0]
return (
#{idx + 1}
{c.topic}
{a.title}
{a.summary && (
{a.summary.length > 160 ? a.summary.slice(0, 160) + '\u2026' : a.summary}
)}
{(a.topics || []).slice(0, 3).map(t => (
{t}
))}
{a.domain}
{'\u{1F6E1}\uFE0F'} {c.avg_trustworthiness != null ? (c.avg_trustworthiness * 100).toFixed(0) + '%' : ''}
{a.article_leaning && (
{a.article_leaning}
)}
{a.source_bias && a.source_bias !== a.article_leaning && (
src:{a.source_bias}
)}
{a.source_factuality && (
{a.source_factuality}
)}
{c.total_coverage > 1 && {'\u{1F4F0}'} {c.total_coverage}}
{a.sponsor && {'\u{1F3E2}'} {a.sponsor}}
Relevance
{c.final_score}
Open
{a.published &&
{a.published}}
)
})}
))
)}
))}
>
)}
{ if (e.target === e.currentTarget) setSponsorInfo(null) }}
>
{sponsorInfo ? (
<>
{sponsorInfo.display}
Parent: {sponsorInfo.parent || '\u2014'}
{sponsorInfo.category && (
{CAT_LABELS[sponsorInfo.category] || sponsorInfo.category}
)}
{sponsorInfo.bias && (
{sponsorInfo.bias}
)}
{sponsorInfo.factuality && (
{sponsorInfo.factuality}
)}
{sponsorInfo.owners && sponsorInfo.owners.length > 0 && (
Shareholders / Funders
{sponsorInfo.owners.map(o => (
{o}
{sponsorInfo.owner_wikis && sponsorInfo.owner_wikis[o] && (
{'\u{1F4D6}'}
)}
))}
)}
{sponsorInfo.wikipedia && (
{'\u{1F4D6}'} Wikipedia
)}
>
) : (
No sponsor data available.
)}
>
)
}