// 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."}
{ setKicked(false); api.generateNow().then(refresh); }}>Retry
);
}
return (
);
}
function TopBar({ greet, sub, onSettings }) {
return (
);
}
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" ? (
) : variant === "C" ? (
) : (
{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}”
}
Reflect after listening
);
}
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 (
);
}
Object.assign(window, { TodayScreen });