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.

)}
) }