mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-18 03:00:42 +00:00
217 lines
7.9 KiB
TypeScript
217 lines
7.9 KiB
TypeScript
import { memo, type MutableRefObject, type Ref } from "react";
|
|
import { useStyletron } from "baseui";
|
|
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
|
import { Copy } from "lucide-react";
|
|
|
|
import { HistoryMinimap } from "./history-minimap";
|
|
import { SkeletonBlock, SkeletonLine } from "./skeleton";
|
|
import { SpinnerDot } from "./ui";
|
|
import { buildDisplayMessages, formatMessageDuration, formatMessageTimestamp, type AgentTab, type HistoryEvent, type Message } from "./view-model";
|
|
|
|
export const MessageList = memo(function MessageList({
|
|
tab,
|
|
scrollRef,
|
|
messageRefs,
|
|
historyEvents,
|
|
onSelectHistoryEvent,
|
|
copiedMessageId,
|
|
onCopyMessage,
|
|
thinkingTimerLabel,
|
|
}: {
|
|
tab: AgentTab | null | undefined;
|
|
scrollRef: Ref<HTMLDivElement>;
|
|
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
|
historyEvents: HistoryEvent[];
|
|
onSelectHistoryEvent: (event: HistoryEvent) => void;
|
|
copiedMessageId: string | null;
|
|
onCopyMessage: (message: Message) => void;
|
|
thinkingTimerLabel: string | null;
|
|
}) {
|
|
const [css, theme] = useStyletron();
|
|
const messages = buildDisplayMessages(tab);
|
|
|
|
return (
|
|
<>
|
|
{historyEvents.length > 0 ? <HistoryMinimap events={historyEvents} onSelect={onSelectHistoryEvent} /> : null}
|
|
<div
|
|
ref={scrollRef}
|
|
className={css({
|
|
padding: "16px 220px 16px 44px",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "12px",
|
|
flex: 1,
|
|
minHeight: 0,
|
|
overflowY: "auto",
|
|
})}
|
|
>
|
|
{tab && messages.length === 0 ? (
|
|
tab.created && tab.status === "running" ? (
|
|
/* New tab that's loading — show message skeleton */
|
|
<div
|
|
className={css({
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "12px",
|
|
flex: 1,
|
|
})}
|
|
>
|
|
<div className={css({ display: "flex", justifyContent: "flex-end" })}>
|
|
<SkeletonBlock width={200} height={44} borderRadius={16} />
|
|
</div>
|
|
<div className={css({ display: "flex", justifyContent: "flex-start" })}>
|
|
<SkeletonBlock width={280} height={64} borderRadius={16} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={css({
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
flex: 1,
|
|
minHeight: "200px",
|
|
gap: "8px",
|
|
})}
|
|
>
|
|
<LabelSmall color={theme.colors.contentTertiary}>
|
|
{!tab.created ? "Choose an agent and model, then send your first message" : "No messages yet in this session"}
|
|
</LabelSmall>
|
|
</div>
|
|
)
|
|
) : null}
|
|
{messages.map((message) => {
|
|
const isUser = message.sender === "client";
|
|
const isCopied = copiedMessageId === message.id;
|
|
const messageTimestamp = formatMessageTimestamp(message.createdAtMs);
|
|
const displayFooter = isUser
|
|
? messageTimestamp
|
|
: message.durationMs
|
|
? `${messageTimestamp} • Took ${formatMessageDuration(message.durationMs)}`
|
|
: null;
|
|
|
|
return (
|
|
<div
|
|
key={message.id}
|
|
ref={(node) => {
|
|
if (node) {
|
|
messageRefs.current.set(message.id, node);
|
|
} else {
|
|
messageRefs.current.delete(message.id);
|
|
}
|
|
}}
|
|
className={css({ display: "flex", justifyContent: isUser ? "flex-end" : "flex-start" })}
|
|
>
|
|
<div
|
|
className={css({
|
|
maxWidth: "80%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: isUser ? "flex-end" : "flex-start",
|
|
gap: "6px",
|
|
})}
|
|
>
|
|
<div
|
|
className={css({
|
|
maxWidth: "100%",
|
|
padding: "12px 16px",
|
|
borderTopLeftRadius: "16px",
|
|
borderTopRightRadius: "16px",
|
|
...(isUser
|
|
? {
|
|
backgroundColor: "#ffffff",
|
|
color: "#000000",
|
|
borderBottomLeftRadius: "16px",
|
|
borderBottomRightRadius: "4px",
|
|
}
|
|
: {
|
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
|
border: `1px solid ${theme.colors.borderOpaque}`,
|
|
color: "#e4e4e7",
|
|
borderBottomLeftRadius: "4px",
|
|
borderBottomRightRadius: "16px",
|
|
}),
|
|
})}
|
|
>
|
|
<div
|
|
data-selectable
|
|
className={css({
|
|
fontSize: "13px",
|
|
lineHeight: "1.6",
|
|
whiteSpace: "pre-wrap",
|
|
wordWrap: "break-word",
|
|
})}
|
|
>
|
|
{message.text}
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={css({
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "10px",
|
|
justifyContent: isUser ? "flex-end" : "flex-start",
|
|
minHeight: "16px",
|
|
paddingLeft: isUser ? undefined : "2px",
|
|
})}
|
|
>
|
|
{displayFooter ? (
|
|
<LabelXSmall
|
|
color={theme.colors.contentTertiary}
|
|
$style={{ fontFamily: '"IBM Plex Mono", monospace', letterSpacing: "0.01em" }}
|
|
>
|
|
{displayFooter}
|
|
</LabelXSmall>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
data-copy-action="true"
|
|
onClick={() => onCopyMessage(message)}
|
|
className={css({
|
|
all: "unset",
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: "5px",
|
|
fontSize: "11px",
|
|
cursor: "pointer",
|
|
color: isCopied ? theme.colors.contentPrimary : theme.colors.contentSecondary,
|
|
transition: "color 160ms ease",
|
|
":hover": { color: theme.colors.contentPrimary },
|
|
})}
|
|
>
|
|
<Copy size={11} />
|
|
{isCopied ? "Copied" : "Copy"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
{tab && tab.status === "running" && messages.length > 0 ? (
|
|
<div className={css({ display: "flex", alignItems: "center", gap: "8px", padding: "4px 0" })}>
|
|
<SpinnerDot size={12} />
|
|
<LabelXSmall color="#ff4f00" $style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
<span>Agent is thinking</span>
|
|
{thinkingTimerLabel ? (
|
|
<span
|
|
className={css({
|
|
padding: "2px 7px",
|
|
borderRadius: "999px",
|
|
backgroundColor: "rgba(255, 79, 0, 0.12)",
|
|
border: "1px solid rgba(255, 79, 0, 0.2)",
|
|
fontFamily: '"IBM Plex Mono", monospace',
|
|
fontSize: "10px",
|
|
letterSpacing: "0.04em",
|
|
})}
|
|
>
|
|
{thinkingTimerLabel}
|
|
</span>
|
|
) : null}
|
|
</LabelXSmall>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</>
|
|
);
|
|
});
|