import { i18n } from "@lingui/core"; import { Plural, Trans, useLingui } from "@lingui/react/macro"; import type { Bot, CoachToday, Group, HabitToday, RunActivityRow } from "@rakazo/contracts"; import { botColors } from "@rakazo/ui-tokens"; import { BotAvatar, Button, EvooMascot, GroupAvatar } from "@rakazo/ui-web"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CoachRhythmControl } from "../components/heute/CoachRhythmControl"; import { HeuteCoach } from "../components/heute/HeuteCoach"; import { runActivityLabel } from "../lib/agent-activity"; import type { DoingItem, DoneItem, JetztItem } from "../lib/heute-brief"; import { buildHeuteBrief, cleanBriefPreview, coachRefreshSignal, formatLocalDate, withHabitDone, } from "../lib/heute-brief"; import { localizedPreview } from "../lib/onboarding-copy"; import { rpc } from "../lib/rpc"; type HeuteBriefProps = { bots: Bot[]; groups: Group[]; onOpenBot: (botId: string, spaceId: string) => void; onOpenGroup: (groupId: string, spaceId: string) => void; onOpenRun: (run: RunActivityRow) => void; onStartChat: () => void; }; const DATE_LABEL: Intl.DateTimeFormatOptions = { weekday: "long", day: "numeric", month: "long" }; export function HeuteBrief({ bots, groups, onOpenBot, onOpenGroup, onOpenRun, onStartChat, }: HeuteBriefProps) { const [activeRuns, setActiveRuns] = useState([]); const [recentRuns, setRecentRuns] = useState([]); const [loading, setLoading] = useState(true); const [coach, setCoach] = useState(null); const [coachSettled, setCoachSettled] = useState(false); const latestCoachRequest = useRef(0); useEffect(() => { let cancelled = false; let timer: number | undefined; const tick = async () => { try { const [active, recent] = await Promise.all([ rpc.runs.list({ filter: "active" }), rpc.runs.list({ filter: "recent" }), ]); if (cancelled) return; setActiveRuns(active.runs); setRecentRuns(recent.runs); } catch { if (cancelled) return; } finally { if (!cancelled) { setLoading(false); timer = window.setTimeout(() => void tick(), 15_000); } } }; void tick(); return () => { cancelled = true; if (timer !== undefined) window.clearTimeout(timer); }; }, []); const refreshCoach = useCallback(async () => { const request = ++latestCoachRequest.current; try { const today = await rpc.coach.today(); if (request === latestCoachRequest.current) setCoach(today); } catch { // Keep the last view; the next refresh retries. } finally { setCoachSettled(true); } }, []); // `coachSignal` is the trigger: a finished run may have logged a check-in or written the brief. const coachSignal = useMemo(() => coachRefreshSignal(recentRuns), [recentRuns]); useEffect(() => { void refreshCoach(); }, [refreshCoach, coachSignal]); useEffect(() => { const onVisible = () => { if (document.visibilityState === "visible") void refreshCoach(); }; document.addEventListener("visibilitychange", onVisible); return () => { document.removeEventListener("visibilitychange", onVisible); // Ignore responses that arrive after unmount. latestCoachRequest.current += 1; }; }, [refreshCoach]); const setHabitDone = useCallback( async (habit: HabitToday, done: boolean) => { if (!coach) return; setCoach((current) => (current ? withHabitDone(current, habit.id, done) : current)); latestCoachRequest.current += 1; try { await rpc.coach.checkIns.log({ kind: "habit", habitId: habit.id, date: coach.date, done }); } finally { void refreshCoach(); } }, [coach, refreshCoach], ); const brief = useMemo( () => buildHeuteBrief({ bots, groups, activeRuns, recentRuns, }), [activeRuns, bots, groups, recentRuns], ); const botColorById = useMemo(() => new Map(bots.map((bot) => [bot.id, bot.color])), [bots]); const locale = i18n.locale || "de"; const dateLabel = coach ? formatLocalDate(coach.date, locale, DATE_LABEL) : new Date().toLocaleDateString(locale, DATE_LABEL); const hasCoachContent = Boolean( coach && (coach.brief || coach.habits.length > 0 || coach.goals.length > 0), ); const hasContent = hasCoachContent || brief.needsYou.length > 0 || brief.doing.length > 0 || brief.done.length > 0; return (

{dateLabel}

{(loading || !coachSettled) && !hasContent ? (

Loading…

) : !hasContent ? ( 0} /> ) : (
{coach ? : null} {brief.needsYou.length > 0 ? (

Needs you

{brief.needsYou.map((item) => ( ))}
{brief.actionCount > brief.needsYou.length ? (

) : null}
) : null} {brief.doing.length > 0 ? (

Doing

{brief.doing.map((item) => ( ))}
) : null} {brief.done.length > 0 ? (

Done

{brief.done.map((item) => ( ))}
) : null}
)}
); } function StatusLine({ actionCount, doingCount, loading, }: { actionCount: number; doingCount: number; loading: boolean; }) { if (loading && actionCount === 0 && doingCount === 0) { return Loading…; } if (actionCount > 0) { return ; } if (doingCount > 0) { return ; } return All quiet; } function EmptyDay({ onStartChat, hasBots }: { onStartChat: () => void; hasBots: boolean }) { return (

Quiet day so far

Start a conversation with an Oo when you are ready.

); } function ActionableChip({ label }: { label: string }) { return ( {label} ); } function NeedsYouRow({ item, botColorById, onOpenBot, onOpenGroup, onOpenRun, }: { item: JetztItem; botColorById: Map; onOpenBot: (botId: string, spaceId: string) => void; onOpenGroup: (groupId: string, spaceId: string) => void; onOpenRun: (run: RunActivityRow) => void; }) { const { t } = useLingui(); if (item.kind === "run") { const run = item.run; const title = run.groupName ? `${run.botName} · ${run.groupName}` : run.botName; const subtitle = run.promptSnippet || runActivityLabel(run.status); const label = t`${title}, ${runActivityLabel(run.status)}`; return ( ); } const { chatKind, chat } = item; const title = chat.name; const preview = cleanBriefPreview(localizedPreview(chat)) || t`Unread reply`; const label = chatKind === "bot" ? t`1:1 conversation with ${title}` : t`Group conversation ${title}`; return ( ); } function DoingRow({ item, botColorById, onOpenRun, }: { item: DoingItem; botColorById: Map; onOpenRun: (run: RunActivityRow) => void; }) { const { t } = useLingui(); const run = item.run; const title = run.groupName ? `${run.botName} · ${run.groupName}` : run.botName; const subtitle = run.promptSnippet || runActivityLabel(run.status); const label = t`${title}, ${runActivityLabel(run.status)}`; return ( ); } function DoneRow({ item, botColorById, onOpenRun, }: { item: DoneItem; botColorById: Map; onOpenRun: (run: RunActivityRow) => void; }) { const { t } = useLingui(); const run = item.run; const title = run.groupName ? `${run.botName} · ${run.groupName}` : run.botName; const subtitle = run.promptSnippet || runActivityLabel(run.status); const label = t`${title}, ${runActivityLabel(run.status)}`; return ( ); } function jetztRowKey(item: JetztItem): string { if (item.kind === "run") return `run:${item.run.runId}`; return `unread:${item.chatKind}:${item.chat.id}`; }