import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog'; import { Label } from '@documenso/ui/primitives/label'; import { Textarea } from '@documenso/ui/primitives/textarea'; import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Plural, Trans } from '@lingui/react/macro'; import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { AiApiError, type DetectFieldsProgressEvent, detectFields } from '../../../server/api/ai/detect-fields.client'; import { AnimatedDocumentScanner } from '../general/animated-document-scanner'; type DialogState = 'PROMPT' | 'PROCESSING' | 'REVIEW' | 'ERROR' | 'RATE_LIMITED'; type AiFieldDetectionDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; onComplete: (fields: NormalizedFieldWithContext[]) => void; envelopeId: string; teamId: number; }; const PROCESSING_MESSAGES = [ msg`Reading your document`, msg`Analyzing page layout`, msg`Looking for form fields`, msg`Detecting signature areas`, msg`Identifying input fields`, msg`Mapping fields to recipients`, msg`Almost done`, ] as const; const FIELD_TYPE_LABELS: Record = { SIGNATURE: msg`Signature`, INITIALS: msg`Initials`, NAME: msg`Name`, EMAIL: msg`Email`, DATE: msg`Date`, TEXT: msg`Text`, NUMBER: msg`Number`, CHECKBOX: msg`Checkbox`, RADIO: msg`Radio`, }; export const AiFieldDetectionDialog = ({ open, onOpenChange, onComplete, envelopeId, teamId, }: AiFieldDetectionDialogProps) => { const { _ } = useLingui(); const [state, setState] = useState('PROMPT'); const [messageIndex, setMessageIndex] = useState(0); const [detectedFields, setDetectedFields] = useState([]); const [error, setError] = useState(null); const [context, setContext] = useState(''); const [progress, setProgress] = useState(null); const onDetectClick = useCallback(async () => { setState('PROCESSING'); setMessageIndex(0); setError(null); setProgress(null); try { await detectFields({ request: { envelopeId, teamId, context: context || undefined, }, onProgress: (progressEvent) => { setProgress(progressEvent); }, onComplete: (event) => { setDetectedFields(event.fields); setState('REVIEW'); }, onError: (err) => { console.error('Detection failed:', err); if (err.status === 429) { setState('RATE_LIMITED'); return; } setError(err.message); setState('ERROR'); }, }); } catch (err) { console.error('Detection failed:', err); if (err instanceof AiApiError && err.status === 429) { setState('RATE_LIMITED'); return; } setError(err instanceof Error ? err.message : 'Failed to detect fields'); setState('ERROR'); } }, [envelopeId, teamId, context]); const onAddFields = () => { onComplete(detectedFields); onOpenChange(false); setState('PROMPT'); setDetectedFields([]); setContext(''); }; const onClose = () => { onOpenChange(false); setState('PROMPT'); setDetectedFields([]); setError(null); setContext(''); setProgress(null); }; // Group fields by type for summary display const fieldCountsByType = useMemo(() => { const counts: Record = {}; for (const field of detectedFields) { counts[field.type] = (counts[field.type] || 0) + 1; } return Object.entries(counts).sort(([, a], [, b]) => b - a); }, [detectedFields]); useEffect(() => { if (state !== 'PROCESSING') { return; } const interval = setInterval(() => { setMessageIndex((prev) => (prev + 1) % PROCESSING_MESSAGES.length); }, 4000); return () => clearInterval(interval); }, [state]); return ( {state === 'PROMPT' && ( <> Detect fields

We'll scan your document to find form fields like signature lines, text inputs, checkboxes, and more. Detected fields will be suggested for you to review.

Your document is processed securely using AI services that don't retain your data.