import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { User, Zap, Play, ArrowLeft, Monitor, Users, CheckCircle2, AlertCircle, ChevronRight, Shuffle, Upload, FileSpreadsheet, Loader2, Search, Film, } from 'lucide-react'; import { PerceptionPlayer } from '@/components/modules/sentinel/PerceptionPlayer'; import { API_URL } from '@/lib/api'; import { fetchContacts } from '@/lib/crmApi'; import { cn } from '@/lib/utils'; import type { MarketingVideo } from '@/types'; import type { CrmContactListItem } from '@/types/crmTypes'; // ── Local static video catalog (served from public/videos/) ────────────────── // Used as primary source when the remote backend is unreachable. const LOCAL_VIDEOS: MarketingVideo[] = [ { id: 'eden-devprayag', title: 'Eden Devprayag — Landmark Riverfront Living', property_name: 'Eden Devprayag', unit_number: '3-4BHK', type: 'Property Walkthrough', duration_seconds: 0, video_url: '/videos/eden-devprayag.mp4', thumbnail_color: '#3b82f6', }, { id: 'sugam-prakriti', title: 'Sugam Prakriti — Nature-Inspired Urban Homes', property_name: 'Sugam Prakriti', unit_number: '2-3BHK', type: 'Property Walkthrough', duration_seconds: 0, video_url: '/videos/sugam-prakriti.mp4', thumbnail_color: '#10b981', }, { id: 'atri-aqua', title: 'Atri Aqua — Premium Waterside Residences', property_name: 'Atri Aqua', unit_number: '2-3BHK', type: 'Property Walkthrough', duration_seconds: 0, video_url: '/videos/atri-aqua.mp4', thumbnail_color: '#06b6d4', }, { id: 'atri-suryatoron', title: 'Atri Suryatoron — Sun-Drenched Elevated Living', property_name: 'Atri Suryatoron', unit_number: '2-3BHK', type: 'Property Walkthrough', duration_seconds: 0, video_url: '/videos/atri-suryatoron.mp4', thumbnail_color: '#f59e0b', }, ]; /** * Resolve the full playback URL for a video. * Local catalog videos (starting with /videos/) are served directly by Vite * from public/. Remote videos from the backend already carry a full /assets/… path * that goes through the Vite proxy. */ function resolveVideoUrl(video_url: string): string { if (video_url.startsWith('/videos/')) { // Served from public/ by Vite — no proxy prefixing needed. return video_url; } // Remote asset — prefix with API_URL so it routes through the Vite /assets proxy. return `${API_URL}${video_url}`; } type SessionMode = 'assigned' | 'auto'; type Step = 'select-video' | 'select-mode' | 'select-lead' | 'session' | 'summary'; interface SentinelClient { personId: string; leadId: string | null; legacyLiId: string | null; name: string; phone: string | null; buyerType: string | null; leadStatus: string | null; budget: string | null; interest: string | null; tags: string[]; currentQdScore: number; priorInteractionCount: number; } function apiAuthHeaders() { const token = localStorage.getItem('velocity-api-token'); const headers: Record = {}; if (token) { headers.Authorization = `Bearer ${token}`; } return headers; } function formatDuration(totalSeconds: number) { const mins = Math.floor(totalSeconds / 60); const secs = totalSeconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } function mapContactToSentinelClient(contact: CrmContactListItem): SentinelClient { const inferredTags = [ contact.buyer_type, contact.lead_status, contact.urgency, ].filter(Boolean) as string[]; return { personId: contact.person_id, leadId: contact.lead_id, legacyLiId: contact.legacy_li_id, name: contact.full_name, phone: contact.primary_phone, buyerType: contact.buyer_type, leadStatus: contact.lead_status, budget: contact.budget_band, interest: contact.primary_interest, tags: inferredTags, currentQdScore: Math.round(Math.max(contact.engagement_score, contact.intent_score, 0.5) * 100), priorInteractionCount: contact.interaction_count, }; } function VideoCard({ video, isSelected, onClick, }: { video: MarketingVideo; isSelected: boolean; onClick: () => void; }) { const videoRef = useRef(null); const hoverTimerRef = useRef | null>(null); // isPreviewing tracks whether we are actively playing a hover-preview. const [isPreviewing, setIsPreviewing] = useState(false); // srcLoaded tracks whether we have set the src attribute yet (lazy load). const [srcLoaded, setSrcLoaded] = useState(false); const [isReady, setIsReady] = useState(false); const resolvedSrc = resolveVideoUrl(video.video_url); // Stop playback and reset state. Keep src so the browser can reuse the // cached data next time the card is hovered. const stopPreview = useCallback(() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } const el = videoRef.current; setIsPreviewing(false); if (el) { el.pause(); // Snap back to a good thumbnail frame without forcing a full seek. if (isFinite(el.duration) && el.duration > 0) { el.currentTime = Math.min(5, el.duration * 0.05); } } }, []); // On mouse-enter: lazily assign src for the first time, then play after a // short delay so rapid hover-throughs don't start loading all videos at once. const startPreview = useCallback(() => { const el = videoRef.current; if (!el) return; // First hover — assign src and set preload to auto so the browser starts // buffering just this one video. if (!srcLoaded) { el.src = resolvedSrc; el.preload = 'auto'; setSrcLoaded(true); } // Small delay so a quick mouse-over doesn't trigger playback. hoverTimerRef.current = setTimeout(() => { if (!el) return; el.muted = true; el.playbackRate = 1.0; void el.play().then(() => setIsPreviewing(true)).catch(() => null); }, 350); }, [srcLoaded, resolvedSrc]); // Cleanup on unmount. useEffect(() => { return () => { if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current); const el = videoRef.current; if (el) { el.pause(); el.removeAttribute('src'); el.load(); } }; }, []); return (
{/* The