"use client"; import { useTranslations } from "next-intl"; import { Sparkles, AlertCircle } from "lucide-react"; import { useMemo, type ReactNode } from "react"; import ReactMarkdown, { defaultUrlTransform } from "react-markdown"; import remarkGfm from "remark-gfm"; import type { AssistantMessageInterface } from "../data/AssistantMessageInterface"; import { MessageSourcesContainer } from "./parts/MessageSourcesContainer"; /** * Render slot for `approval-request` messages. The approval card lives in the * consuming app (it depends on the app-registered AssistantAction module), so * the chat renderer receives it as a function instead of importing it. */ export type ApprovalActionRenderer = (message: AssistantMessageInterface) => ReactNode; /** * Render slot for `mention:///` links inside assistant markdown. The * consuming app owns the entity chip (avatar, hovercard, navigation), so the * chat renderer receives it as a function instead of importing it. */ export type MentionRenderer = (p: { type: string; id: string; alias: string }) => ReactNode; const MENTION_HREF = /^mention:\/\/([^/]+)\/(.+)$/; /** * `react-markdown` strips every href whose protocol is not in its safe list, * which would empty `mention://…` before the custom `a` renderer ever sees it. * Let that one scheme through and defer everything else to the default. */ function mentionUrlTransform(url: string): string { return MENTION_HREF.test(url) ? url : defaultUrlTransform(url); } interface Props { message: AssistantMessageInterface; isLatestAssistant: boolean; onSelectFollowUp: (q: string) => void; failedMessageIds?: Set; onRetry?: (tempId: string) => void; renderApprovalAction?: ApprovalActionRenderer; renderMention?: MentionRenderer; } export function MessageItem({ message, isLatestAssistant, onSelectFollowUp, failedMessageIds, onRetry, renderApprovalAction, renderMention, }: Props) { const t = useTranslations(); const isUser = message.role === "user"; const isFailed = isUser && !!failedMessageIds?.has(message.id); const markdownComponents = useMemo( () => ({ a: ({ href, children }: { href?: string; children?: ReactNode }) => { const match = href ? MENTION_HREF.exec(href) : null; if (!match || !renderMention) return {children}; return <>{renderMention({ type: match[1], id: match[2], alias: String(children ?? "") })}; }, }), [renderMention], ); if (isUser) { return (
{/* Rendered as markdown, not raw text: a message composed with mentions is stored as `[Alias](mention:///)`, which would otherwise show the link markup and a bare uuid to the person who just wrote it. `markdownComponents` routes those hrefs through `renderMention`. */} {/* Links inside this bubble sit on `bg-primary`. The mention chip and any markdown link default to `text-primary`, which is the same teal and renders invisible here, so force the on-primary colour and lean on weight for the affordance (links are never underlined in this UI). */}
{message.content}
{isFailed && (
{t("features.assistant.send_failed")}
)}
); } const isApprovalRequest = message.messageType === "approval-request" && !!renderApprovalAction; return (
{t("features.assistant.agent_name")}
{isApprovalRequest ? ( renderApprovalAction(message) ) : ( // Markdown blocks arrive unstyled under Tailwind preflight (all margins // zeroed): restore flow spacing between sibling blocks and give // blockquotes a callout treatment — assistant answers use them for // proposals and asides that must stand apart from factual prose.
{message.content}
)}
); }