evOo-local/apps/web/src/pages/PeerMessagesOverlay.tsx
cynarAI 738811dbe7
Read the web app at 16px base size on phones (#155)
Text sizes now follow a --font-scale variable that is 1 on wider screens and
8/7 below 768px, so the 14px base text reads at 16px on phones (Safari stops
zooming into fields at that size) and every other size grows by the same
ratio. One-off pixel sizes in the shell move onto the shared text tokens so
they scale too. The stage subtitle now truncates inside the screen instead
of spilling past both edges.


Claude-Session: https://claude.ai/code/session_01PFEDgbyJm12ExL3Kn4Dk8B

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 12:42:30 +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-ui-body-sm 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>
);
}