import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import type { PersonaState } from "@/shared/ai-elements/persona"; import asset from "@/shared/ai-elements/persona-resource-manifest.json"; import { cn } from "@/shared/ui/cn"; const Persona = lazy(() => import("@/shared/ai-elements/persona").then((module) => ({ default: module.Persona })) ); const stillSources = { idle: "/agent/obsidian-water-idle.png", asleep: "/agent/obsidian-water-offline.png" }; type AgentPersonaProps = { className?: string; state?: PersonaState; }; function usePrefersReducedMotion() { const [reducedMotion, setReducedMotion] = useState( () => window.matchMedia("(prefers-reduced-motion: reduce)").matches ); useEffect(() => { const query = window.matchMedia("(prefers-reduced-motion: reduce)"); const update = () => setReducedMotion(query.matches); update(); query.addEventListener("change", update); return () => query.removeEventListener("change", update); }, []); return reducedMotion; } function useElementVisibility() { const containerRef = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { const container = containerRef.current; if (!container) return; let intersectsViewport = false; const updateVisibility = () => setVisible(intersectsViewport && document.visibilityState === "visible"); const observer = new IntersectionObserver(([entry]) => { intersectsViewport = Boolean(entry?.isIntersecting); updateVisibility(); }); observer.observe(container); document.addEventListener("visibilitychange", updateVisibility); return () => { observer.disconnect(); document.removeEventListener("visibilitychange", updateVisibility); }; }, []); return { containerRef, visible }; } function PersonaStill({ state }: { state: PersonaState }) { const source = stillSources[state === "asleep" ? "asleep" : "idle"]; return ; } function LivePersona({ state }: { state: PersonaState }) { const [loaded, setLoaded] = useState(false); const [ready, setReady] = useState(false); const [failed, setFailed] = useState(false); const onLoad = useCallback(() => setLoaded(true), []); const onLoadError = useCallback(() => setFailed(true), []); useEffect(() => { if (!loaded) return; // Keep the matching still visible while Rive completes its entrance animation. const timer = window.setTimeout(() => setReady(true), 600); return () => window.clearTimeout(timer); }, [loaded]); return ( {!failed ? ( ) : null} ); } export function AgentPersona({ className, state = "idle" }: AgentPersonaProps) { const { containerRef, visible } = useElementVisibility(); const reducedMotion = usePrefersReducedMotion(); const [hovered, setHovered] = useState(false); const live = visible && !reducedMotion; useEffect(() => { if (!live) setHovered(false); }, [live]); return ( ); }