import { ActionBarPrimitive, AuiIf, MessagePrimitive, useAuiState } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { CheckIcon, CopyIcon, Pencil } from "lucide-react"; import Image from "next/image"; import { type FC, useState } from "react"; import { currentThreadAtom } from "@/atoms/chat/current-thread.atom"; import { messageDocumentsMapAtom } from "@/atoms/chat/mentioned-documents.atom"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; interface AuthorMetadata { displayName: string | null; avatarUrl: string | null; } const UserAvatar: FC = ({ displayName, avatarUrl }) => { const [hasError, setHasError] = useState(false); const initials = displayName ? displayName .split(" ") .map((n) => n[0]) .join("") .toUpperCase() .slice(0, 2) : "U"; if (avatarUrl && !hasError) { return ( {displayName setHasError(true)} unoptimized /> ); } return (
{initials}
); }; export const UserMessage: FC = () => { const messageId = useAuiState(({ message }) => message?.id); const messageText = useAuiState(({ message }) => (message?.content ?? []) .map((part) => typeof part === "object" && part !== null && "type" in part && (part as { type?: string }).type === "text" && "text" in part ? String((part as { text?: string }).text ?? "") : "" ) .join("") ); const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom); const mentionedDocs = messageId ? messageDocumentsMap[messageId] : undefined; const metadata = useAuiState(({ message }) => message?.metadata); const author = metadata?.custom?.author as AuthorMetadata | undefined; const isSharedChat = useAtomValue(currentThreadAtom).visibility === "SEARCH_SPACE"; const showAvatar = isSharedChat && !!author; return (
{mentionedDocs && mentionedDocs.length > 0 ? ( ) : ( )}
{showAvatar && (
)}
); }; const UserMessageWithMentionChips: FC<{ text: string; mentionedDocs: { id: number; title: string; document_type: string }[]; }> = ({ text, mentionedDocs }) => { type Segment = | { type: "text"; value: string; start: number } | { type: "mention"; doc: { id: number; title: string; document_type: string }; start: number }; const tokens = mentionedDocs .map((doc) => ({ doc, token: `@${doc.title}` })) .sort((a, b) => b.token.length - a.token.length); const segments: Segment[] = []; let i = 0; let buffer = ""; let bufferStart = 0; while (i < text.length) { const tokenMatch = tokens.find(({ token }) => text.startsWith(token, i)); if (tokenMatch) { if (buffer) { segments.push({ type: "text", value: buffer, start: bufferStart }); buffer = ""; } segments.push({ type: "mention", doc: tokenMatch.doc, start: i }); i += tokenMatch.token.length; bufferStart = i; continue; } if (!buffer) bufferStart = i; buffer += text[i]; i += 1; } if (buffer) { segments.push({ type: "text", value: buffer, start: bufferStart }); } return ( {segments.map((segment) => segment.type === "text" ? ( {segment.value} ) : ( {getConnectorIcon(segment.doc.document_type ?? "UNKNOWN", "h-3 w-3")} {segment.doc.title} ) )} ); }; const UserActionBar: FC = () => { const isThreadRunning = useAuiState(({ thread }) => thread.isRunning); // Get current message ID const currentMessageId = useAuiState(({ message }) => message?.id); // Find the last user message ID in the thread (computed once, memoized by selector) const lastUserMessageId = useAuiState(({ thread }) => { const messages = thread.messages; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === "user") { return messages[i].id; } } return null; }); // Simple comparison - no iteration needed per message const isLastUserMessage = currentMessageId === lastUserMessageId; // Show edit button only on the last user message and when thread is not running const canEdit = isLastUserMessage && !isThreadRunning; return ( message.isCopied}> !message.isCopied}> {canEdit && ( )} ); };