'use client' import * as React from 'react' import { Sparkles, AlertCircle, CheckCircle2, MinusCircle } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from '../ui/dialog' /** * Minimal subset of the API client surface this component depends on. * Lets consumers pass either the full `EnrichmentApi` from `@startsimpli/api` * or a custom adapter — keeps this component decoupled from the singleton. */ export interface ApolloEnrichmentClient { enrichApollo(contactIds: string[]): Promise } /** Mirrors `ApolloEnrichmentSummary` from @startsimpli/api. */ export interface ApolloEnrichmentSummary { total: number enriched: string[] skipped: Array<{ contact_id: string; reason: string }> errors: Array<{ contact_id: string; error: string }> missing: string[] } export interface ApolloEnrichButtonProps { /** Contact ids to enrich. Disabled when empty. */ contactIds: string[] /** API client with `enrichApollo`. Pass `api.enrichment` from @startsimpli/api. */ enrichmentApi: ApolloEnrichmentClient /** Called once the API call resolves (success or with per-entry errors). */ onComplete?: (summary: ApolloEnrichmentSummary) => void /** Called when the dialog is closed (regardless of completion). */ onClose?: () => void label?: string size?: 'sm' | 'md' className?: string disabled?: boolean } type Phase = | { kind: 'idle' } | { kind: 'running' } | { kind: 'done'; summary: ApolloEnrichmentSummary } | { kind: 'error'; message: string } /** * Trigger Apollo enrichment for a set of contacts. Opens a dialog showing * progress + the per-bucket summary (enriched / skipped / errors / missing). * * App-agnostic — works for any consumer that can supply an * `ApolloEnrichmentClient`. */ export function ApolloEnrichButton({ contactIds, enrichmentApi, onComplete, onClose, label = 'Enrich with Apollo', size = 'sm', className = '', disabled = false, }: ApolloEnrichButtonProps) { const [open, setOpen] = React.useState(false) const [phase, setPhase] = React.useState({ kind: 'idle' }) const sizeClasses = size === 'sm' ? 'px-3 py-1.5 text-xs' : 'px-4 py-2 text-sm' const iconClasses = size === 'sm' ? 'w-3 h-3' : 'w-4 h-4' const isEmpty = contactIds.length === 0 const isRunning = phase.kind === 'running' const buttonDisabled = disabled || isEmpty || isRunning const handleClick = async () => { setOpen(true) setPhase({ kind: 'running' }) try { const summary = await enrichmentApi.enrichApollo(contactIds) setPhase({ kind: 'done', summary }) onComplete?.(summary) } catch (err) { setPhase({ kind: 'error', message: err instanceof Error ? err.message : 'Enrichment failed', }) } } const handleOpenChange = (next: boolean) => { setOpen(next) if (!next) { onClose?.() // Reset phase after the dialog finishes closing so the next click // starts fresh, but keep the result visible until then. setTimeout(() => setPhase({ kind: 'idle' }), 200) } } return ( <> Apollo enrichment {phase.kind === 'running' ? `Enriching ${contactIds.length} contact${contactIds.length === 1 ? '' : 's'}…` : phase.kind === 'done' ? 'Apollo enrichment complete.' : phase.kind === 'error' ? 'Apollo enrichment failed.' : ''}
{phase.kind === 'running' && (
Reaching Apollo for {contactIds.length} contact {contactIds.length === 1 ? '' : 's'}. This may take a few seconds.
)} {phase.kind === 'error' && (
{phase.message}
)} {phase.kind === 'done' && ( )}
) } function ApolloEnrichmentSummaryView({ summary, }: { summary: ApolloEnrichmentSummary }) { const enrichedCount = summary.enriched.length const skippedCount = summary.skipped.length const errorCount = summary.errors.length const missingCount = summary.missing.length return (
} label="Enriched" value={enrichedCount} /> } label="Skipped" value={skippedCount} /> } label="Errored" value={errorCount} /> } label="Not accessible" value={missingCount} />
{summary.skipped.length > 0 && ( ({ id: s.contact_id, text: s.reason, }))} /> )} {summary.errors.length > 0 && ( ({ id: e.contact_id, text: e.error, }))} tone="error" /> )}
) } function Stat({ icon, label, value, }: { icon: React.ReactNode label: string value: number }) { return (
{icon}
{label}
{value}
) } function DetailsList({ title, items, tone = 'neutral', }: { title: string items: Array<{ id: string; text: string }> tone?: 'neutral' | 'error' }) { const toneClasses = tone === 'error' ? 'border-red-200 bg-red-50 text-red-800' : 'border-gray-200 bg-gray-50 text-gray-800' return (
{title} ({items.length})
    {items.map((item, i) => (
  • {item.id.slice(0, 8)}…{' '} {item.text}
  • ))}
) }