import React, { memo, createContext, useContext, useState, useMemo, } from 'react'; import { View, Text, TouchableOpacity, Alert, Image, ViewStyle, } from 'react-native'; import { ChevronLeft, ChevronRight } from 'lucide-react-native'; import type { UIMessage } from 'ai'; import Animated from 'react-native-reanimated'; import { useUserMessageAnimation } from './userAnimation'; import { useBlankSize } from './useBlank'; import Markdown from 'react-native-markdown-display'; type MessageContextType = { role: UIMessage['role']; message?: UIMessage; }; const MessageContext = createContext(null); const useMessageContext = () => { const context = useContext(MessageContext); if (!context) { throw new Error( 'MessageToolbar and other children must be used inside ' ); } return context; }; const mergeRefs = ( ...refs: Array | null | undefined> ): React.RefCallback => { return (node: T | null) => { refs.forEach((ref) => { if (typeof ref === 'function') { ref(node); } else if (ref != null) { (ref as React.MutableRefObject).current = node; } }); }; }; export type MessageProps = { role: UIMessage['role']; children: React.ReactNode; className?: string; index: number; message: UIMessage; }; export type MessageContentProps = { children: React.ReactNode; className?: string; }; export const Message = memo( ({ role, children, className, index, message }: MessageProps) => { const isUserFirstMessage = index === 0; const { style: animationStyle, ref: animRef, onLayout: animOnLayout, } = useUserMessageAnimation({ disabled: !isUserFirstMessage }); const { ref: blankRef, onLayout: blankOnLayout } = useBlankSize({ role: 'user', disabled: !isUserFirstMessage, }); const combinedRef = useMemo( () => mergeRefs(animRef, blankRef), [animRef, blankRef] ); const contextValue = useMemo(() => ({ role, message }), [role, message]); if (role === 'user') { return ( { animOnLayout?.(event); blankOnLayout?.(event); }} style={animationStyle as ViewStyle} className={`group mt-4 flex w-full max-w-[95%] flex-col gap-2 ${className || ''}`} > {children} ); } return ( {children} ); } ); export const MessageContent = memo( ({ children, className }: MessageContentProps) => { const { role } = useMessageContext(); const roleStyles = role === 'user' ? 'self-end bg-muted max-w-[90%] px-4' : 'self-start '; return ( {children} ); } ); export const MessageResponse = memo(({ message }: { message: UIMessage }) => { const markdownRules = { text: (node, children, parent) => { return ( {node.content} ); }, ordered_list: (node, children) => { return ( {children} ); }, list_item: (node, children, parent) => { const isOrdered = parent?.type === 'ordered_list'; const index = node.index ?? 0; return ( {isOrdered ? `${index + 1}.` : '•'} {children} ); }, paragraph: (node, children) => { return ( {children} ); }, strong: (node, children) => ( {children} ), em: (node, children) => ( {children} ), fence: (node) => ( {node.content} ), code_block: (node) => ( {node.content} ), code_inline: (node) => ( {node.content} ), }; if (!message?.parts) { return {message?.content || ''}; } const hasText = message.parts.some((p) => p.type === 'text'); const hasFile = message.parts.some((p) => p.type === 'file'); if (!hasText && !hasFile) { return Thinking...; } return ( {message.parts.map((part, index) => { if (part.type === 'text') { return ( {part.text || ''} ); } if (part.type === 'file') { let uri = ''; if (part.url) uri = part.url; else if (part.data && part.mimeType) { uri = `data:${part.mimeType};base64,${part.data}`; } if (!uri) return null; return ( ); } return null; })} ); }); export type MessageToolbarProps = { children: React.ReactNode; className?: string; message?: UIMessage; }; export const MessageToolbar = memo( ({ children, className }: MessageToolbarProps) => { const { role, message } = useMessageContext(); const roleStyles = role === 'user' ? 'self-end' : 'self-start'; const hasText = message?.parts?.some( (p) => p.type === 'text' && p.text?.length > 0 ); if (!hasText) return null; if (role === 'user') return null; return ( {children} ); } ); export const MessageAction = ({ onPress, tooltip, children, }: { onPress?: () => void; tooltip?: string; children: React.ReactNode; }) => { const handlePress = () => { if (tooltip) Alert.alert(tooltip); onPress?.(); }; return ( {children} ); }; interface MessageBranchContextType { currentBranch: number; totalBranches: number; goToPrevious: () => void; goToNext: () => void; branches: React.ReactElement[]; setBranches: (branches: React.ReactElement[]) => void; } const MessageBranchContext = createContext( null ); const useMessageBranch = () => { const context = useContext(MessageBranchContext); if (!context) { throw new Error( 'MessageBranch components must be used within ' ); } return context; }; export const MessageBranch = ({ defaultBranch = 0, onBranchChange, children, className, }: { defaultBranch?: number; onBranchChange?: (index: number) => void; children: React.ReactNode; className?: string; }) => { const [currentBranch, setCurrentBranch] = useState(defaultBranch); const [branches, setBranches] = useState([]); const handleBranchChange = (newBranch: number) => { setCurrentBranch(newBranch); onBranchChange?.(newBranch); }; const goToPrevious = () => { const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1; handleBranchChange(newBranch); }; const goToNext = () => { const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0; handleBranchChange(newBranch); }; const contextValue: MessageBranchContextType = { branches, currentBranch, goToNext, goToPrevious, setBranches, totalBranches: branches.length, }; return ( {children} ); }; export const MessageBranchContent = ({ children, }: { children: React.ReactNode; }) => { const { currentBranch, setBranches } = useMessageBranch(); const childrenArray = React.Children.toArray( children ) as React.ReactElement[]; React.useEffect(() => { if (branches.length !== childrenArray.length) { setBranches(childrenArray); } }, [childrenArray, branches.length]); return childrenArray.map((branch, index) => ( {branch} )); }; export const MessageBranchSelector = ({ children, }: { children: React.ReactNode; }) => { const { totalBranches } = useMessageBranch(); if (totalBranches <= 1) return null; return {children}; }; export const MessageBranchPrevious = () => ( {}} className="h-8 w-8 items-center justify-center" > ); export const MessageBranchNext = () => ( {}} className="h-8 w-8 items-center justify-center" > ); export const MessageBranchPage = () => { const { currentBranch, totalBranches } = useMessageBranch(); return ( {currentBranch + 1} of {totalBranches} ); };