feat: generate recipients and fields based on recipients

This commit is contained in:
Ephraim Atta-Duncan
2025-11-17 01:44:22 +00:00
parent 9e0f07f806
commit dbed8b362e
15 changed files with 1539 additions and 241 deletions

View File

@ -27,7 +27,14 @@ import {
} from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentAiPromptDialog } from '~/components/dialogs/document-ai-prompt-dialog';
import { DocumentAiRecipientsDialog } from '~/components/dialogs/document-ai-recipients-dialog';
import { useCurrentTeam } from '~/providers/team';
import {
type RecipientForCreation,
analyzeRecipientsFromDocument,
ensureRecipientEmails,
} from '~/utils/analyze-ai-recipients';
export type EnvelopeUploadButtonProps = {
className?: string;
@ -55,8 +62,14 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits();
const [isLoading, setIsLoading] = useState(false);
const [showAiPromptDialog, setShowAiPromptDialog] = useState(false);
const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null);
const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null);
const [showAiRecipientsDialog, setShowAiRecipientsDialog] = useState(false);
const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true);
const { mutateAsync: createEnvelope } = trpc.envelope.create.useMutation();
const { mutateAsync: createRecipients } = trpc.envelope.recipient.createMany.useMutation();
const disabledMessage = useMemo(() => {
if (organisation.subscription && remaining.documents === 0) {
@ -108,16 +121,29 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
? formatDocumentsPath(team.url)
: formatTemplatesPath(team.url);
await navigate(`${pathPrefix}/${id}/edit`);
// Show AI prompt dialog for documents only
if (type === EnvelopeType.DOCUMENT) {
setUploadedDocumentId(id);
setPendingRecipients(null);
setShowAiRecipientsDialog(false);
setShouldNavigateAfterPromptClose(true);
setShowAiPromptDialog(true);
toast({
title: type === EnvelopeType.DOCUMENT ? t`Document uploaded` : t`Template uploaded`,
description:
type === EnvelopeType.DOCUMENT
? t`Your document has been uploaded successfully.`
: t`Your template has been uploaded successfully.`,
duration: 5000,
});
toast({
title: t`Document uploaded`,
description: t`Your document has been uploaded successfully.`,
duration: 5000,
});
} else {
// Templates - navigate immediately
await navigate(`${pathPrefix}/${id}/edit`);
toast({
title: t`Template uploaded`,
description: t`Your template has been uploaded successfully.`,
duration: 5000,
});
}
} catch (err) {
const error = AppError.parseError(err);
@ -169,6 +195,114 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
});
};
const navigateToEnvelopeEditor = () => {
if (!uploadedDocumentId) {
return;
}
const pathPrefix = formatDocumentsPath(team.url);
void navigate(`${pathPrefix}/${uploadedDocumentId}/edit`);
};
const handleAiAccept = async () => {
if (!uploadedDocumentId) {
return;
}
try {
const recipients = await analyzeRecipientsFromDocument(uploadedDocumentId);
if (recipients.length === 0) {
toast({
title: t`No recipients detected`,
description: t`You can add recipients manually in the editor`,
duration: 5000,
});
throw new Error('NO_RECIPIENTS_DETECTED');
}
const recipientsWithEmails = ensureRecipientEmails(recipients, uploadedDocumentId);
setPendingRecipients(recipientsWithEmails);
setShouldNavigateAfterPromptClose(false);
setShowAiPromptDialog(false);
setShowAiRecipientsDialog(true);
} catch (err) {
if (!(err instanceof Error && err.message === 'NO_RECIPIENTS_DETECTED')) {
const error = AppError.parseError(err);
toast({
title: t`Failed to analyze recipients`,
description: error.userMessage || t`You can add recipients manually in the editor`,
variant: 'destructive',
duration: 7500,
});
}
throw err;
}
};
const handleAiSkip = () => {
setShouldNavigateAfterPromptClose(true);
setShowAiPromptDialog(false);
navigateToEnvelopeEditor();
};
const handleRecipientsCancel = () => {
setShowAiRecipientsDialog(false);
setPendingRecipients(null);
navigateToEnvelopeEditor();
};
const handleRecipientsConfirm = async (recipientsToCreate: RecipientForCreation[]) => {
if (!uploadedDocumentId) {
return;
}
try {
await createRecipients({
envelopeId: uploadedDocumentId,
data: recipientsToCreate,
});
toast({
title: t`Recipients added`,
description: t`Successfully detected ${recipientsToCreate.length} recipient(s)`,
duration: 5000,
});
setShowAiRecipientsDialog(false);
setPendingRecipients(null);
navigateToEnvelopeEditor();
} catch (err) {
const error = AppError.parseError(err);
toast({
title: t`Failed to add recipients`,
description: error.userMessage || t`Please review the recipients and try again`,
variant: 'destructive',
duration: 7500,
});
throw err;
}
};
const handlePromptDialogOpenChange = (open: boolean) => {
setShowAiPromptDialog(open);
if (open) {
setShouldNavigateAfterPromptClose(true);
return;
}
if (!open && shouldNavigateAfterPromptClose) {
navigateToEnvelopeEditor();
}
};
return (
<div className={cn('relative', className)}>
<TooltipProvider>
@ -201,6 +335,27 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
)}
</Tooltip>
</TooltipProvider>
<DocumentAiPromptDialog
open={showAiPromptDialog}
onOpenChange={handlePromptDialogOpenChange}
onAccept={handleAiAccept}
onSkip={handleAiSkip}
/>
<DocumentAiRecipientsDialog
open={showAiRecipientsDialog}
recipients={pendingRecipients}
onOpenChange={(open) => {
if (!open) {
handleRecipientsCancel();
} else {
setShowAiRecipientsDialog(true);
}
}}
onCancel={handleRecipientsCancel}
onSubmit={handleRecipientsConfirm}
/>
</div>
);
};