"use client"; import { TranscriptionTile } from "@/transcriptions/TranscriptionTile"; import { TrackReferenceOrPlaceholder } from "@livekit/components-react"; import { useCallback, useEffect, useRef, useState } from "react"; export interface ChatOverlayProps { agentAudioTrack?: TrackReferenceOrPlaceholder; accentColor: string; inputDisabled?: boolean; isVisible: boolean; position: { x: number; y: number }; onPositionChange: (position: { x: number; y: number }) => void; containerRef: React.RefObject; onToggle: () => void; } export function ChatOverlay({ agentAudioTrack, accentColor, inputDisabled, isVisible, position, onPositionChange, containerRef, onToggle, }: ChatOverlayProps) { const overlayRef = useRef(null); const headerRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const dragOffset = useRef({ x: 0, y: 0 }); // Responsive sizing based on container size const [containerSize, setContainerSize] = useState({ width: 360, height: 500 }); useEffect(() => { const updateSize = () => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); setContainerSize({ width: rect.width, height: rect.height }); } }; updateSize(); const resizeObserver = new ResizeObserver(updateSize); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => { resizeObserver.disconnect(); }; }, [containerRef]); // Calculate overlay size as percentage of container, with min/max constraints // Width: larger (up to 95% of container) const overlayWidth = Math.min( Math.max(containerSize.width * 0.9, 280), containerSize.width * 0.95 ); // Height: smaller (reduced from 60-85% to 40-60%) const overlayHeight = Math.min( Math.max(containerSize.height * 0.4, 250), containerSize.height * 0.6 ); // Position overlay at center (slightly moved up) when first shown const hasPositionedRef = useRef(false); useEffect(() => { if (isVisible && containerRef.current && containerSize.width > 0 && overlayWidth > 0 && overlayHeight > 0) { // Calculate center position, moved up by 15% of container height const centerX = (containerSize.width - overlayWidth) / 2; const centerY = (containerSize.height - overlayHeight) / 2 - (containerSize.height * 0.15); // Only auto-position on first show (when position is at origin) if (!hasPositionedRef.current && position.x === 0 && position.y === 0) { onPositionChange({ x: Math.max(0, centerX), y: Math.max(0, centerY) }); hasPositionedRef.current = true; } } }, [isVisible, containerSize.width, containerSize.height, overlayWidth, overlayHeight, containerRef, position, onPositionChange]); const handleDragStart = (e: React.MouseEvent | React.TouchEvent) => { if (!overlayRef.current || !headerRef.current) return; // Only allow dragging from the header, but not from buttons const target = e.target as HTMLElement; if (!headerRef.current.contains(target)) return; // Don't drag if clicking on the close button if (target.closest('button') || target.closest('svg')) return; e.preventDefault(); e.stopPropagation(); setIsDragging(true); const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; dragOffset.current = { x: clientX - position.x, y: clientY - position.y, }; }; const handleDragMove = useCallback((e: MouseEvent | TouchEvent) => { if (!isDragging || !containerRef.current || !overlayRef.current) return; e.preventDefault(); e.stopPropagation(); const containerRect = containerRef.current.getBoundingClientRect(); const overlayRect = overlayRef.current.getBoundingClientRect(); const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; let newX = clientX - dragOffset.current.x; let newY = clientY - dragOffset.current.y; // Constrain within container bounds const maxX = containerRect.width - overlayRect.width; const maxY = containerRect.height - overlayRect.height; const minY = 0; // Allow dragging to top newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(minY, Math.min(newY, maxY)); onPositionChange({ x: newX, y: newY }); }, [isDragging, containerRef, overlayRef, onPositionChange]); const handleDragEnd = useCallback(() => { setIsDragging(false); }, []); useEffect(() => { if (isDragging) { window.addEventListener("mousemove", handleDragMove); window.addEventListener("mouseup", handleDragEnd); window.addEventListener("touchmove", handleDragMove, { passive: false }); window.addEventListener("touchend", handleDragEnd); } return () => { window.removeEventListener("mousemove", handleDragMove); window.removeEventListener("mouseup", handleDragEnd); window.removeEventListener("touchmove", handleDragMove); window.removeEventListener("touchend", handleDragEnd); }; }, [isDragging, handleDragMove, handleDragEnd]); return (
{/* Header with drag handle and close button */}
Chat
{/* Chat content with padding */}
); }