// Today screen — morning listen, wired to /api/today. const { useState: useState_T, useEffect: useEffect_T, useRef: useRef_T } = React; const DAY_NAMES = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]; const prettyTitle = (id) => (id || "Today").replace(/_/g, " ").toUpperCase(); function TodayScreen({ onReflect, onSettings, variant = "C", state }) { const [today, setToday] = useState_T(null); const [user, setUser] = useState_T(null); const [err, setErr] = useState_T(null); const [kicked, setKicked] = useState_T(false); const refresh = () => api.getToday().then(setToday).catch(e => setErr(e)); useEffect_T(() => { Promise.all([api.getToday(), api.getUser().catch(() => null)]) .then(([t, u]) => { setToday(t); setUser(u); }) .catch(e => { console.warn("/api/today failed:", e); setErr(e); }); }, []); // First-time flow: if onboarded but no audio yet and no brewing in flight, // kick off generation immediately (user doesn't have to wait for the cron). useEffect_T(() => { if (!today || today.audio_url || kicked) return; if (today.generation_status === "brewing") return; // already running if (today.generation_status === "skipped") return; // yesterday unlistened — respect setKicked(true); api.generateNow().then(refresh).catch(e => console.warn("generate-now failed:", e)); }, [today, kicked]); // Poll while brewing; stop the moment audio appears or status flips to failed. useEffect_T(() => { if (!today) return; if (today.audio_url) return; if (today.generation_status === "failed") return; if (today.generation_status === "skipped") return; const h = setInterval(refresh, 4000); return () => clearInterval(h); }, [today?.audio_url, today?.generation_status]); const greetPrefix = greeting(); const greetName = user?.name || ""; const daynum = user?.streak ? `Day ${user.streak}` : ""; const dayword = today?.date ? DAY_NAMES[new Date(today.date + "T00:00:00").getDay()] : ""; const subLine = [dayword, daynum].filter(Boolean).join(" · "); const effectiveState = state ?? (today === null && !err ? "loading" : today?.audio_url ? "ready" : today?.generation_status === "failed" ? "failed" : today?.generation_status === "skipped" ? "waiting-on-yesterday" : "brewing"); if (effectiveState === "loading") { return (
); } if (effectiveState === "brewing") { return (
CRAFTING YOUR
MORNING…
Three minutes of you, written for today. About a minute.
); } if (effectiveState === "waiting-on-yesterday") { return (
YESTERDAY’S MORNING
IS STILL WAITING.
We won’t stack a new one on top. Three minutes — sit with the last.
); } if (effectiveState === "failed") { return (
SOMETHING BROKE.
{today?.generation_error || "Generation failed."}
); } return ( ); } function TopBar({ greet, sub, onSettings }) { return (
{greet}
{sub}
); } function TodayReady({ onReflect, onSettings, variant, today, greet, sub }) { const audioRef = useRef_T(null); const [isPlaying, setIsPlaying] = useState_T(false); const [pos, setPos] = useState_T(0); const [total, setTotal] = useState_T(today?.duration || 0); useEffect_T(() => { const a = audioRef.current; if (!a) return; const onTime = () => setPos(a.currentTime); const onMeta = () => setTotal(a.duration || today?.duration || 0); const onEnd = () => { setIsPlaying(false); setPos(0); }; a.addEventListener("timeupdate", onTime); a.addEventListener("loadedmetadata", onMeta); a.addEventListener("ended", onEnd); return () => { a.removeEventListener("timeupdate", onTime); a.removeEventListener("loadedmetadata", onMeta); a.removeEventListener("ended", onEnd); }; }, [today?.audio_url]); const toggle = () => { const a = audioRef.current; if (!a) return; if (isPlaying) { a.pause(); setIsPlaying(false); } else { a.play().then(() => setIsPlaying(true)).catch(e => console.warn("play failed", e)); } }; const fmt = (s) => { if (!s || !isFinite(s)) return "--:--"; const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${String(m).padStart(2,"0")}:${String(r).padStart(2,"0")}`; }; const pct = total ? (pos / total) * 100 : 0; const title = prettyTitle(today?.title); const theme = today?.theme; const quote = today?.pull_quote; const Hero = variant === "B" ? (
{title}
{fmt(total)}
) : variant === "C" ? (
{theme}
{fmt(total)}
{title}
{quote}
) : (
{theme}
{title}
{fmt(total)}
); return (
{Hero}
{ const a = audioRef.current; if (!a || !total) return; const r = e.currentTarget.getBoundingClientRect(); const p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); a.currentTime = p * total; setPos(a.currentTime); }}>
{fmt(pos)} {fmt(total)}
{quote &&
“{quote}”
}
); } function greeting() { const h = new Date().getHours(); if (h < 5) return "Late night"; if (h < 12) return "Good morning"; if (h < 17) return "Afternoon"; if (h < 21) return "Good evening"; return "Good night"; } // Slow, breathing organic shape — CSS-only morph. Used while a script is being // written. No spinner, no progress bar; the point is patience, not hurry. function Blob() { return (