chore: review

This commit is contained in:
Ephraim Atta-Duncan
2025-11-19 08:53:57 +00:00
parent 57c4c3fd48
commit a0a3e7fb93
4 changed files with 103 additions and 53 deletions

View File

@ -22,6 +22,7 @@ import {
type TDetectedRecipient, type TDetectedRecipient,
ZAnalyzeRecipientsRequestSchema, ZAnalyzeRecipientsRequestSchema,
ZDetectFormFieldsRequestSchema, ZDetectFormFieldsRequestSchema,
ZDetectFormFieldsResponseSchema,
ZDetectedFormFieldSchema, ZDetectedFormFieldSchema,
ZDetectedRecipientLLMSchema, ZDetectedRecipientLLMSchema,
} from './types'; } from './types';
@ -180,6 +181,13 @@ export const aiRoute = new Hono<HonoEnv>()
try { try {
const { user } = await getSession(c.req.raw); const { user } = await getSession(c.req.raw);
if (!user.emailVerified) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Email verification required',
userMessage: 'Please verify your email to use AI features',
});
}
const body = await c.req.json(); const body = await c.req.json();
const parsed = ZDetectFormFieldsRequestSchema.safeParse(body); const parsed = ZDetectFormFieldsRequestSchema.safeParse(body);
@ -259,15 +267,25 @@ export const aiRoute = new Hono<HonoEnv>()
); );
const detectedFields: TDetectFormFieldsResponse = []; const detectedFields: TDetectFormFieldsResponse = [];
const failedPages: number[] = [];
for (const [index, result] of results.entries()) { for (const [index, result] of results.entries()) {
if (result.status === 'fulfilled') { if (result.status === 'fulfilled') {
detectedFields.push(...result.value); detectedFields.push(...result.value);
} else { } else {
const pageNumber = renderedPages[index]?.pageNumber ?? index + 1; const pageNumber = renderedPages[index]?.pageNumber ?? index + 1;
console.error(`Failed to detect fields on page ${pageNumber}:`, result.reason); console.error(`Failed to detect fields on page ${pageNumber}:`, result.reason);
failedPages.push(pageNumber);
} }
} }
if (failedPages.length > 0) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: `Failed to detect fields on pages: ${failedPages.join(', ')}`,
userMessage: 'We could not detect fields on some pages. Please try again.',
});
}
if (env('NEXT_PUBLIC_AI_DEBUG_PREVIEW') === 'true') { if (env('NEXT_PUBLIC_AI_DEBUG_PREVIEW') === 'true') {
const debugDir = join(process.cwd(), '..', '..', 'packages', 'assets', 'ai-previews'); const debugDir = join(process.cwd(), '..', '..', 'packages', 'assets', 'ai-previews');
await mkdir(debugDir, { recursive: true }); await mkdir(debugDir, { recursive: true });
@ -378,7 +396,9 @@ export const aiRoute = new Hono<HonoEnv>()
} }
} }
return c.json<TDetectFormFieldsResponse>(detectedFields); const validatedResponse = ZDetectFormFieldsResponseSchema.parse(detectedFields);
return c.json<TDetectFormFieldsResponse>(validatedResponse);
} catch (error) { } catch (error) {
if (error instanceof AppError) { if (error instanceof AppError) {
throw error; throw error;
@ -396,6 +416,13 @@ export const aiRoute = new Hono<HonoEnv>()
try { try {
const { user } = await getSession(c.req.raw); const { user } = await getSession(c.req.raw);
if (!user.emailVerified) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Email verification required',
userMessage: 'Please verify your email to use AI features',
});
}
const body = await c.req.json(); const body = await c.req.json();
const parsed = ZAnalyzeRecipientsRequestSchema.safeParse(body); const parsed = ZAnalyzeRecipientsRequestSchema.safeParse(body);
@ -455,9 +482,13 @@ export const aiRoute = new Hono<HonoEnv>()
const allRecipients: TDetectedRecipient[] = []; const allRecipients: TDetectedRecipient[] = [];
let recipientIndex = 1; let recipientIndex = 1;
for (const result of results) { const failedPages: number[] = [];
for (const [index, result] of results.entries()) {
if (result.status !== 'fulfilled') { if (result.status !== 'fulfilled') {
console.error('Failed to analyze recipients on a page:', result.reason); const pageNumber = pagesToAnalyze[index]?.pageNumber ?? index + 1;
console.error(`Failed to analyze recipients on page ${pageNumber}:`, result.reason);
failedPages.push(pageNumber);
continue; continue;
} }
@ -479,6 +510,13 @@ export const aiRoute = new Hono<HonoEnv>()
allRecipients.push(...recipientsWithEmails); allRecipients.push(...recipientsWithEmails);
} }
if (failedPages.length > 0) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: `Failed to analyze recipients on pages: ${failedPages.join(', ')}`,
userMessage: 'We could not analyze recipients on some pages. Please try again.',
});
}
return c.json<TAnalyzeRecipientsResponse>(allRecipients); return c.json<TAnalyzeRecipientsResponse>(allRecipients);
} catch (error) { } catch (error) {
if (error instanceof AppError) { if (error instanceof AppError) {

View File

@ -1,6 +1,11 @@
import { z } from 'zod'; import { z } from 'zod';
import type { TDetectedFormField } from '@documenso/lib/types/document-analysis'; import {
type TDetectedFormField,
ZDetectedFormFieldSchema,
} from '@documenso/lib/types/document-analysis';
export { ZDetectedFormFieldSchema };
export const ZGenerateTextRequestSchema = z.object({ export const ZGenerateTextRequestSchema = z.object({
prompt: z.string().min(1, 'Prompt is required').max(5000, 'Prompt is too long'), prompt: z.string().min(1, 'Prompt is required').max(5000, 'Prompt is too long'),
@ -13,38 +18,6 @@ export const ZGenerateTextResponseSchema = z.object({
export type TGenerateTextRequest = z.infer<typeof ZGenerateTextRequestSchema>; export type TGenerateTextRequest = z.infer<typeof ZGenerateTextRequestSchema>;
export type TGenerateTextResponse = z.infer<typeof ZGenerateTextResponseSchema>; export type TGenerateTextResponse = z.infer<typeof ZGenerateTextResponseSchema>;
export const ZDetectedFormFieldSchema = z.object({
boundingBox: z
.array(z.number())
.length(4)
.describe('Bounding box [ymin, xmin, ymax, xmax] in normalized 0-1000 range'),
label: z
.enum([
'SIGNATURE',
'INITIALS',
'NAME',
'EMAIL',
'DATE',
'TEXT',
'NUMBER',
'RADIO',
'CHECKBOX',
'DROPDOWN',
])
.describe('Documenso field type inferred from nearby label text or visual characteristics'),
pageNumber: z
.number()
.int()
.positive()
.describe('1-indexed page number where field was detected'),
recipientId: z
.number()
.int()
.describe(
'ID of the recipient (from the provided envelope recipients) who should own the field',
),
});
export const ZDetectFormFieldsRequestSchema = z.object({ export const ZDetectFormFieldsRequestSchema = z.object({
envelopeId: z.string().min(1, { message: 'Envelope ID is required' }), envelopeId: z.string().min(1, { message: 'Envelope ID is required' }),
}); });

View File

@ -28,7 +28,7 @@ export const renderPdfToImage = async (pdfBytes: Uint8Array) => {
try { try {
const scale = 2; const scale = 2;
const pages = await Promise.all( const results = await Promise.allSettled(
Array.from({ length: pdf.numPages }, async (_, index) => { Array.from({ length: pdf.numPages }, async (_, index) => {
const pageNumber = index + 1; const pageNumber = index + 1;
const page = await pdf.getPage(pageNumber); const page = await pdf.getPage(pageNumber);
@ -54,6 +54,26 @@ export const renderPdfToImage = async (pdfBytes: Uint8Array) => {
}), }),
); );
const pages = results
.filter(
(
result,
): result is PromiseFulfilledResult<{
image: Buffer;
pageNumber: number;
width: number;
height: number;
}> => result.status === 'fulfilled',
)
.map((result) => result.value);
if (results.some((result) => result.status === 'rejected')) {
console.error(
'Some pages failed to render:',
results.filter((result) => result.status === 'rejected').map((result) => result.reason),
);
}
return pages; return pages;
} finally { } finally {
await pdf.destroy(); await pdf.destroy();

View File

@ -1,16 +1,35 @@
export type TDetectedFormField = { import { z } from 'zod';
boundingBox: number[];
label: export const ZDetectedFormFieldSchema = z.object({
| 'SIGNATURE' boundingBox: z
| 'INITIALS' .array(z.number().min(0).max(1000))
| 'NAME' .length(4)
| 'EMAIL' .describe('Bounding box [ymin, xmin, ymax, xmax] in normalized 0-1000 range'),
| 'DATE' label: z
| 'TEXT' .enum([
| 'NUMBER' 'SIGNATURE',
| 'RADIO' 'INITIALS',
| 'CHECKBOX' 'NAME',
| 'DROPDOWN'; 'EMAIL',
pageNumber: number; 'DATE',
recipientId: number; 'TEXT',
}; 'NUMBER',
'RADIO',
'CHECKBOX',
'DROPDOWN',
])
.describe('Documenso field type inferred from nearby label text or visual characteristics'),
pageNumber: z
.number()
.int()
.positive()
.describe('1-indexed page number where field was detected'),
recipientId: z
.number()
.int()
.describe(
'ID of the recipient (from the provided envelope recipients) who should own the field',
),
});
export type TDetectedFormField = z.infer<typeof ZDetectedFormFieldSchema>;