106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import { Index, createEffect, createSignal, type Accessor } from "solid-js"
|
|
import VirtualItem from "./virtual-item"
|
|
import MessageBlock from "./message-block"
|
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
|
|
|
const VIRTUAL_ITEM_MARGIN_PX = 800
|
|
const ESTIMATED_MESSAGE_HEIGHT = 320
|
|
const INITIAL_FORCE_MIN_ITEMS = 12
|
|
const INITIAL_FORCE_OVERSCAN = 6
|
|
|
|
interface MessageBlockListProps {
|
|
instanceId: string
|
|
sessionId: string
|
|
store: () => InstanceMessageStore
|
|
messageIds: () => string[]
|
|
messageIndexMap: () => Map<string, number>
|
|
lastAssistantIndex: () => number
|
|
showThinking: () => boolean
|
|
thinkingDefaultExpanded: () => boolean
|
|
showUsageMetrics: () => boolean
|
|
scrollContainer: Accessor<HTMLDivElement | undefined>
|
|
loading?: boolean
|
|
onRevert?: (messageId: string) => void
|
|
onFork?: (messageId?: string) => void
|
|
onContentRendered?: () => void
|
|
setBottomSentinel: (element: HTMLDivElement | null) => void
|
|
}
|
|
|
|
export default function MessageBlockList(props: MessageBlockListProps) {
|
|
const [initialForceActive, setInitialForceActive] = createSignal(true)
|
|
const [initialForceInitialized, setInitialForceInitialized] = createSignal(false)
|
|
const [initialForceStartIndex, setInitialForceStartIndex] = createSignal(0)
|
|
const [, setInitialForceRemaining] = createSignal(0)
|
|
|
|
createEffect(() => {
|
|
props.instanceId
|
|
props.sessionId
|
|
setInitialForceActive(true)
|
|
setInitialForceInitialized(false)
|
|
setInitialForceStartIndex(0)
|
|
setInitialForceRemaining(0)
|
|
})
|
|
|
|
createEffect(() => {
|
|
if (!initialForceActive() || initialForceInitialized()) return
|
|
const ids = props.messageIds()
|
|
if (ids.length === 0) return
|
|
const viewportHeight = props.scrollContainer()?.clientHeight ?? (typeof window !== "undefined" ? window.innerHeight : 800)
|
|
const estimatedCount = Math.min(
|
|
ids.length,
|
|
Math.max(INITIAL_FORCE_MIN_ITEMS, Math.ceil(viewportHeight / ESTIMATED_MESSAGE_HEIGHT) + INITIAL_FORCE_OVERSCAN),
|
|
)
|
|
setInitialForceStartIndex(Math.max(0, ids.length - estimatedCount))
|
|
setInitialForceRemaining(estimatedCount)
|
|
setInitialForceInitialized(true)
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<Index each={props.messageIds()}>
|
|
{(messageId) => {
|
|
const messageIndex = () => props.messageIndexMap().get(messageId()) ?? 0
|
|
const forceVisible = () => initialForceActive() && messageIndex() >= initialForceStartIndex()
|
|
const handleMeasured = () => {
|
|
if (!forceVisible()) return
|
|
setInitialForceRemaining((value) => {
|
|
const next = value > 0 ? value - 1 : 0
|
|
if (next === 0) {
|
|
setInitialForceActive(false)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
return (
|
|
<VirtualItem
|
|
cacheKey={messageId()}
|
|
scrollContainer={props.scrollContainer}
|
|
threshold={VIRTUAL_ITEM_MARGIN_PX}
|
|
placeholderClass="message-stream-placeholder"
|
|
virtualizationEnabled={() => !props.loading}
|
|
forceVisible={forceVisible}
|
|
onMeasured={handleMeasured}
|
|
>
|
|
<MessageBlock
|
|
messageId={messageId()}
|
|
instanceId={props.instanceId}
|
|
sessionId={props.sessionId}
|
|
store={props.store}
|
|
messageIndexMap={props.messageIndexMap}
|
|
lastAssistantIndex={props.lastAssistantIndex}
|
|
showThinking={props.showThinking}
|
|
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
|
showUsageMetrics={props.showUsageMetrics}
|
|
onRevert={props.onRevert}
|
|
onFork={props.onFork}
|
|
onContentRendered={props.onContentRendered}
|
|
/>
|
|
</VirtualItem>
|
|
)
|
|
}}
|
|
</Index>
|
|
<div ref={props.setBottomSentinel} aria-hidden="true" />
|
|
</>
|
|
)
|
|
}
|