99 lines
2.8 KiB
TypeScript
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 };
|
|
}
|