import { useEffect, useMemo, useState } from "react"; import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import Sidebar from "./components/Sidebar.jsx"; import TopBar from "./components/TopBar.jsx"; import MobileMenu from "./components/MobileMenu.jsx"; import TopicSelector from "./components/TopicSelector.jsx"; import MapView from "./pages/MapView.jsx"; import TimelineView from "./pages/TimelineView.jsx"; import ContrastingGroupsView from "./pages/ContrastingGroupsView.jsx"; import { fetchMeta } from "./api/client.js"; import { DEFAULT_NEWS_TIMEFRAME } from "./constants/timeframes.js"; const STORAGE_KEY = "FrameVis.query.v1"; const DEFAULT_QUERY = { topic: "", newsTimeframe: DEFAULT_NEWS_TIMEFRAME, }; function normalizeStoredQuery(raw) { try { const parsed = JSON.parse(raw); return { topic: typeof parsed.topic === "string" ? parsed.topic : "", newsTimeframe: parsed.newsTimeframe ?? DEFAULT_NEWS_TIMEFRAME, }; } catch { return DEFAULT_QUERY; } } export default function App() { const location = useLocation(); const [query, setQuery] = useState(() => { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return { ...DEFAULT_QUERY, ...normalizeStoredQuery(raw) }; } catch { /* ignore */ } return DEFAULT_QUERY; }); const [meta, setMeta] = useState(null); const [metaLoading, setMetaLoading] = useState(false); const [selectorOpen, setSelectorOpen] = useState(!query.topic); const [isCompact, setIsCompact] = useState(() => typeof window !== "undefined" ? window.innerWidth <= 900 : false ); const [sidebarOpen, setSidebarOpen] = useState(true); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const topicKey = query.topic?.trim() ?? ""; const newsTimeframeKey = query.newsTimeframe ?? DEFAULT_NEWS_TIMEFRAME; useEffect(() => { if (!topicKey) return; let cancelled = false; setMetaLoading(true); const persist = { topic: topicKey, newsTimeframe: newsTimeframeKey, }; fetchMeta(persist) .then((m) => !cancelled && setMeta(m)) .finally(() => !cancelled && setMetaLoading(false)); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(persist)); } catch { /* ignore quota errors */ } return () => { cancelled = true; }; }, [topicKey, newsTimeframeKey]); useEffect(() => { const onResize = () => setIsCompact(window.innerWidth <= 900); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); useEffect(() => { setMobileMenuOpen(false); }, [location.pathname]); const hasTopic = Boolean(query.topic); const routedViews = useMemo( () => ( } /> } /> } /> } /> } /> ), [query] ); return (
{ if (isCompact) { setMobileMenuOpen((v) => !v); } else { setSidebarOpen((v) => !v); } }} onEditTopic={() => setSelectorOpen(true)} />
{hasTopic ? routedViews : setSelectorOpen(true)} />}
{isCompact && ( setMobileMenuOpen(false)} /> )} {selectorOpen && ( setSelectorOpen(false) : undefined} onSubmit={(next) => { setQuery(next); setSelectorOpen(false); }} /> )}
); } function EmptyState({ onStart }) { return (

FrameVis · Intelligence Terminal

How is the world telling this story?

Compare how a single conflict, policy, or event is framed across countries, sources, and time. Choose a topic to begin.

); }