Improve message timeline tooltip positioning

This commit is contained in:
Shantur Rathore
2025-12-09 17:19:55 +00:00
parent 9769d7a46e
commit d3706d2985

View File

@@ -239,6 +239,9 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
const store = () => messageStoreBus.getOrCreate(props.instanceId) const store = () => messageStoreBus.getOrCreate(props.instanceId)
const [hoveredSegment, setHoveredSegment] = createSignal<TimelineSegment | null>(null) const [hoveredSegment, setHoveredSegment] = createSignal<TimelineSegment | null>(null)
const [tooltipCoords, setTooltipCoords] = createSignal<{ top: number; left: number }>({ top: 0, left: 0 }) const [tooltipCoords, setTooltipCoords] = createSignal<{ top: number; left: number }>({ top: 0, left: 0 })
const [hoverAnchorRect, setHoverAnchorRect] = createSignal<{ top: number; left: number; width: number; height: number } | null>(null)
const [tooltipSize, setTooltipSize] = createSignal<{ width: number; height: number }>({ width: 360, height: 420 })
const [tooltipElement, setTooltipElement] = createSignal<HTMLDivElement | null>(null)
let hoverTimer: number | null = null let hoverTimer: number | null = null
const showTools = () => props.showToolSegments ?? true const showTools = () => props.showToolSegments ?? true
@@ -261,31 +264,40 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
if (typeof window === "undefined") return if (typeof window === "undefined") return
clearHoverTimer() clearHoverTimer()
const target = event.currentTarget as HTMLButtonElement const target = event.currentTarget as HTMLButtonElement
hoverTimer = window.setTimeout(() => { hoverTimer = window.setTimeout(() => {
const rect = target.getBoundingClientRect() const rect = target.getBoundingClientRect()
const tooltipWidth = 360 setHoverAnchorRect({ top: rect.top, left: rect.left, width: rect.width, height: rect.height })
const tooltipHeight = 420 setHoveredSegment(segment)
const verticalGap = 16 }, 200)
const horizontalGap = 16
const preferredTop = rect.top + rect.height / 2 - tooltipHeight / 2
const clampedTop = Math.min(window.innerHeight - tooltipHeight - verticalGap, Math.max(verticalGap, preferredTop))
const preferredLeft = rect.left - tooltipWidth - horizontalGap
const clampedLeft = Math.max(horizontalGap, preferredLeft)
setTooltipCoords({ top: clampedTop, left: clampedLeft })
setHoveredSegment(segment)
}, 200)
} }
const handleMouseLeave = () => { const handleMouseLeave = () => {
clearHoverTimer() clearHoverTimer()
setHoveredSegment(null) setHoveredSegment(null)
setHoverAnchorRect(null)
} }
createEffect(() => {
if (typeof window === "undefined") return
const anchor = hoverAnchorRect()
const segment = hoveredSegment()
if (!anchor || !segment) return
const { width, height } = tooltipSize()
const verticalGap = 16
const horizontalGap = 16
const preferredTop = anchor.top + anchor.height / 2 - height / 2
const maxTop = window.innerHeight - height - verticalGap
const clampedTop = Math.min(maxTop, Math.max(verticalGap, preferredTop))
const preferredLeft = anchor.left - width - horizontalGap
const clampedLeft = Math.max(horizontalGap, preferredLeft)
setTooltipCoords({ top: clampedTop, left: clampedLeft })
})
onCleanup(() => clearHoverTimer()) onCleanup(() => clearHoverTimer())
createEffect(() => { createEffect(() => {
const activeId = props.activeMessageId const activeId = props.activeMessageId
if (!activeId) return if (!activeId) return
const targetSegment = props.segments.find((segment) => segment.messageId === activeId) const targetSegment = props.segments.find((segment) => segment.messageId === activeId)
if (!targetSegment) return if (!targetSegment) return
@@ -300,8 +312,23 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
} }
}) })
}) })
createEffect(() => {
const element = tooltipElement()
if (!element || typeof window === "undefined") return
const updateSize = () => {
const rect = element.getBoundingClientRect()
setTooltipSize({ width: rect.width, height: rect.height })
}
updateSize()
if (typeof ResizeObserver === "undefined") return
const observer = new ResizeObserver(() => updateSize())
observer.observe(element)
onCleanup(() => observer.disconnect())
})
const previewData = createMemo(() => { const previewData = createMemo(() => {
const segment = hoveredSegment() const segment = hoveredSegment()
if (!segment) return null if (!segment) return null
const record = store().getMessage(segment.messageId) const record = store().getMessage(segment.messageId)
@@ -343,16 +370,23 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
}} }}
</For> </For>
<Show when={previewData()}> <Show when={previewData()}>
{(data) => ( {(data) => {
<div class="message-timeline-tooltip" style={{ top: `${tooltipCoords().top}px`, left: `${tooltipCoords().left}px` }}> onCleanup(() => setTooltipElement(null))
<MessagePreview return (
messageId={data().messageId} <div
instanceId={props.instanceId} ref={(element) => setTooltipElement(element)}
sessionId={props.sessionId} class="message-timeline-tooltip"
store={store} style={{ top: `${tooltipCoords().top}px`, left: `${tooltipCoords().left}px` }}
/> >
</div> <MessagePreview
)} messageId={data().messageId}
instanceId={props.instanceId}
sessionId={props.sessionId}
store={store}
/>
</div>
)
}}
</Show> </Show>
</div> </div>
) )