chore: refactor

This commit is contained in:
Ephraim Atta-Duncan
2025-11-18 20:07:04 +00:00
parent 13bd5815d9
commit 8e2ca94020
13 changed files with 221 additions and 247 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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) {