import { Message as VercelMessage } from 'ai/react' import { Loader2, User } from 'lucide-react' import { createContext, PropsWithChildren, ReactNode, useMemo } from 'react' import ReactMarkdown from 'react-markdown' import { Components } from 'react-markdown/lib/ast-to-react' import remarkGfm from 'remark-gfm' import { ProfileImage } from 'components/ui/ProfileImage' import { useProfile } from 'lib/profile' import { cn, markdownComponents, WarningIcon } from 'ui' import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock' import { DisplayBlockRenderer } from './DisplayBlockRenderer' import { Heading3, Hyperlink, InlineCode, ListItem, MarkdownPre, OrderedList, } from './MessageMarkdown' interface MessageContextType { isLoading: boolean readOnly?: boolean } export const MessageContext = createContext({ isLoading: false }) const baseMarkdownComponents: Partial = { ol: OrderedList, li: ListItem, h3: Heading3, code: InlineCode, a: Hyperlink, img: ({ src }) => [Image: {src}], } interface MessageProps { id: string message: VercelMessage isLoading: boolean readOnly?: boolean action?: ReactNode variant?: 'default' | 'warning' onResults: ({ messageId, resultId, results, }: { messageId: string resultId?: string results: any[] }) => void } export const Message = function Message({ id, message, isLoading, readOnly, action = null, variant = 'default', onResults, }: PropsWithChildren) { const { profile } = useProfile() const allMarkdownComponents: Partial = useMemo( () => ({ ...markdownComponents, ...baseMarkdownComponents, pre: ({ children }) => ( {children} ), }), [id, onResults] ) if (!message) { console.error(`Message component received undefined message prop for id: ${id}`) return null } const { role, content, parts } = message const isUser = role === 'user' const shouldUsePartsRendering = parts && parts.length > 0 const hasTextContent = content && content.trim().length > 0 return (
{variant === 'warning' && } {action}
{isUser && ( )}
{shouldUsePartsRendering ? ( (() => { const shownLoadingTools = new Set() return parts.map( (part: NonNullable[number], index: number) => { switch (part.type) { case 'text': return ( div]:my-4 prose-h1:text-xl prose-h1:mt-6 prose-h3:no-underline prose-h3:text-base prose-h3:mb-4 prose-strong:font-medium prose-strong:text-foreground break-words [&>p:not(:last-child)]:!mb-2 [&>*>p:first-child]:!mt-0 [&>*>p:last-child]:!mb-0 [&>*>*>p:first-child]:!mt-0 [&>*>*>p:last-child]:!mb-0 [&>ol>li]:!pl-4', isUser && 'text-foreground [&>p]:font-medium' )} remarkPlugins={[remarkGfm]} components={allMarkdownComponents} > {part.text} ) case 'tool-invocation': { const { toolCallId, toolName, args, state } = part.toolInvocation if (state === 'call' || state === 'partial-call') { if (shownLoadingTools.has(toolName)) { return null } shownLoadingTools.add(toolName) return (
{`Calling ${toolName}...`}
) } // Only render the result UI for known tools when state is 'result' switch (toolName) { case 'display_query': { return ( ) } case 'display_edge_function': { return (
) } default: // For unknown tools, just show nothing for result return null } } case 'reasoning': case 'source': case 'file': return null default: return null } } ) })() ) : hasTextContent ? ( {content} ) : ( Assistant is thinking... )}
) }