feat: Oracle CRM Page, Synthetic Client Data and Live Snapshot when hitting emotion hotpoint
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user