diff --git a/packages/ui/src/components/message-timeline.tsx b/packages/ui/src/components/message-timeline.tsx index 23a33d65..3f63bda2 100644 --- a/packages/ui/src/components/message-timeline.tsx +++ b/packages/ui/src/components/message-timeline.tsx @@ -239,6 +239,9 @@ const MessageTimeline: Component = (props) => { const store = () => messageStoreBus.getOrCreate(props.instanceId) const [hoveredSegment, setHoveredSegment] = createSignal(null) 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(null) let hoverTimer: number | null = null const showTools = () => props.showToolSegments ?? true @@ -261,31 +264,40 @@ const MessageTimeline: Component = (props) => { if (typeof window === "undefined") return clearHoverTimer() const target = event.currentTarget as HTMLButtonElement - hoverTimer = window.setTimeout(() => { - const rect = target.getBoundingClientRect() - const tooltipWidth = 360 - const tooltipHeight = 420 - const verticalGap = 16 - 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) - + hoverTimer = window.setTimeout(() => { + const rect = target.getBoundingClientRect() + setHoverAnchorRect({ top: rect.top, left: rect.left, width: rect.width, height: rect.height }) + setHoveredSegment(segment) + }, 200) } - + const handleMouseLeave = () => { clearHoverTimer() 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()) - + createEffect(() => { const activeId = props.activeMessageId + if (!activeId) return const targetSegment = props.segments.find((segment) => segment.messageId === activeId) if (!targetSegment) return @@ -300,8 +312,23 @@ const MessageTimeline: Component = (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 segment = hoveredSegment() if (!segment) return null const record = store().getMessage(segment.messageId) @@ -343,16 +370,23 @@ const MessageTimeline: Component = (props) => { }} - {(data) => ( -
- -
- )} + {(data) => { + onCleanup(() => setTooltipElement(null)) + return ( +
setTooltipElement(element)} + class="message-timeline-tooltip" + style={{ top: `${tooltipCoords().top}px`, left: `${tooltipCoords().left}px` }} + > + +
+ ) + }}
)