mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
chore: review
This commit is contained in:
@ -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) {
|
||||||
|
|||||||
@ -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' }),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user