Spaces:
Sleeping
Sleeping
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Phone, TrendingUp, Heart, ArrowRightLeft, PhoneOff } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| import type { TimelineEvent } from "@shared/schema"; | |
| interface InteractionTimelineProps { | |
| events: TimelineEvent[]; | |
| className?: string; | |
| } | |
| const eventConfig: Record<TimelineEvent["type"], { icon: React.ReactNode; color: string; bgColor: string }> = { | |
| call_start: { | |
| icon: <Phone className="h-3 w-3" />, | |
| color: "text-emerald-600 dark:text-emerald-400", | |
| bgColor: "bg-emerald-500/10", | |
| }, | |
| upsell_attempt: { | |
| icon: <TrendingUp className="h-3 w-3" />, | |
| color: "text-amber-600 dark:text-amber-400", | |
| bgColor: "bg-amber-500/10", | |
| }, | |
| sentiment_change: { | |
| icon: <Heart className="h-3 w-3" />, | |
| color: "text-pink-600 dark:text-pink-400", | |
| bgColor: "bg-pink-500/10", | |
| }, | |
| transfer: { | |
| icon: <ArrowRightLeft className="h-3 w-3" />, | |
| color: "text-primary", | |
| bgColor: "bg-primary/10", | |
| }, | |
| call_end: { | |
| icon: <PhoneOff className="h-3 w-3" />, | |
| color: "text-muted-foreground", | |
| bgColor: "bg-muted", | |
| }, | |
| }; | |
| export function InteractionTimeline({ events, className }: InteractionTimelineProps) { | |
| const formatTime = (date: Date | string): string => { | |
| const d = new Date(date); | |
| return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); | |
| }; | |
| return ( | |
| <div className={cn("flex flex-col", className)}> | |
| <div className="flex items-center justify-between px-4 py-3 border-b border-border"> | |
| <h3 className="text-sm font-semibold">Interaction Timeline</h3> | |
| <span className="text-xs text-muted-foreground">{events.length} events</span> | |
| </div> | |
| <ScrollArea className="flex-1 px-4"> | |
| <div className="relative py-4" role="list" aria-label="Call interaction timeline"> | |
| {events.length === 0 && ( | |
| <div className="text-center py-8 text-sm text-muted-foreground"> | |
| No events recorded yet | |
| </div> | |
| )} | |
| {events.map((event, index) => { | |
| const config = eventConfig[event.type]; | |
| const isLast = index === events.length - 1; | |
| return ( | |
| <div key={event.id} className="relative flex gap-3 pb-4" role="listitem" data-testid={`timeline-event-${event.id}`}> | |
| {!isLast && ( | |
| <div | |
| className="absolute left-[11px] top-6 h-full w-px bg-border" | |
| aria-hidden="true" | |
| /> | |
| )} | |
| <div | |
| className={cn( | |
| "relative z-10 flex h-6 w-6 shrink-0 items-center justify-center rounded-full", | |
| config.bgColor, | |
| config.color | |
| )} | |
| > | |
| {config.icon} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <span className="text-sm font-medium truncate">{event.description}</span> | |
| <span className="text-xs text-muted-foreground font-mono shrink-0"> | |
| {formatTime(event.timestamp)} | |
| </span> | |
| </div> | |
| <span className="text-xs text-muted-foreground capitalize"> | |
| {event.type.replace(/_/g, " ")} | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </ScrollArea> | |
| </div> | |
| ); | |
| } | |