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) => (
)}
)}
)
}