import { createContext, useContext, useCallback, useEffect, useState, useRef, type ReactNode, type RefObject, } from 'react'; // ---------- Types ---------- export interface AISection { id: string; // e.g. "project-tasks", "tasks-list", "timeline-chart" label: string; // Human-readable, e.g. "Tasks", "Project Timeline" ref: RefObject; projectId?: string; // If section is project-scoped } interface FloatingChatState { isOpen: boolean; activeSectionId: string | null; position: { x: number; y: number; width: number }; morphTargetId: string | null; projectId?: string; } interface FloatingChatContextValue { // State state: FloatingChatState; sections: Map; // Section registry registerSection: (section: AISection) => void; unregisterSection: (id: string) => void; // Actions openAtSection: (sectionId: string) => void; moveToSection: (sectionId: string) => void; close: () => void; setMorphTarget: (id: string | null) => void; } // ---------- Constants ---------- const CHAT_WIDTH = 380; const CHAT_HEIGHT = 420; const PADDING = 16; // ---------- Position computation ---------- function computeAnchorPosition( sectionRef: RefObject, ): { x: number; y: number; width: number } { const el = sectionRef.current; if (!el) return { x: PADDING, y: PADDING, width: CHAT_WIDTH }; const rect = el.getBoundingClientRect(); // Anchor to top-right of section, offset inward let x = rect.right - CHAT_WIDTH - PADDING; let y = rect.top + PADDING; // Edge-collision clamping x = Math.max(PADDING, Math.min(x, window.innerWidth - CHAT_WIDTH - PADDING)); y = Math.max( PADDING, Math.min(y, window.innerHeight - CHAT_HEIGHT - PADDING), ); return { x, y, width: CHAT_WIDTH }; } // ---------- Context ---------- const FloatingChatCtx = createContext(null); export function useFloatingChat(): FloatingChatContextValue { const ctx = useContext(FloatingChatCtx); if (!ctx) throw new Error('useFloatingChat must be used within FloatingChatProvider'); return ctx; } // Convenience hook for pages to register a section export function useAISection(section: AISection): void { const { registerSection, unregisterSection } = useFloatingChat(); useEffect(() => { registerSection(section); return () => unregisterSection(section.id); }, [section.id, registerSection, unregisterSection]); } // ---------- Provider ---------- export function FloatingChatProvider({ children }: { children: ReactNode }) { const sectionsRef = useRef>(new Map()); const [sections, setSections] = useState>(new Map()); const [state, setState] = useState({ isOpen: false, activeSectionId: null, position: { x: 0, y: 0, width: CHAT_WIDTH }, morphTargetId: null, }); const registerSection = useCallback((section: AISection) => { sectionsRef.current.set(section.id, section); setSections(new Map(sectionsRef.current)); }, []); const unregisterSection = useCallback((id: string) => { sectionsRef.current.delete(id); setSections(new Map(sectionsRef.current)); }, []); const openAtSection = useCallback((sectionId: string) => { const section = sectionsRef.current.get(sectionId); if (!section) return; const position = computeAnchorPosition(section.ref); setState({ isOpen: true, activeSectionId: sectionId, position, morphTargetId: null, projectId: section.projectId, }); }, []); const moveToSection = useCallback((sectionId: string) => { const section = sectionsRef.current.get(sectionId); if (!section) return; const position = computeAnchorPosition(section.ref); setState((prev) => ({ ...prev, activeSectionId: sectionId, position, projectId: section.projectId, })); }, []); const close = useCallback(() => { setState((prev) => ({ ...prev, isOpen: false, activeSectionId: null, morphTargetId: null, })); }, []); const setMorphTarget = useCallback((id: string | null) => { setState((prev) => ({ ...prev, morphTargetId: id })); }, []); return ( {children} ); }