evOo-local/apps/web/src/pages/PeerMessagesOverlay.tsx
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

156 lines
5.7 KiB
TypeScript

import { Trans, useLingui } from "@lingui/react/macro";
import { ChatMarkdown } from "@rakazo/chat-ui/web";
import type { ThreadMessage } from "@rakazo/contracts";
import { BotAvatar, Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
import { useEffect, useMemo, useState } from "react";
import { loadPeerHistory } from "../lib/peer-history";
import { peerConversations } from "../lib/peer-messages";
import { rpc } from "../lib/rpc";
/**
* Full-screen view-only transcript of a bot-to-bot exchange.
* Opened from a Messaged / Message from chip in the human thread.
*/
export function PeerMessagesOverlay({
botId,
botName,
botColor,
peerBotId,
peerBotName: initialPeerBotName,
peerBotColor,
onClose,
}: {
botId: string;
botName: string;
botColor: string;
peerBotId: string;
peerBotName: string;
peerBotColor: string;
onClose: () => void;
}) {
const { t } = useLingui();
const [messages, setMessages] = useState<readonly ThreadMessage[]>([]);
const [historyReady, setHistoryReady] = useState(false);
const [historyFailed, setHistoryFailed] = useState(false);
const [reloadKey, setReloadKey] = useState(0);
const conversation = useMemo(() => {
if (!historyReady) return null;
return peerConversations(messages).find((entry) => entry.peerBotId === peerBotId) ?? null;
}, [historyReady, messages, peerBotId]);
const peerBotName = conversation?.peerBotName ?? initialPeerBotName;
useEffect(() => {
const abort = new AbortController();
setHistoryReady(false);
setHistoryFailed(false);
setMessages([]);
void loadPeerHistory({
signal: abort.signal,
loadPage: (before, signal) =>
rpc.threads.messages({ botId, before, includePeerRuns: true }, { signal }),
})
.then((loaded) => {
if (abort.signal.aborted) return;
setMessages(loaded);
setHistoryReady(true);
})
.catch(() => {
if (abort.signal.aborted) return;
setHistoryFailed(true);
setHistoryReady(true);
});
return () => {
abort.abort();
};
}, [botId, reloadKey]);
const title = `${botName} · ${peerBotName}`;
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent
data-testid="peer-conversation-view"
showCloseButton={false}
className="inset-0 top-0 left-0 flex h-full w-full max-w-none translate-x-0 translate-y-0 flex-col gap-0 rounded-none bg-background p-0 text-foreground ring-0 sm:max-w-none"
>
<div className="flex items-center justify-between gap-4 border-b border-sidebar-border px-[18px] py-3.5">
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex items-center -space-x-2">
<BotAvatar color={botColor} identity={botId} size={28} />
<BotAvatar color={peerBotColor} identity={peerBotId} size={28} />
</div>
<DialogTitle className="truncate text-ui-body font-medium text-foreground" dir="auto">
{title}
</DialogTitle>
</div>
<DialogClose aria-label={t`Close`} render={<Button variant="ghost" size="sm" />}>
<Trans>Close</Trans>
</DialogClose>
</div>
{!historyReady ? (
<div className="grid flex-1 place-items-center px-8 text-center text-ui-body text-muted-foreground/80">
<Trans>Loading…</Trans>
</div>
) : historyFailed ? (
<div className="grid flex-1 place-items-center px-8 text-center text-ui-body text-muted-foreground/80">
<div className="flex flex-col items-center gap-3">
<Trans>Could not load this chat.</Trans>
<Button
variant="outline"
size="sm"
onClick={() => setReloadKey((value) => value + 1)}
>
<Trans>Retry now</Trans>
</Button>
</div>
</div>
) : !conversation || conversation.messages.length === 0 ? (
<div className="grid flex-1 place-items-center px-8 text-center text-ui-body text-muted-foreground/80">
<Trans>No messages with {peerBotName} yet.</Trans>
</div>
) : (
<div
data-testid="peer-conversation-transcript"
className="rk-scroll flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto px-4 py-5 md:px-7 md:py-6"
>
{conversation.messages.map((peerMessage, index) => {
const sent = peerMessage.direction === "sent";
return (
<div
key={`${peerMessage.messageId}-${index}`}
className={`flex ${sent ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2.5 ${
sent ? "bg-accent" : "bg-muted"
}`}
>
<div className="mb-1 text-ui-body-sm text-muted-foreground/70" dir="auto">
{sent ? botName : peerBotName}
</div>
<div className="text-[14.5px] leading-[1.5] text-foreground/90" dir="auto">
<ChatMarkdown>{peerMessage.text}</ChatMarkdown>
</div>
</div>
</div>
);
})}
</div>
)}
<div className="flex items-center gap-4 border-t border-sidebar-border px-[18px] py-3.5">
<p className="text-ui-body text-muted-foreground/80">
<Trans>This chat is view-only</Trans>
</p>
</div>
</DialogContent>
</Dialog>
);
}