feat: Oracle CRM Page, Synthetic Client Data and Live Snapshot when hitting emotion hotpoint

This commit is contained in:
Sagnik
2026-04-19 00:43:01 +05:30
parent f616a33ab0
commit 4b21c2cad6
197 changed files with 105054 additions and 89 deletions

View File

@@ -17,11 +17,12 @@ import {
Search,
Film,
} from 'lucide-react';
import { useStore } from '@/store/useStore';
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 { Lead, MarketingVideo } from '@/types';
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.
@@ -86,6 +87,21 @@ function resolveVideoUrl(video_url: string): string {
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<string, string> = {};
@@ -101,6 +117,29 @@ function formatDuration(totalSeconds: number) {
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,
@@ -259,12 +298,12 @@ function LeadRow({
isSelected,
onClick,
}: {
lead: Lead;
lead: SentinelClient;
isSelected: boolean;
onClick: () => void;
}) {
const initials = lead.name.split(' ').map((n) => n[0]).join('').slice(0, 2);
const qd = lead.quantumDynamicsScore;
const qd = lead.currentQdScore;
return (
<motion.button
@@ -323,7 +362,7 @@ function SessionSummary({
sessionMode,
onNewSession,
}: {
lead?: Lead;
lead?: SentinelClient;
finalQdScore: number;
sessionMode: SessionMode;
onNewSession: () => void;
@@ -394,15 +433,16 @@ function SessionSummary({
}
export function SentinelLiveSession() {
const { leads } = useStore();
const [step, setStep] = useState<Step>('select-video');
const [videos, setVideos] = useState<MarketingVideo[]>([]);
const [videosLoading, setVideosLoading] = useState(true);
const [videosError, setVideosError] = useState<string>('');
const [selectedVideo, setSelectedVideo] = useState<MarketingVideo | null>(null);
const [sessionMode, setSessionMode] = useState<SessionMode>('assigned');
const [selectedLead, setSelectedLead] = useState<Lead | null>(null);
const [clients, setClients] = useState<SentinelClient[]>([]);
const [clientsLoading, setClientsLoading] = useState(true);
const [clientsError, setClientsError] = useState('');
const [selectedLead, setSelectedLead] = useState<SentinelClient | null>(null);
const [leadSearch, setLeadSearch] = useState('');
const [finalQdScore, setFinalQdScore] = useState(50);
const [sceneFile, setSceneFile] = useState<File | null>(null);
@@ -451,6 +491,33 @@ export function SentinelLiveSession() {
};
}, []);
useEffect(() => {
let cancelled = false;
const loadClients = async () => {
setClientsLoading(true);
setClientsError('');
try {
const data = await fetchContacts({ limit: 200, offset: 0 });
if (!cancelled) {
setClients(data.contacts.map(mapContactToSentinelClient));
}
} catch (error) {
if (!cancelled) {
setClients([]);
setClientsError(error instanceof Error ? error.message : 'Failed to load CRM contacts.');
}
} finally {
if (!cancelled) {
setClientsLoading(false);
}
}
};
void loadClients();
return () => {
cancelled = true;
};
}, []);
const selectedVideoSceneStatus = useMemo(() => {
if (!selectedVideo) {
return 'No video selected';
@@ -467,9 +534,9 @@ export function SentinelLiveSession() {
const filteredLeads = useMemo(() => {
const query = leadSearch.trim().toLowerCase();
if (!query) {
return leads;
return clients;
}
return leads.filter((lead) => {
return clients.filter((lead) => {
const haystack = [
lead.name,
lead.phone,
@@ -482,7 +549,7 @@ export function SentinelLiveSession() {
.toLowerCase();
return haystack.includes(query);
});
}, [leadSearch, leads]);
}, [clients, leadSearch]);
const handleVideoSelect = (video: MarketingVideo) => setSelectedVideo(video);
const handleVideoConfirm = () => {
@@ -782,15 +849,24 @@ export function SentinelLiveSession() {
/>
</div>
<div className="flex flex-col gap-2 max-h-72 overflow-y-auto custom-scrollbar mb-4">
{filteredLeads.map((lead) => (
{clientsLoading ? (
<div className="rounded-xl border border-white/8 bg-white/[0.02] px-4 py-8 text-center text-sm text-zinc-500 flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Loading CRM contacts
</div>
) : clientsError ? (
<div className="rounded-xl border border-red-500/20 bg-red-500/5 px-4 py-6 text-center text-sm text-red-300">
{clientsError}
</div>
) : filteredLeads.map((lead) => (
<LeadRow
key={lead.id}
key={lead.personId}
lead={lead}
isSelected={selectedLead?.id === lead.id}
isSelected={selectedLead?.personId === lead.personId}
onClick={() => setSelectedLead(lead)}
/>
))}
{filteredLeads.length === 0 && (
{!clientsLoading && !clientsError && filteredLeads.length === 0 && (
<div className="rounded-xl border border-white/8 bg-white/[0.02] px-4 py-8 text-center text-sm text-zinc-500">
No client matches that search.
</div>
@@ -837,7 +913,14 @@ export function SentinelLiveSession() {
<PerceptionPlayer
videoUrl={resolveVideoUrl(selectedVideo.video_url)}
videoAssetId={selectedVideo.id}
leadId={sessionMode === 'assigned' ? selectedLead?.id : undefined}
personId={sessionMode === 'assigned' ? selectedLead?.personId : undefined}
canonicalLeadId={sessionMode === 'assigned' ? selectedLead?.leadId ?? undefined : undefined}
leadId={sessionMode === 'assigned' ? selectedLead?.legacyLiId ?? undefined : undefined}
leadName={sessionMode === 'assigned' ? selectedLead?.name : undefined}
leadBudget={sessionMode === 'assigned' ? selectedLead?.budget ?? undefined : undefined}
leadInterest={sessionMode === 'assigned' ? selectedLead?.interest ?? undefined : undefined}
priorInteractionCount={sessionMode === 'assigned' ? selectedLead?.priorInteractionCount : undefined}
initialQdScore={sessionMode === 'assigned' ? selectedLead?.currentQdScore : undefined}
sessionMode={sessionMode}
videoTitle={selectedVideo.title}
onSessionComplete={handleSessionComplete}