import { useLingui } from "@lingui/react/macro"; import type { Bot, CardAct, InboxItem, InboxThreadTarget } from "@rakazo/contracts"; import { resolveOoColor } from "@rakazo/core"; import { BotAvatar, parseBotAvatar } from "@rakazo/ui-web"; import { Monitor } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { inboxItemKey, inboxRefreshSignal } from "../lib/main-inbox"; import { rpc } from "../lib/rpc"; import { newClientNonce } from "../pages/shell/thread-state"; import { AskCard } from "./AskCard"; import { ThreadCard } from "./ThreadCard"; /** Catches what the bots' state does not reveal, such as a second ask with the same text. */ const INBOX_POLL_MS = 15_000; type PendingAskItem = Extract; type SuggestionItem = Extract; /** Thread procedures for an item run in the item's own space. */ function itemContext(item: InboxItem) { return { context: item.space ? { spaceId: item.space.id } : {} }; } /** * What the user's bots in all of their spaces need from them outside the main conversation, with * each bot as the sender: asks, approvals and suggestions are answered in place, takeovers and unread threads * open the bot's thread. */ export function MainInbox({ bots, onOpenBot, onOpenGroup, onOpenInSpace, }: { /** The space's bots; a change in their status, unread flag or preview refreshes the inbox. */ bots: readonly Pick[]; onOpenBot: (botId: string) => void; onOpenGroup: (groupId: string) => void; /** Opens an app path, such as a thread, in another of the user's spaces. */ onOpenInSpace: (spaceId: string, path: string) => void; }) { const { t } = useLingui(); const [items, setItems] = useState([]); const latestRequest = useRef(0); const signal = useMemo(() => inboxRefreshSignal(bots), [bots]); const refresh = useCallback(async () => { const request = ++latestRequest.current; try { const listed = await rpc.inbox.list(); if (request === latestRequest.current) setItems(listed.items); } catch { // Keep the last list; the next refresh retries. } }, []); // `signal` is the trigger: refetch whenever the bots' state changes. useEffect(() => { void refresh(); }, [refresh, signal]); useEffect(() => { const timer = window.setInterval(() => { if (document.visibilityState === "visible") void refresh(); }, INBOX_POLL_MS); return () => { window.clearInterval(timer); // Ignore responses that arrive after unmount. latestRequest.current += 1; }; }, [refresh]); const open = useCallback( ({ target, space }: { target: InboxThreadTarget; space?: InboxItem["space"] }) => { if (space) { onOpenInSpace( space.id, "botId" in target ? `/app/${target.botId}` : `/app/g/${target.groupId}`, ); } else if ("botId" in target) onOpenBot(target.botId); else onOpenGroup(target.groupId); }, [onOpenBot, onOpenGroup, onOpenInSpace], ); const answer = useCallback( async (item: PendingAskItem, text: string) => { try { // Answering reads the ask, so the bot's thread should not come back as unread with the // same question. Mark it first: the reply to this answer marks it unread again. if ("botId" in item.target) { await rpc.threads.markRead(item.target, itemContext(item)).catch(() => undefined); } await rpc.threads.answer( { ...item.target, runId: item.runId, messageId: item.messageId, answer: text }, itemContext(item), ); const key = inboxItemKey(item); setItems((current) => current.filter((entry) => inboxItemKey(entry) !== key)); } finally { void refresh(); } }, [refresh], ); const actOnSuggestion = useCallback( async (item: SuggestionItem, act: CardAct) => { const cardId = item.card.id; if (!cardId) return; try { await rpc.threads.act( { ...item.target, messageId: item.messageId, cardId, act, clientNonce: newClientNonce(), }, itemContext(item), ); } finally { void refresh(); } }, [refresh], ); if (items.length === 0) return null; return ( // Shrinks and scrolls before it can push the composer out of view.
    {items.map((item) => (
  • {item.kind === "suggestion" ? (
    actOnSuggestion(item, act)} />
    ) : item.kind === "ask" || item.kind === "approval" ? (
    answer(item, text)} className="max-w-none" />
    ) : ( )}
  • ))}
); } /** The bot, and the space it works in when that is not the current one. */ function Sender({ bot, space }: { bot: InboxItem["bot"]; space?: InboxItem["space"] }) { const color = resolveOoColor(bot.id, parseBotAvatar(bot.color).color).light; return ( {bot.name} {space ? ( {space.name} ) : null} ); }