/** * DuplicateWarningBanner — Animated warning card shown when a potential * duplicate ticket is detected during ticket creation. * * Features: * - Animated slide-in warning with similarity score * - Link to parent ticket * - "Subscribe to existing" vs "Create anyway" actions * - Matches the HELPDESK.AI design system (glass morphism, gradient) */ import React, { useState, useEffect } from 'react'; import { AlertTriangle, Copy, Eye, Bell, X, ChevronDown, ChevronUp, ArrowRight, Send, } from 'lucide-react'; /** * Format similarity as percentage with color. */ function formatSimilarity(score) { if (score == null) return '—'; const pct = Math.round(score * 100); if (pct >= 95) return { value: `${pct}%`, color: '#DC2626' }; if (pct >= 85) return { value: `${pct}%`, color: '#EA580C' }; if (pct >= 75) return { value: `${pct}%`, color: '#CA8A04' }; return { value: `${pct}%`, color: '#16A34A' }; } /** * DuplicateWarningBanner * * Props: * - duplicate: { is_duplicate, duplicate_ticket_id, parent_subject, similarity, candidates } * - onSubscribe: () => void — User wants to subscribe to existing ticket * - onCreateAnyway: () => void — User wants to create ticket regardless * - onDismiss: () => void — Dismiss warning */ export default function DuplicateWarningBanner({ duplicate, onSubscribe, onCreateAnyway, onDismiss, }) { const [expanded, setExpanded] = useState(false); const [visible, setVisible] = useState(false); // Animate in on mount useEffect(() => { const timer = setTimeout(() => setVisible(true), 100); return () => clearTimeout(timer); }, []); if (!duplicate || !duplicate.is_duplicate) return null; const { similarity, parent_subject, duplicate_ticket_id, candidates } = duplicate; const sim = formatSimilarity(similarity); const extraCandidates = (candidates || []).filter( c => c.id !== duplicate_ticket_id ); return (
{/* Warning Header */}
{/* Icon */}
{/* Content */}

Potential Duplicate Detected

A similar issue was recently reported by your teammate.

{onDismiss && ( )}
{/* Match Info Card */}
{/* Similarity Badge */}
= 0.85 ? '#FEF2F2' : '#FFFBEB' }} > {sim.value} Match
{/* Parent Ticket Info */}
{parent_subject && (

{parent_subject}

)}

Ticket #{((duplicate_ticket_id || '') + '').slice(0, 8).toUpperCase()}

{/* View Parent Ticket */} View
{/* Extra candidates */} {extraCandidates.length > 0 && (
{expanded && (
{extraCandidates.map((c, idx) => (
= 0.85 ? '#FEF2F2' : '#FEFCE8', color: c.similarity >= 0.85 ? '#DC2626' : '#CA8A04', }} > {Math.round(c.similarity * 100)}% {c.subject || `#${((c.id || '') + '').slice(0, 8)}`}
{c.assigned_team || ''}
))}
)}
)}
{/* Action Buttons */}
{onSubscribe && ( )} {onCreateAnyway && ( )}

AI-powered semantic match · all-MiniLM-L6-v2

); }