import { For, Show, onSettled } from 'solid-js' import type { UIMessage } from '@tanstack/ai-client' import { createVirtualizer } from '../../lib/create-virtualizer' import type { VirtualItem, Virtualizer } from '../../lib/create-virtualizer' import { RotateCw } from '../icons' import type { ChatUIMessage } from '../../store/chat.store' import { formatReplyTokenLine, userMessageToResendParts } from './chat-thread-utils' import { MessageParts } from './ChatMessageParts' const MESSAGE_GAP_PX = 16 const ESTIMATED_MESSAGE_HEIGHT_PX = 96 const SCROLL_END_THRESHOLD_PX = 96 type ChatThreadMessageListProps = { messages: () => ChatUIMessage[] isLoading: () => boolean lastAssistantMessageIndex: () => number sessionTotalSuffix: () => string onResendUserMessage: (message: UIMessage) => void failedUserMessage: () => UIMessage | null error: () => Error | undefined } export function ChatThreadMessageList(props: ChatThreadMessageListProps) { let scrollContainerRef: HTMLDivElement | undefined const virtualizer = createVirtualizer({ get count() { return props.messages().length }, getScrollElement: () => scrollContainerRef ?? null, estimateSize: () => ESTIMATED_MESSAGE_HEIGHT_PX, getItemKey: (index) => props.messages()[index]?.id ?? index, anchorTo: 'end', followOnAppend: true, scrollEndThreshold: SCROLL_END_THRESHOLD_PX, gap: MESSAGE_GAP_PX, overscan: 6, }) onSettled(() => { virtualizer.scrollToEnd() }) return (
{ scrollContainerRef = el }} class="min-h-0 flex-1 overflow-y-auto px-3 py-4 sm:px-6" >
{(virtualItem) => ( )}
) } function VirtualChatMessage(props: { virtualItem: VirtualItem virtualizer: Virtualizer listProps: ChatThreadMessageListProps }) { // Read from the live messages list so streaming updates stay reactive // even though virtual items themselves do not change identity. const message = () => props.listProps.messages()[props.virtualItem.index] return ( {(msg) => (
{ // Solid assigns refs before the node is connected; defer measurement. queueMicrotask(() => props.virtualizer.measureElement(el)) }} class={[ 'absolute top-0 left-0 flex w-full', { 'justify-end': msg().role === 'user', 'justify-start': msg().role !== 'user', }, ]} style={{ transform: `translateY(${props.virtualItem.start}px)`, }} >
)}
) } function MessageBubble(props: { message: ChatUIMessage }) { return (
) } function AssistantUsageLine(props: { message: ChatUIMessage isLastAssistantMessage: boolean sessionTotalSuffix: () => string }) { return ( {(usage) => (

{formatReplyTokenLine(usage)} {props.isLastAssistantMessage ? props.sessionTotalSuffix() : ''}

)}
) } function UserResendButton(props: { message: ChatUIMessage isLoading: () => boolean failedUserMessage: () => UIMessage | null onResendUserMessage: (message: UIMessage) => void }) { return ( ) } function ErrorBanner(props: { props: ChatThreadMessageListProps }) { return ( {(err) => (
{err.message} {(message) => ( )}
)}
) }