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>
156 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|