mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
chore: refactor
This commit is contained in:
@ -15,22 +15,22 @@ import {
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
|
||||
type DocumentAiStep = 'PROMPT' | 'PROCESSING';
|
||||
type RecipientDetectionStep = 'PROMPT' | 'PROCESSING';
|
||||
|
||||
export type DocumentAiPromptDialogProps = {
|
||||
export type RecipientDetectionPromptDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAccept: () => Promise<void> | void;
|
||||
onSkip: () => void;
|
||||
};
|
||||
|
||||
export const DocumentAiPromptDialog = ({
|
||||
export const RecipientDetectionPromptDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onAccept,
|
||||
onSkip,
|
||||
}: DocumentAiPromptDialogProps) => {
|
||||
const [currentStep, setCurrentStep] = useState<DocumentAiStep>('PROMPT');
|
||||
}: RecipientDetectionPromptDialogProps) => {
|
||||
const [currentStep, setCurrentStep] = useState<RecipientDetectionStep>('PROMPT');
|
||||
|
||||
// Reset to first step when dialog closes
|
||||
useEffect(() => {
|
||||
@ -39,7 +39,7 @@ export const DocumentAiPromptDialog = ({
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleUseAi = () => {
|
||||
const handleStartDetection = () => {
|
||||
setCurrentStep('PROCESSING');
|
||||
|
||||
Promise.resolve(onAccept()).catch(() => {
|
||||
@ -61,12 +61,12 @@ export const DocumentAiPromptDialog = ({
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Trans>Use AI to prepare your document?</Trans>
|
||||
<Trans>Auto-detect recipients?</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Would you like to use AI to automatically add recipients to your document?
|
||||
This can save you time in setting up your document.
|
||||
Would you like to automatically detect recipients in your document? This can
|
||||
save you time in setting up your document.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@ -75,8 +75,8 @@ export const DocumentAiPromptDialog = ({
|
||||
<Button type="button" variant="secondary" onClick={handleSkip}>
|
||||
<Trans>Skip for now</Trans>
|
||||
</Button>
|
||||
<Button type="button" onClick={handleUseAi}>
|
||||
<Trans>Use AI</Trans>
|
||||
<Button type="button" onClick={handleStartDetection}>
|
||||
<Trans>Detect recipients</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@ -90,8 +90,7 @@ export const DocumentAiPromptDialog = ({
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
<Trans>
|
||||
Our AI is scanning your document to detect recipient names, emails, and
|
||||
signing order.
|
||||
Scanning your document to detect recipient names, emails, and signing order.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@ -34,9 +34,9 @@ import {
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||
|
||||
import type { RecipientForCreation } from '~/utils/analyze-ai-recipients';
|
||||
import type { RecipientForCreation } from '~/utils/detect-document-recipients';
|
||||
|
||||
const ZDocumentAiRecipientSchema = z.object({
|
||||
const ZSuggestedRecipientSchema = z.object({
|
||||
formId: z.string().min(1),
|
||||
name: z
|
||||
.string()
|
||||
@ -50,15 +50,15 @@ const ZDocumentAiRecipientSchema = z.object({
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
});
|
||||
|
||||
const ZDocumentAiRecipientsForm = z.object({
|
||||
const ZSuggestedRecipientsFormSchema = z.object({
|
||||
recipients: z
|
||||
.array(ZDocumentAiRecipientSchema)
|
||||
.array(ZSuggestedRecipientSchema)
|
||||
.min(1, { message: msg`Please add at least one recipient`.id }),
|
||||
});
|
||||
|
||||
type TDocumentAiRecipientsForm = z.infer<typeof ZDocumentAiRecipientsForm>;
|
||||
type TSuggestedRecipientsFormSchema = z.infer<typeof ZSuggestedRecipientsFormSchema>;
|
||||
|
||||
export type DocumentAiRecipientsDialogProps = {
|
||||
export type SuggestedRecipientsDialogProps = {
|
||||
open: boolean;
|
||||
recipients: RecipientForCreation[] | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@ -66,13 +66,13 @@ export type DocumentAiRecipientsDialogProps = {
|
||||
onSubmit: (recipients: RecipientForCreation[]) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DocumentAiRecipientsDialog = ({
|
||||
export const SuggestedRecipientsDialog = ({
|
||||
open,
|
||||
recipients,
|
||||
onOpenChange,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: DocumentAiRecipientsDialogProps) => {
|
||||
}: SuggestedRecipientsDialogProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
||||
@ -117,8 +117,8 @@ export const DocumentAiRecipientsDialog = ({
|
||||
];
|
||||
}, [recipients]);
|
||||
|
||||
const form = useForm<TDocumentAiRecipientsForm>({
|
||||
resolver: zodResolver(ZDocumentAiRecipientsForm),
|
||||
const form = useForm<TSuggestedRecipientsFormSchema>({
|
||||
resolver: zodResolver(ZSuggestedRecipientsFormSchema),
|
||||
defaultValues: {
|
||||
recipients: defaultRecipients,
|
||||
},
|
||||
@ -54,30 +54,7 @@ const EnvelopeEditorFieldsPageRenderer = lazy(
|
||||
async () => import('./envelope-editor-fields-page-renderer'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Enforces minimum field dimensions and centers the field when expanding to meet minimums.
|
||||
*
|
||||
* AI often detects form lines as very thin fields (0.2-0.5% height). This function ensures
|
||||
* fields meet minimum usability requirements by expanding them to at least 30px height and
|
||||
* 36px width, while keeping them centered on their original position.
|
||||
*
|
||||
* @param params - Field dimensions and page size
|
||||
* @param params.positionX - Field X position as percentage (0-100)
|
||||
* @param params.positionY - Field Y position as percentage (0-100)
|
||||
* @param params.width - Field width as percentage (0-100)
|
||||
* @param params.height - Field height as percentage (0-100)
|
||||
* @param params.pageWidth - Page width in pixels
|
||||
* @param params.pageHeight - Page height in pixels
|
||||
* @returns Adjusted field dimensions with minimums enforced and centered
|
||||
*
|
||||
* @example
|
||||
* // AI detected a thin line: 0.3% height
|
||||
* const adjusted = enforceMinimumFieldDimensions({
|
||||
* positionX: 20, positionY: 50, width: 30, height: 0.3,
|
||||
* pageWidth: 800, pageHeight: 1100
|
||||
* });
|
||||
* // Result: height expanded to ~2.7% (30px), centered on original position
|
||||
*/
|
||||
// Expands fields to minimum usable dimensions (30px height, 36px width) and centers them
|
||||
const enforceMinimumFieldDimensions = (params: {
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
@ -94,7 +71,6 @@ const enforceMinimumFieldDimensions = (params: {
|
||||
const MIN_HEIGHT_PX = 30;
|
||||
const MIN_WIDTH_PX = 36;
|
||||
|
||||
// Convert percentage to pixels to check against minimums
|
||||
const widthPx = (params.width / 100) * params.pageWidth;
|
||||
const heightPx = (params.height / 100) * params.pageHeight;
|
||||
|
||||
@ -136,7 +112,7 @@ const enforceMinimumFieldDimensions = (params: {
|
||||
};
|
||||
};
|
||||
|
||||
const processAllPagesWithAI = async (params: {
|
||||
const detectFormFieldsInDocument = async (params: {
|
||||
envelopeId: string;
|
||||
onProgress: (current: number, total: number) => void;
|
||||
}): Promise<{
|
||||
@ -148,10 +124,9 @@ const processAllPagesWithAI = async (params: {
|
||||
const errors = new Map<number, Error>();
|
||||
|
||||
try {
|
||||
// Make single API call to process all pages server-side
|
||||
onProgress(0, 1);
|
||||
|
||||
const response = await fetch('/api/ai/detect-form-fields', {
|
||||
const response = await fetch('/api/ai/detect-fields', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -162,12 +137,11 @@ const processAllPagesWithAI = async (params: {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`AI detection failed: ${response.statusText} - ${errorText}`);
|
||||
throw new Error(`Field detection failed: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const detectedFields: TDetectedFormField[] = await response.json();
|
||||
|
||||
// Group fields by page number
|
||||
for (const field of detectedFields) {
|
||||
if (!fieldsPerPage.has(field.pageNumber)) {
|
||||
fieldsPerPage.set(field.pageNumber, []);
|
||||
@ -177,7 +151,6 @@ const processAllPagesWithAI = async (params: {
|
||||
|
||||
onProgress(1, 1);
|
||||
} catch (error) {
|
||||
// If request fails, treat it as error for all pages
|
||||
errors.set(0, error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
|
||||
@ -206,7 +179,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isAutoAddingFields, setIsAutoAddingFields] = useState(false);
|
||||
const [isDetectingFields, setIsAutoAddingFields] = useState(false);
|
||||
const [processingProgress, setProcessingProgress] = useState<{
|
||||
current: number;
|
||||
total: number;
|
||||
@ -224,14 +197,10 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
|
||||
const isMetaSame = isDeepEqual(selectedField.fieldMeta, fieldMeta);
|
||||
|
||||
// Todo: Envelopes - Clean up console logs.
|
||||
if (!isMetaSame) {
|
||||
console.log('TRIGGER UPDATE');
|
||||
editorFields.updateFieldByFormId(selectedField.formId, {
|
||||
fieldMeta,
|
||||
});
|
||||
} else {
|
||||
console.log('DATA IS SAME, NO UPDATE');
|
||||
}
|
||||
};
|
||||
|
||||
@ -251,7 +220,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
<div className="relative flex h-full">
|
||||
<div className="relative flex w-full flex-col overflow-y-auto">
|
||||
{/* Horizontal envelope item selector */}
|
||||
{isAutoAddingFields && (
|
||||
{isDetectingFields && (
|
||||
<>
|
||||
<div className="edge-glow edge-glow-top pointer-events-none fixed left-0 right-0 top-0 z-20 h-16" />
|
||||
<div className="edge-glow edge-glow-right pointer-events-none fixed bottom-0 right-0 top-0 z-20 w-16" />
|
||||
@ -353,7 +322,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
<Button
|
||||
className="mt-4 w-full"
|
||||
variant="outline"
|
||||
disabled={isAutoAddingFields}
|
||||
disabled={isDetectingFields}
|
||||
onClick={async () => {
|
||||
setIsAutoAddingFields(true);
|
||||
setProcessingProgress(null);
|
||||
@ -377,7 +346,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fieldsPerPage, errors } = await processAllPagesWithAI({
|
||||
const { fieldsPerPage, errors } = await detectFormFieldsInDocument({
|
||||
envelopeId: envelope.id,
|
||||
onProgress: (current, total) => {
|
||||
setProcessingProgress({ current, total });
|
||||
@ -488,7 +457,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isAutoAddingFields ? <Trans>Processing...</Trans> : <Trans>Auto add fields</Trans>}
|
||||
{isDetectingFields ? <Trans>Processing...</Trans> : <Trans>Auto add fields</Trans>}
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Loader } from 'lucide-react';
|
||||
import {
|
||||
@ -27,14 +26,14 @@ import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-rou
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
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 { RecipientDetectionPromptDialog } from '~/components/dialogs/recipient-detection-prompt-dialog';
|
||||
import { SuggestedRecipientsDialog } from '~/components/dialogs/suggested-recipients-dialog';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import {
|
||||
type RecipientForCreation,
|
||||
analyzeRecipientsFromDocument,
|
||||
detectRecipientsInDocument,
|
||||
ensureRecipientEmails,
|
||||
} from '~/utils/analyze-ai-recipients';
|
||||
} from '~/utils/detect-document-recipients';
|
||||
|
||||
export interface EnvelopeDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
@ -59,10 +58,10 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showAiPromptDialog, setShowAiPromptDialog] = useState(false);
|
||||
const [showRecipientDetectionPrompt, setShowRecipientDetectionPrompt] = useState(false);
|
||||
const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null);
|
||||
const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null);
|
||||
const [showAiRecipientsDialog, setShowAiRecipientsDialog] = useState(false);
|
||||
const [showSuggestedRecipientsDialog, setShowSuggestedRecipientsDialog] = useState(false);
|
||||
const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true);
|
||||
|
||||
const userTimezone =
|
||||
@ -125,9 +124,9 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
// Show AI prompt dialog for documents
|
||||
setUploadedDocumentId(id);
|
||||
setPendingRecipients(null);
|
||||
setShowAiRecipientsDialog(false);
|
||||
setShowSuggestedRecipientsDialog(false);
|
||||
setShouldNavigateAfterPromptClose(true);
|
||||
setShowAiPromptDialog(true);
|
||||
setShowRecipientDetectionPrompt(true);
|
||||
} else {
|
||||
// Templates - navigate immediately
|
||||
const pathPrefix = formatTemplatesPath(team.url);
|
||||
@ -228,13 +227,13 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
void navigate(`${pathPrefix}/${uploadedDocumentId}/edit`);
|
||||
};
|
||||
|
||||
const handleAiAccept = async () => {
|
||||
const handleStartRecipientDetection = async () => {
|
||||
if (!uploadedDocumentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recipients = await analyzeRecipientsFromDocument(uploadedDocumentId);
|
||||
const recipients = await detectRecipientsInDocument(uploadedDocumentId);
|
||||
|
||||
if (recipients.length === 0) {
|
||||
toast({
|
||||
@ -250,14 +249,14 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
|
||||
setPendingRecipients(recipientsWithEmails);
|
||||
setShouldNavigateAfterPromptClose(false);
|
||||
setShowAiPromptDialog(false);
|
||||
setShowAiRecipientsDialog(true);
|
||||
setShowRecipientDetectionPrompt(false);
|
||||
setShowSuggestedRecipientsDialog(true);
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && error.message === 'NO_RECIPIENTS_DETECTED')) {
|
||||
const parsedError = AppError.parseError(error);
|
||||
|
||||
toast({
|
||||
title: t`Failed to analyze recipients`,
|
||||
title: t`Failed to detect recipients`,
|
||||
description: parsedError.userMessage || t`You can add recipients manually in the editor`,
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
@ -268,14 +267,14 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAiSkip = () => {
|
||||
const handleSkipRecipientDetection = () => {
|
||||
setShouldNavigateAfterPromptClose(true);
|
||||
setShowAiPromptDialog(false);
|
||||
setShowRecipientDetectionPrompt(false);
|
||||
navigateToEnvelopeEditor();
|
||||
};
|
||||
|
||||
const handleRecipientsCancel = () => {
|
||||
setShowAiRecipientsDialog(false);
|
||||
setShowSuggestedRecipientsDialog(false);
|
||||
setPendingRecipients(null);
|
||||
navigateToEnvelopeEditor();
|
||||
};
|
||||
@ -297,7 +296,7 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setShowAiRecipientsDialog(false);
|
||||
setShowSuggestedRecipientsDialog(false);
|
||||
setPendingRecipients(null);
|
||||
navigateToEnvelopeEditor();
|
||||
} catch (error) {
|
||||
@ -315,7 +314,7 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
};
|
||||
|
||||
const handlePromptDialogOpenChange = (open: boolean) => {
|
||||
setShowAiPromptDialog(open);
|
||||
setShowRecipientDetectionPrompt(open);
|
||||
|
||||
if (open) {
|
||||
setShouldNavigateAfterPromptClose(true);
|
||||
@ -394,21 +393,21 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentAiPromptDialog
|
||||
open={showAiPromptDialog}
|
||||
<RecipientDetectionPromptDialog
|
||||
open={showRecipientDetectionPrompt}
|
||||
onOpenChange={handlePromptDialogOpenChange}
|
||||
onAccept={handleAiAccept}
|
||||
onSkip={handleAiSkip}
|
||||
onAccept={handleStartRecipientDetection}
|
||||
onSkip={handleSkipRecipientDetection}
|
||||
/>
|
||||
|
||||
<DocumentAiRecipientsDialog
|
||||
open={showAiRecipientsDialog}
|
||||
<SuggestedRecipientsDialog
|
||||
open={showSuggestedRecipientsDialog}
|
||||
recipients={pendingRecipients}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleRecipientsCancel();
|
||||
} else {
|
||||
setShowAiRecipientsDialog(true);
|
||||
setShowSuggestedRecipientsDialog(true);
|
||||
}
|
||||
}}
|
||||
onCancel={handleRecipientsCancel}
|
||||
|
||||
@ -27,14 +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 { RecipientDetectionPromptDialog } from '~/components/dialogs/recipient-detection-prompt-dialog';
|
||||
import { SuggestedRecipientsDialog } from '~/components/dialogs/suggested-recipients-dialog';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import {
|
||||
type RecipientForCreation,
|
||||
analyzeRecipientsFromDocument,
|
||||
detectRecipientsInDocument,
|
||||
ensureRecipientEmails,
|
||||
} from '~/utils/analyze-ai-recipients';
|
||||
} from '~/utils/detect-document-recipients';
|
||||
|
||||
export type EnvelopeUploadButtonProps = {
|
||||
className?: string;
|
||||
@ -62,10 +62,10 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showAiPromptDialog, setShowAiPromptDialog] = useState(false);
|
||||
const [showRecipientDetectionPrompt, setShowAiPromptDialog] = useState(false);
|
||||
const [uploadedDocumentId, setUploadedDocumentId] = useState<string | null>(null);
|
||||
const [pendingRecipients, setPendingRecipients] = useState<RecipientForCreation[] | null>(null);
|
||||
const [showAiRecipientsDialog, setShowAiRecipientsDialog] = useState(false);
|
||||
const [showSuggestedRecipientsDialog, setShowAiRecipientsDialog] = useState(false);
|
||||
const [shouldNavigateAfterPromptClose, setShouldNavigateAfterPromptClose] = useState(true);
|
||||
|
||||
const { mutateAsync: createEnvelope } = trpc.envelope.create.useMutation();
|
||||
@ -204,13 +204,13 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
void navigate(`${pathPrefix}/${uploadedDocumentId}/edit`);
|
||||
};
|
||||
|
||||
const handleAiAccept = async () => {
|
||||
const handleStartRecipientDetection = async () => {
|
||||
if (!uploadedDocumentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recipients = await analyzeRecipientsFromDocument(uploadedDocumentId);
|
||||
const recipients = await detectRecipientsInDocument(uploadedDocumentId);
|
||||
|
||||
if (recipients.length === 0) {
|
||||
toast({
|
||||
@ -244,7 +244,7 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
}
|
||||
};
|
||||
|
||||
const handleAiSkip = () => {
|
||||
const handleSkipRecipientDetection = () => {
|
||||
setShouldNavigateAfterPromptClose(true);
|
||||
setShowAiPromptDialog(false);
|
||||
navigateToEnvelopeEditor();
|
||||
@ -336,15 +336,15 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<DocumentAiPromptDialog
|
||||
open={showAiPromptDialog}
|
||||
<RecipientDetectionPromptDialog
|
||||
open={showRecipientDetectionPrompt}
|
||||
onOpenChange={handlePromptDialogOpenChange}
|
||||
onAccept={handleAiAccept}
|
||||
onSkip={handleAiSkip}
|
||||
onAccept={handleStartRecipientDetection}
|
||||
onSkip={handleSkipRecipientDetection}
|
||||
/>
|
||||
|
||||
<DocumentAiRecipientsDialog
|
||||
open={showAiRecipientsDialog}
|
||||
<SuggestedRecipientsDialog
|
||||
open={showSuggestedRecipientsDialog}
|
||||
recipients={pendingRecipients}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
|
||||
export type AiRecipient = {
|
||||
export type SuggestedRecipient = {
|
||||
name: string;
|
||||
email?: string;
|
||||
role: 'SIGNER' | 'APPROVER' | 'CC';
|
||||
signingOrder?: number;
|
||||
};
|
||||
|
||||
export const analyzeRecipientsFromDocument = async (envelopeId: string): Promise<AiRecipient[]> => {
|
||||
export const detectRecipientsInDocument = async (
|
||||
envelopeId: string,
|
||||
): Promise<SuggestedRecipient[]> => {
|
||||
try {
|
||||
const response = await fetch('/api/ai/analyze-recipients', {
|
||||
const response = await fetch('/api/ai/detect-recipients', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -20,10 +22,12 @@ export const analyzeRecipientsFromDocument = async (envelopeId: string): Promise
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to analyze recipients');
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to detect recipients',
|
||||
});
|
||||
}
|
||||
|
||||
return (await response.json()) as AiRecipient[];
|
||||
return (await response.json()) as SuggestedRecipient[];
|
||||
} catch (error) {
|
||||
throw AppError.parseError(error);
|
||||
}
|
||||
@ -37,7 +41,7 @@ export type RecipientForCreation = {
|
||||
};
|
||||
|
||||
export const ensureRecipientEmails = (
|
||||
recipients: AiRecipient[],
|
||||
recipients: SuggestedRecipient[],
|
||||
envelopeId: string,
|
||||
): RecipientForCreation[] => {
|
||||
const allowedRoles: RecipientRole[] = [
|
||||
Reference in New Issue
Block a user