storytellurx-v1/app/hooks/use-observe-turn-visibility.ts

99 lines
2.8 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import {
type NarrativeMessage,
useNarrative,
} from "../game-engine/narrative-provider.tsx";
export function useObserveTurnVisibility(
scrollableContainerRef: React.RefObject<HTMLElement | null>,
) {
const { messages } = useNarrative();
const [visibleTurn, setVisibleTurn] = useState<NarrativeMessage | null>(
null,
);
const observerRef = useRef<IntersectionObserver | null>(null);
const visibleImageTurnsRef = useRef<Map<Element, NarrativeMessage>>(
new Map(),
);
useEffect(() => {
const container = scrollableContainerRef.current;
if (!container) return;
const updateActiveBackground = () => {
const visibleElementsData = Array.from(
visibleImageTurnsRef.current.entries(),
);
if (visibleElementsData.length === 0) {
setVisibleTurn(null);
return;
}
visibleElementsData.sort(
(a, b) =>
b[0].getBoundingClientRect().top - a[0].getBoundingClientRect().top,
);
const activeElementData = visibleElementsData[0]?.[1] ?? null;
setVisibleTurn(activeElementData);
};
const observerCallback: IntersectionObserverCallback = (entries) => {
let stateChanged = false;
entries.forEach((entry) => {
const element = entry.target as HTMLElement;
const turnId = element.dataset.turnId;
if (!turnId) return;
if (entry.isIntersecting) {
const turn = messages.find((t) => t.id === turnId) as
| NarrativeMessage
| undefined;
if (
turn &&
turn.role === "assistant" &&
turn.parts.some((v) =>
v.type === "tool-invocation" &&
v.toolInvocation.state === "result" &&
v.toolInvocation.result.imageUrl
)
) {
visibleImageTurnsRef.current.set(element, turn);
stateChanged = true;
}
} else {
if (visibleImageTurnsRef.current.has(element)) {
visibleImageTurnsRef.current.delete(element);
stateChanged = true;
}
}
});
if (stateChanged) {
updateActiveBackground();
}
};
observerRef.current = new IntersectionObserver(observerCallback, {
root: container,
rootMargin: "0px",
threshold: 0.1,
});
const currentObserver = observerRef.current;
const targetElements = container.querySelectorAll(
'div[data-turn-role="assistant"][data-turn-id]',
);
targetElements.forEach((el) => currentObserver.observe(el));
return () => {
currentObserver.disconnect();
visibleImageTurnsRef.current.clear();
};
}, [messages, scrollableContainerRef, setVisibleTurn]);
return { visibleTurn };
}