chore: improve detection prompts

This commit is contained in:
Ephraim Atta-Duncan
2025-11-18 21:53:36 +00:00
parent c8e254aff1
commit 548a74ab89
6 changed files with 102 additions and 66 deletions

View File

@ -69,3 +69,4 @@
--font-noto: 'Noto Sans', 'Noto Sans Korean', 'Noto Sans Japanese', 'Noto Sans Chinese'; --font-noto: 'Noto Sans', 'Noto Sans Korean', 'Noto Sans Japanese', 'Noto Sans Chinese';
} }
} }

View File

@ -1,19 +1,19 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { LoaderIcon } from 'lucide-react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, AlertDialog,
DialogContent, AlertDialogAction,
DialogDescription, AlertDialogCancel,
DialogFooter, AlertDialogContent,
DialogHeader, AlertDialogDescription,
DialogTitle, AlertDialogFooter,
} from '@documenso/ui/primitives/dialog'; AlertDialogHeader,
AlertDialogTitle,
} from '@documenso/ui/primitives/alert-dialog';
type RecipientDetectionStep = 'PROMPT' | 'PROCESSING'; type RecipientDetectionStep = 'PROMPT' | 'PROCESSING';
@ -32,14 +32,14 @@ export const RecipientDetectionPromptDialog = ({
}: RecipientDetectionPromptDialogProps) => { }: RecipientDetectionPromptDialogProps) => {
const [currentStep, setCurrentStep] = useState<RecipientDetectionStep>('PROMPT'); const [currentStep, setCurrentStep] = useState<RecipientDetectionStep>('PROMPT');
// Reset to first step when dialog closes
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setCurrentStep('PROMPT'); setCurrentStep('PROMPT');
} }
}, [open]); }, [open]);
const handleStartDetection = () => { const handleStartDetection = (e: React.MouseEvent) => {
e.preventDefault();
setCurrentStep('PROCESSING'); setCurrentStep('PROCESSING');
Promise.resolve(onAccept()).catch(() => { Promise.resolve(onAccept()).catch(() => {
@ -52,54 +52,75 @@ export const RecipientDetectionPromptDialog = ({
}; };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <AlertDialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <AlertDialogContent>
<fieldset disabled={currentStep === 'PROCESSING'}> <fieldset disabled={currentStep === 'PROCESSING'}>
<AnimateGenericFadeInOut motionKey={currentStep}> <AnimateGenericFadeInOut motionKey={currentStep} className="grid gap-4">
{match(currentStep) {match(currentStep)
.with('PROMPT', () => ( .with('PROMPT', () => (
<> <>
<DialogHeader> <AlertDialogHeader>
<DialogTitle className="flex items-center gap-2"> <AlertDialogTitle>
<Trans>Auto-detect recipients?</Trans> <Trans>Auto-detect recipients?</Trans>
</DialogTitle> </AlertDialogTitle>
<DialogDescription> <AlertDialogDescription>
<Trans> <Trans>
Would you like to automatically detect recipients in your document? This can Would you like to automatically detect recipients in your document? This can
save you time in setting up your document. save you time in setting up your document.
</Trans> </Trans>
</DialogDescription> </AlertDialogDescription>
</DialogHeader> </AlertDialogHeader>
<DialogFooter> <AlertDialogFooter>
<Button type="button" variant="secondary" onClick={handleSkip}> <AlertDialogCancel onClick={handleSkip}>
<Trans>Skip for now</Trans> <Trans>Skip for now</Trans>
</Button> </AlertDialogCancel>
<Button type="button" onClick={handleStartDetection}> <AlertDialogAction onClick={handleStartDetection}>
<Trans>Detect recipients</Trans> <Trans>Detect recipients</Trans>
</Button> </AlertDialogAction>
</DialogFooter> </AlertDialogFooter>
</> </>
)) ))
.with('PROCESSING', () => ( .with('PROCESSING', () => (
<> <div className="flex flex-col items-center justify-center py-4">
<DialogHeader> <div className="relative mb-4 flex items-center justify-center">
<DialogTitle className="flex items-center justify-center gap-2"> <div
<LoaderIcon className="h-5 w-5 animate-spin" /> className="border-muted-foreground/20 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left flex-col gap-y-1 overflow-hidden rounded-lg border px-2 py-4 backdrop-blur-sm"
style={{ transform: 'translateZ(0px)' }}
>
<div className="bg-muted-foreground/20 h-2 w-full rounded-[2px]"></div>
<div className="bg-muted-foreground/20 h-2 w-5/6 rounded-[2px]"></div>
<div className="bg-muted-foreground/20 h-2 w-full rounded-[2px]"></div>
<div className="bg-muted-foreground/20 h-2 w-4/5 rounded-[2px]"></div>
<div className="bg-muted-foreground/20 h-2 w-full rounded-[2px]"></div>
<div className="bg-muted-foreground/20 h-2 w-3/4 rounded-[2px]"></div>
</div>
<div
className="bg-documenso/80 animate-scan pointer-events-none absolute left-1/2 top-0 z-20 h-0.5 w-24 -translate-x-1/2"
style={{
transform: 'translateX(-50%) translateZ(0px)',
}}
/>
</div>
<AlertDialogHeader>
<AlertDialogTitle className="text-center">
<Trans>Analyzing your document</Trans> <Trans>Analyzing your document</Trans>
</DialogTitle> </AlertDialogTitle>
<DialogDescription className="text-center"> <AlertDialogDescription className="text-center">
<Trans> <Trans>
Scanning your document to detect recipient names, emails, and signing order. Scanning your document to detect recipient names, emails, and signing order.
</Trans> </Trans>
</DialogDescription> </AlertDialogDescription>
</DialogHeader> </AlertDialogHeader>
</> </div>
)) ))
.exhaustive()} .exhaustive()}
</AnimateGenericFadeInOut> </AnimateGenericFadeInOut>
</fieldset> </fieldset>
</DialogContent> </AlertDialogContent>
</Dialog> </AlertDialog>
); );
}; };

View File

@ -58,10 +58,10 @@ export const EnvelopeDropZoneWrapper = ({
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showRecipientDetectionPrompt, setShowRecipientDetectionPrompt] = useState(false); const [showExtractionPrompt, setShowExtractionPrompt] = useState(false);
const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null); const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null);
const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null); const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null);
const [showSuggestedRecipientsDialog, setShowSuggestedRecipientsDialog] = useState(false); const [showRecipientsDialog, setShowRecipientsDialog] = useState(false);
const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true); const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true);
const userTimezone = const userTimezone =
@ -124,9 +124,9 @@ export const EnvelopeDropZoneWrapper = ({
// Show AI prompt dialog for documents // Show AI prompt dialog for documents
setUploadedDocumentId(id); setUploadedDocumentId(id);
setPendingRecipients(null); setPendingRecipients(null);
setShowSuggestedRecipientsDialog(false); setShowRecipientsDialog(false);
setShouldNavigateAfterPromptClose(true); setShouldNavigateAfterPromptClose(true);
setShowRecipientDetectionPrompt(true); setShowExtractionPrompt(true);
} else { } else {
// Templates - navigate immediately // Templates - navigate immediately
const pathPrefix = formatTemplatesPath(team.url); const pathPrefix = formatTemplatesPath(team.url);
@ -242,15 +242,18 @@ export const EnvelopeDropZoneWrapper = ({
duration: 5000, duration: 5000,
}); });
throw new Error('NO_RECIPIENTS_DETECTED'); setShouldNavigateAfterPromptClose(true);
setShowExtractionPrompt(false);
navigateToEnvelopeEditor();
return;
} }
const recipientsWithEmails = ensureRecipientEmails(recipients, uploadedDocumentId); const recipientsWithEmails = ensureRecipientEmails(recipients, uploadedDocumentId);
setPendingRecipients(recipientsWithEmails); setPendingRecipients(recipientsWithEmails);
setShouldNavigateAfterPromptClose(false); setShouldNavigateAfterPromptClose(false);
setShowRecipientDetectionPrompt(false); setShowExtractionPrompt(false);
setShowSuggestedRecipientsDialog(true); setShowRecipientsDialog(true);
} catch (error) { } catch (error) {
if (!(error instanceof Error && error.message === 'NO_RECIPIENTS_DETECTED')) { if (!(error instanceof Error && error.message === 'NO_RECIPIENTS_DETECTED')) {
const parsedError = AppError.parseError(error); const parsedError = AppError.parseError(error);
@ -269,12 +272,12 @@ export const EnvelopeDropZoneWrapper = ({
const handleSkipRecipientDetection = () => { const handleSkipRecipientDetection = () => {
setShouldNavigateAfterPromptClose(true); setShouldNavigateAfterPromptClose(true);
setShowRecipientDetectionPrompt(false); setShowExtractionPrompt(false);
navigateToEnvelopeEditor(); navigateToEnvelopeEditor();
}; };
const handleRecipientsCancel = () => { const handleRecipientsCancel = () => {
setShowSuggestedRecipientsDialog(false); setShowRecipientsDialog(false);
setPendingRecipients(null); setPendingRecipients(null);
navigateToEnvelopeEditor(); navigateToEnvelopeEditor();
}; };
@ -296,7 +299,7 @@ export const EnvelopeDropZoneWrapper = ({
duration: 5000, duration: 5000,
}); });
setShowSuggestedRecipientsDialog(false); setShowRecipientsDialog(false);
setPendingRecipients(null); setPendingRecipients(null);
navigateToEnvelopeEditor(); navigateToEnvelopeEditor();
} catch (error) { } catch (error) {
@ -314,7 +317,7 @@ export const EnvelopeDropZoneWrapper = ({
}; };
const handlePromptDialogOpenChange = (open: boolean) => { const handlePromptDialogOpenChange = (open: boolean) => {
setShowRecipientDetectionPrompt(open); setShowExtractionPrompt(open);
if (open) { if (open) {
setShouldNavigateAfterPromptClose(true); setShouldNavigateAfterPromptClose(true);
@ -394,20 +397,20 @@ export const EnvelopeDropZoneWrapper = ({
)} )}
<RecipientDetectionPromptDialog <RecipientDetectionPromptDialog
open={showRecipientDetectionPrompt} open={showExtractionPrompt}
onOpenChange={handlePromptDialogOpenChange} onOpenChange={handlePromptDialogOpenChange}
onAccept={handleStartRecipientDetection} onAccept={handleStartRecipientDetection}
onSkip={handleSkipRecipientDetection} onSkip={handleSkipRecipientDetection}
/> />
<SuggestedRecipientsDialog <SuggestedRecipientsDialog
open={showSuggestedRecipientsDialog} open={showRecipientsDialog}
recipients={pendingRecipients} recipients={pendingRecipients}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
handleRecipientsCancel(); handleRecipientsCancel();
} else { } else {
setShowSuggestedRecipientsDialog(true); setShowRecipientsDialog(true);
} }
}} }}
onCancel={handleRecipientsCancel} onCancel={handleRecipientsCancel}

View File

@ -62,10 +62,10 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits(); const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showRecipientDetectionPrompt, setShowAiPromptDialog] = useState(false); const [showExtractionPrompt, setShowExtractionPrompt] = useState(false);
const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null); const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null);
const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null); const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null);
const [showSuggestedRecipientsDialog, setShowAiRecipientsDialog] = useState(false); const [showRecipientsDialog, setShowRecipientsDialog] = useState(false);
const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true); const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true);
const { mutateAsync: createEnvelope } = trpc.envelope.create.useMutation(); const { mutateAsync: createEnvelope } = trpc.envelope.create.useMutation();
@ -125,9 +125,9 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
if (type === EnvelopeType.DOCUMENT) { if (type === EnvelopeType.DOCUMENT) {
setUploadedDocumentId(id); setUploadedDocumentId(id);
setPendingRecipients(null); setPendingRecipients(null);
setShowAiRecipientsDialog(false); setShowRecipientsDialog(false);
setShouldNavigateAfterPromptClose(true); setShouldNavigateAfterPromptClose(true);
setShowAiPromptDialog(true); setShowExtractionPrompt(true);
toast({ toast({
title: t`Document uploaded`, title: t`Document uploaded`,
@ -219,15 +219,18 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
duration: 5000, duration: 5000,
}); });
throw new Error('NO_RECIPIENTS_DETECTED'); setShouldNavigateAfterPromptClose(true);
setShowExtractionPrompt(false);
navigateToEnvelopeEditor();
return;
} }
const recipientsWithEmails = ensureRecipientEmails(recipients, uploadedDocumentId); const recipientsWithEmails = ensureRecipientEmails(recipients, uploadedDocumentId);
setPendingRecipients(recipientsWithEmails); setPendingRecipients(recipientsWithEmails);
setShouldNavigateAfterPromptClose(false); setShouldNavigateAfterPromptClose(false);
setShowAiPromptDialog(false); setShowExtractionPrompt(false);
setShowAiRecipientsDialog(true); setShowRecipientsDialog(true);
} catch (err) { } catch (err) {
if (!(err instanceof Error && err.message === 'NO_RECIPIENTS_DETECTED')) { if (!(err instanceof Error && err.message === 'NO_RECIPIENTS_DETECTED')) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -246,12 +249,12 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
const handleSkipRecipientDetection = () => { const handleSkipRecipientDetection = () => {
setShouldNavigateAfterPromptClose(true); setShouldNavigateAfterPromptClose(true);
setShowAiPromptDialog(false); setShowExtractionPrompt(false);
navigateToEnvelopeEditor(); navigateToEnvelopeEditor();
}; };
const handleRecipientsCancel = () => { const handleRecipientsCancel = () => {
setShowAiRecipientsDialog(false); setShowRecipientsDialog(false);
setPendingRecipients(null); setPendingRecipients(null);
navigateToEnvelopeEditor(); navigateToEnvelopeEditor();
}; };
@ -273,7 +276,7 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
duration: 5000, duration: 5000,
}); });
setShowAiRecipientsDialog(false); setShowRecipientsDialog(false);
setPendingRecipients(null); setPendingRecipients(null);
navigateToEnvelopeEditor(); navigateToEnvelopeEditor();
} catch (err) { } catch (err) {
@ -291,7 +294,7 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
}; };
const handlePromptDialogOpenChange = (open: boolean) => { const handlePromptDialogOpenChange = (open: boolean) => {
setShowAiPromptDialog(open); setShowExtractionPrompt(open);
if (open) { if (open) {
setShouldNavigateAfterPromptClose(true); setShouldNavigateAfterPromptClose(true);
@ -337,20 +340,20 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
</TooltipProvider> </TooltipProvider>
<RecipientDetectionPromptDialog <RecipientDetectionPromptDialog
open={showRecipientDetectionPrompt} open={showExtractionPrompt}
onOpenChange={handlePromptDialogOpenChange} onOpenChange={handlePromptDialogOpenChange}
onAccept={handleStartRecipientDetection} onAccept={handleStartRecipientDetection}
onSkip={handleSkipRecipientDetection} onSkip={handleSkipRecipientDetection}
/> />
<SuggestedRecipientsDialog <SuggestedRecipientsDialog
open={showSuggestedRecipientsDialog} open={showRecipientsDialog}
recipients={pendingRecipients} recipients={pendingRecipients}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
handleRecipientsCancel(); handleRecipientsCancel();
} else { } else {
setShowAiRecipientsDialog(true); setShowRecipientsDialog(true);
} }
}} }}
onCancel={handleRecipientsCancel} onCancel={handleRecipientsCancel}

View File

@ -106,6 +106,7 @@ IMPORTANT:
- If no clear ordering, omit signingOrder - If no clear ordering, omit signingOrder
- Return empty array if absolutely no recipients can be detected - Return empty array if absolutely no recipients can be detected
- Do NOT invent recipients - only extract what's clearly present - Do NOT invent recipients - only extract what's clearly present
- If a signature line exists but no name is associated with it, DO NOT return a recipient with name "<UNKNOWN>". Skip it.
EXAMPLES: EXAMPLES:
Good: Good:
@ -116,4 +117,5 @@ Good:
Bad: Bad:
- Extracting the document title as a recipient name - Extracting the document title as a recipient name
- Making up email addresses that aren't in the document - Making up email addresses that aren't in the document
- Adding people not mentioned in the document`; - Adding people not mentioned in the document
- Using placeholder names like "<UNKNOWN>", "Unknown", "Signer"`;

View File

@ -142,11 +142,17 @@ module.exports = {
'0%,70%,100%': { opacity: '1' }, '0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' }, '20%,50%': { opacity: '0' },
}, },
scan: {
'0%': { top: '0%' },
'50%': { top: '95%' },
'100%': { top: '0%' },
},
}, },
animation: { animation: {
'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out',
'caret-blink': 'caret-blink 1.25s ease-out infinite', 'caret-blink': 'caret-blink 1.25s ease-out infinite',
scan: 'scan 4s linear infinite',
}, },
screens: { screens: {
'3xl': '1920px', '3xl': '1920px',