From 94098bd7629160d63391d3585e7a6504c524fdc1 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 29 Oct 2025 18:14:16 +0000 Subject: [PATCH] feat: generates coordinates for bounding boxes --- .env.example | 4 + apps/remix/package.json | 6 +- apps/remix/server/api/ai.ts | 373 +++++++++++++++++++++++++++++ apps/remix/server/api/ai.types.ts | 56 +++++ apps/remix/server/router.ts | 4 + package-lock.json | 211 +++++++++++++++- package.json | 6 +- packages/api/package.json | 2 +- packages/auth/package.json | 2 +- packages/ee/package.json | 2 +- packages/lib/package.json | 2 +- packages/trpc/package.json | 2 +- packages/tsconfig/process-env.d.ts | 5 + packages/ui/package.json | 2 +- 14 files changed, 656 insertions(+), 21 deletions(-) create mode 100644 apps/remix/server/api/ai.ts create mode 100644 apps/remix/server/api/ai.types.ts diff --git a/.env.example b/.env.example index 980792eb6..5748ec493 100644 --- a/.env.example +++ b/.env.example @@ -135,6 +135,10 @@ NEXT_PUBLIC_FEATURE_BILLING_ENABLED= # OPTIONAL: Leave blank to allow users to signup through /signup page. NEXT_PUBLIC_DISABLE_SIGNUP= +# [[AI]] +# OPTIONAL: API key for Google Generative AI (Gemini). Get your key from https://ai.google.dev +GOOGLE_GENERATIVE_AI_API_KEY="" + # [[E2E Tests]] E2E_TEST_AUTHENTICATE_USERNAME="Test User" E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com" diff --git a/apps/remix/package.json b/apps/remix/package.json index 7e3c00771..d281637f4 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -14,6 +14,8 @@ "with:env": "dotenv -e ../../.env -e ../../.env.local --" }, "dependencies": { + "@ai-sdk/google": "^2.0.25", + "@ai-sdk/react": "^2.0.82", "@cantoo/pdf-lib": "^2.5.2", "@documenso/api": "*", "@documenso/assets": "*", @@ -38,6 +40,7 @@ "@react-router/serve": "^7.6.0", "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.3", + "ai": "^5.0.82", "autoprefixer": "^10.4.13", "colord": "^2.9.3", "framer-motion": "^10.12.8", @@ -68,6 +71,7 @@ "remix-themes": "^2.0.4", "satori": "^0.12.1", "sharp": "0.32.6", + "skia-canvas": "^3.0.8", "tailwindcss": "^3.4.15", "ts-pattern": "^5.0.5", "ua-parser-js": "^1.0.37", @@ -104,4 +108,4 @@ "vite-tsconfig-paths": "^5.1.4" }, "version": "1.13.1" -} \ No newline at end of file +} diff --git a/apps/remix/server/api/ai.ts b/apps/remix/server/api/ai.ts new file mode 100644 index 000000000..4929a71e3 --- /dev/null +++ b/apps/remix/server/api/ai.ts @@ -0,0 +1,373 @@ +import { google } from '@ai-sdk/google'; +import { sValidator } from '@hono/standard-validator'; +import { generateObject, generateText } from 'ai'; +import { readFile, writeFile } from 'fs/promises'; +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { join } from 'path'; +import sharp from 'sharp'; +import { Canvas, Image } from 'skia-canvas'; + +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; + +import type { HonoEnv } from '../router'; +import { + type TDetectObjectsAndDrawResponse, + type TDetectObjectsResponse, + type TGenerateTextResponse, + ZDetectObjectsAndDrawRequestSchema, + ZDetectObjectsRequestSchema, + ZDetectObjectsResponseSchema, + ZGenerateTextRequestSchema, +} from './ai.types'; + +/** + * Resize and compress image for better Gemini API accuracy. + * Resizes to max width of 1000px (maintaining aspect ratio) and compresses to JPEG at 70% quality. + * This preprocessing improves bounding box detection accuracy. + */ +async function resizeAndCompressImage(imageBuffer: Buffer): Promise { + const metadata = await sharp(imageBuffer).metadata(); + const originalWidth = metadata.width || 0; + + if (originalWidth > 1000) { + return await sharp(imageBuffer) + .resize({ width: 1000, withoutEnlargement: true }) + .jpeg({ quality: 70 }) + .toBuffer(); + } + + return await sharp(imageBuffer).jpeg({ quality: 70 }).toBuffer(); +} + +export const aiRoute = new Hono() + .use( + '*', + cors({ + origin: 'http://localhost:3000', + allowMethods: ['POST', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + credentials: true, + }), + ) + + .post('/generate', sValidator('json', ZGenerateTextRequestSchema), async (c) => { + try { + await getSession(c.req.raw); + + const { prompt } = c.req.valid('json'); + + const result = await generateText({ + model: google('gemini-2.0-flash-exp'), + prompt, + }); + + return c.json({ text: result.text }); + } catch (error) { + console.error('AI generation failed:', error); + + if (error instanceof AppError) { + throw error; + } + + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Failed to generate text', + userMessage: 'An error occurred while generating the text. Please try again.', + }); + } + }) + + .post('/detect-objects', sValidator('json', ZDetectObjectsRequestSchema), async (c) => { + try { + await getSession(c.req.raw); + + const { imagePath } = c.req.valid('json'); + + const imageBuffer = await readFile(imagePath); + const compressedImageBuffer = await resizeAndCompressImage(imageBuffer); + const base64Image = compressedImageBuffer.toString('base64'); + + const result = await generateObject({ + model: google('gemini-2.5-pro'), + schema: ZDetectObjectsResponseSchema, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + image: `data:image/jpeg;base64,${base64Image}`, + }, + { + type: 'text', + text: `You are analyzing a form document image to detect fillable fields for the Documenso document signing platform. + +IMPORTANT RULES: +1. Only detect EMPTY/UNFILLED fields (ignore boxes that already contain text or data) +2. Analyze nearby text labels to determine the field type +3. Return bounding boxes for the fillable area only, NOT the label text +4. Each bounding box must be in the format [ymin, xmin, ymax, xmax] where all coordinates are NORMALIZED to a 0-1000 scale + +FIELD TYPES TO DETECT: +• SIGNATURE - Signature lines, boxes labeled 'Signature', 'Sign here', 'Authorized signature', 'X____' +• INITIALS - Small boxes labeled 'Initials', 'Initial here', typically smaller than signature fields +• NAME - Boxes labeled 'Name', 'Full name', 'Your name', 'Print name', 'Printed name' +• EMAIL - Boxes labeled 'Email', 'Email address', 'E-mail', 'Email:' +• DATE - Boxes labeled 'Date', 'Date signed', "Today's date", or showing date format placeholders like 'MM/DD/YYYY', '__/__/____' +• CHECKBOX - Empty checkbox squares (☐) with or without labels, typically small square boxes +• RADIO - Empty radio button circles (○) in groups, typically circular selection options +• NUMBER - Boxes labeled with numeric context: 'Amount', 'Quantity', 'Phone', 'Phone number', 'ZIP', 'ZIP code', 'Age', 'Price', '#' +• DROPDOWN - Boxes with dropdown indicators (▼, ↓) or labeled 'Select', 'Choose', 'Please select' +• TEXT - Any other empty text input boxes, general input fields, unlabeled boxes, or when field type is uncertain + +DETECTION GUIDELINES: +- Read text located near the box (above, to the left, or inside the box boundary) to infer the field type +- If you're uncertain which type fits best, default to TEXT +- For checkboxes and radio buttons: Detect each individual box/circle separately, not the label +- Signature fields are often longer horizontal lines or larger boxes +- Date fields often show format hints or date separators (slashes, dashes) +- Look for visual patterns: underscores (____), horizontal lines, box outlines +- Return coordinates for the fillable area, not the descriptive label text + +COORDINATE SYSTEM: +- [ymin, xmin, ymax, xmax] normalized to 0-1000 scale +- Top-left corner: ymin and xmin close to 0 +- Bottom-right corner: ymax and xmax close to 1000 +- Coordinates represent positions on a 1000x1000 grid overlaid on the image`, + }, + ], + }, + ], + }); + + return c.json(result.object); + } catch (error) { + console.error('Object detection failed:', error); + + if (error instanceof AppError) { + throw error; + } + + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Failed to detect objects', + userMessage: 'An error occurred while detecting objects. Please try again.', + }); + } + }) + + .post( + '/detect-object-and-draw', + sValidator('json', ZDetectObjectsAndDrawRequestSchema), + async (c) => { + try { + await getSession(c.req.raw); + + const { imagePath } = c.req.valid('json'); + + console.log(`[detect-object-and-draw] Reading image from: ${imagePath}`); + + const imageBuffer = await readFile(imagePath); + const metadata = await sharp(imageBuffer).metadata(); + const imageWidth = metadata.width; + const imageHeight = metadata.height; + + console.log( + `[detect-object-and-draw] Original image dimensions: ${imageWidth}x${imageHeight}`, + ); + + if (!imageWidth || !imageHeight) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Unable to extract image dimensions', + userMessage: 'The image file appears to be invalid or corrupted.', + }); + } + + console.log('[detect-object-and-draw] Compressing image for Gemini API...'); + const compressedImageBuffer = await resizeAndCompressImage(imageBuffer); + const base64Image = compressedImageBuffer.toString('base64'); + + console.log('[detect-object-and-draw] Calling Gemini API for form field detection...'); + const result = await generateObject({ + model: google('gemini-2.5-pro'), + schema: ZDetectObjectsResponseSchema, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + image: `data:image/jpeg;base64,${base64Image}`, + }, + { + type: 'text', + text: `You are analyzing a form document image to detect fillable fields for the Documenso document signing platform. + +IMPORTANT RULES: +1. Only detect EMPTY/UNFILLED fields (ignore boxes that already contain text or data) +2. Analyze nearby text labels to determine the field type +3. Return bounding boxes for the fillable area only, NOT the label text +4. Each bounding box must be in the format [ymin, xmin, ymax, xmax] where all coordinates are NORMALIZED to a 0-1000 scale + +FIELD TYPES TO DETECT: +• SIGNATURE - Signature lines, boxes labeled 'Signature', 'Sign here', 'Authorized signature', 'X____' +• INITIALS - Small boxes labeled 'Initials', 'Initial here', typically smaller than signature fields +• NAME - Boxes labeled 'Name', 'Full name', 'Your name', 'Print name', 'Printed name' +• EMAIL - Boxes labeled 'Email', 'Email address', 'E-mail', 'Email:' +• DATE - Boxes labeled 'Date', 'Date signed', "Today's date", or showing date format placeholders like 'MM/DD/YYYY', '__/__/____' +• CHECKBOX - Empty checkbox squares (☐) with or without labels, typically small square boxes +• RADIO - Empty radio button circles (○) in groups, typically circular selection options +• NUMBER - Boxes labeled with numeric context: 'Amount', 'Quantity', 'Phone', 'Phone number', 'ZIP', 'ZIP code', 'Age', 'Price', '#' +• DROPDOWN - Boxes with dropdown indicators (▼, ↓) or labeled 'Select', 'Choose', 'Please select' +• TEXT - Any other empty text input boxes, general input fields, unlabeled boxes, or when field type is uncertain + +DETECTION GUIDELINES: +- Read text located near the box (above, to the left, or inside the box boundary) to infer the field type +- If you're uncertain which type fits best, default to TEXT +- For checkboxes and radio buttons: Detect each individual box/circle separately, not the label +- Signature fields are often longer horizontal lines or larger boxes +- Date fields often show format hints or date separators (slashes, dashes) +- Look for visual patterns: underscores (____), horizontal lines, box outlines +- Return coordinates for the fillable area, not the descriptive label text + +COORDINATE SYSTEM: +- [ymin, xmin, ymax, xmax] normalized to 0-1000 scale +- Top-left corner: ymin and xmin close to 0 +- Bottom-right corner: ymax and xmax close to 1000 +- Coordinates represent positions on a 1000x1000 grid overlaid on the image`, + }, + ], + }, + ], + }); + console.log('[detect-object-and-draw] Gemini API call completed'); + + const detectedObjects = result.object; + + console.log( + `[detect-object-and-draw] Detected ${detectedObjects.length} objects, starting to draw...`, + ); + + const padding = { left: 80, top: 20, right: 20, bottom: 40 }; + const canvas = new Canvas( + imageWidth + padding.left + padding.right, + imageHeight + padding.top + padding.bottom, + ); + const ctx = canvas.getContext('2d'); + + const img = new Image(); + img.src = imageBuffer; + ctx.drawImage(img, padding.left, padding.top); + + ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; + ctx.lineWidth = 1; + + for (let i = 0; i <= 1000; i += 100) { + const x = padding.left + (i / 1000) * imageWidth; + ctx.beginPath(); + ctx.moveTo(x, padding.top); + ctx.lineTo(x, imageHeight + padding.top); + ctx.stroke(); + } + + // Horizontal grid lines (every 100 units on 0-1000 scale) + for (let i = 0; i <= 1000; i += 100) { + const y = padding.top + (i / 1000) * imageHeight; + ctx.beginPath(); + ctx.moveTo(padding.left, y); + ctx.lineTo(imageWidth + padding.left, y); + ctx.stroke(); + } + + const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF']; + + detectedObjects.forEach((obj, index) => { + const [ymin, xmin, ymax, xmax] = obj.box_2d.map((coord) => coord / 1000); + + const x = xmin * imageWidth + padding.left; + const y = ymin * imageHeight + padding.top; + const width = (xmax - xmin) * imageWidth; + const height = (ymax - ymin) * imageHeight; + + ctx.strokeStyle = colors[index % colors.length]; + ctx.lineWidth = 5; + ctx.strokeRect(x, y, width, height); + + ctx.fillStyle = colors[index % colors.length]; + ctx.font = '20px Arial'; + ctx.fillText(obj.label, x, y - 5); + }); + + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 1; + ctx.font = '26px Arial'; + + ctx.beginPath(); + ctx.moveTo(padding.left, padding.top); + ctx.lineTo(padding.left, imageHeight + padding.top); + ctx.stroke(); + + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + for (let i = 0; i <= 1000; i += 100) { + const y = padding.top + (i / 1000) * imageHeight; + ctx.fillStyle = '#000000'; + ctx.fillText(i.toString(), padding.left - 5, y); + + ctx.beginPath(); + ctx.moveTo(padding.left - 5, y); + ctx.lineTo(padding.left, y); + ctx.stroke(); + } + + ctx.beginPath(); + ctx.moveTo(padding.left, imageHeight + padding.top); + ctx.lineTo(imageWidth + padding.left, imageHeight + padding.top); + ctx.stroke(); + + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + for (let i = 0; i <= 1000; i += 100) { + const x = padding.left + (i / 1000) * imageWidth; + ctx.fillStyle = '#000000'; + ctx.fillText(i.toString(), x, imageHeight + padding.top + 5); + + ctx.beginPath(); + ctx.moveTo(x, imageHeight + padding.top); + ctx.lineTo(x, imageHeight + padding.top + 5); + ctx.stroke(); + } + + const now = new Date(); + const timestamp = now + .toISOString() + .replace(/[-:]/g, '') + .replace(/\..+/, '') + .replace('T', '_'); + const outputFilename = `detected_objects_${timestamp}.png`; + const outputPath = join(process.cwd(), outputFilename); + + console.log('[detect-object-and-draw] Converting canvas to PNG buffer...'); + const pngBuffer = await canvas.toBuffer('png'); + console.log(`[detect-object-and-draw] Saving to: ${outputPath}`); + await writeFile(outputPath, pngBuffer); + + console.log('[detect-object-and-draw] Image saved successfully!'); + return c.json({ + outputPath, + detectedObjects, + }); + } catch (error) { + console.error('Object detection and drawing failed:', error); + + if (error instanceof AppError) { + throw error; + } + + throw new AppError(AppErrorCode.UNKNOWN_ERROR, { + message: 'Failed to detect objects and draw', + userMessage: 'An error occurred while detecting and drawing objects. Please try again.', + }); + } + }, + ); diff --git a/apps/remix/server/api/ai.types.ts b/apps/remix/server/api/ai.types.ts new file mode 100644 index 000000000..1293d6c10 --- /dev/null +++ b/apps/remix/server/api/ai.types.ts @@ -0,0 +1,56 @@ +import { z } from 'zod'; + +export const ZGenerateTextRequestSchema = z.object({ + prompt: z.string().min(1, 'Prompt is required').max(5000, 'Prompt is too long'), +}); + +export const ZGenerateTextResponseSchema = z.object({ + text: z.string(), +}); + +export type TGenerateTextRequest = z.infer; +export type TGenerateTextResponse = z.infer; + +export const ZDetectedObjectSchema = z.object({ + box_2d: 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'), +}); + +export const ZDetectObjectsRequestSchema = z.object({ + imagePath: z.string().min(1, 'Image path is required'), + // TODO: Replace with file upload - reference files.ts pattern +}); + +export const ZDetectObjectsResponseSchema = z.array(ZDetectedObjectSchema); + +export type TDetectedObject = z.infer; +export type TDetectObjectsRequest = z.infer; +export type TDetectObjectsResponse = z.infer; + +export const ZDetectObjectsAndDrawRequestSchema = z.object({ + imagePath: z.string().min(1, 'Image path is required'), +}); + +export const ZDetectObjectsAndDrawResponseSchema = z.object({ + outputPath: z.string().describe('Path to the generated image with bounding boxes'), + detectedObjects: z.array(ZDetectedObjectSchema).describe('Array of detected objects'), +}); + +export type TDetectObjectsAndDrawRequest = z.infer; +export type TDetectObjectsAndDrawResponse = z.infer; diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts index 5eb448933..7a6863753 100644 --- a/apps/remix/server/router.ts +++ b/apps/remix/server/router.ts @@ -14,6 +14,7 @@ import { getIpAddress } from '@documenso/lib/universal/get-ip-address'; import { logger } from '@documenso/lib/utils/logger'; import { openApiDocument } from '@documenso/trpc/server/open-api'; +import { aiRoute } from './api/ai'; import { filesRoute } from './api/files'; import { type AppContext, appContext } from './context'; import { appMiddleware } from './middleware'; @@ -83,6 +84,9 @@ app.route('/api/auth', auth); // Files route. app.route('/api/files', filesRoute); +// AI route. +app.route('/api/ai', aiRoute); + // API servers. app.use(`/api/v1/*`, cors()); app.route('/api/v1', tsRestHonoApp); diff --git a/package-lock.json b/package-lock.json index ab1b56a2b..49895c28f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,13 @@ "@documenso/prisma": "^0.0.0", "@lingui/conf": "^5.2.0", "@lingui/core": "^5.2.0", + "ai": "^5.0.82", "inngest-cli": "^0.29.1", "luxon": "^3.5.0", "mupdf": "^1.0.0", "react": "^18", "typescript": "5.6.2", - "zod": "3.24.1" + "zod": "3.25.76" }, "devDependencies": { "@commitlint/cli": "^17.7.1", @@ -91,6 +92,8 @@ "name": "@documenso/remix", "version": "1.13.1", "dependencies": { + "@ai-sdk/google": "^2.0.25", + "@ai-sdk/react": "^2.0.82", "@cantoo/pdf-lib": "^2.5.2", "@documenso/api": "*", "@documenso/assets": "*", @@ -115,6 +118,7 @@ "@react-router/serve": "^7.6.0", "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.3", + "ai": "^5.0.82", "autoprefixer": "^10.4.13", "colord": "^2.9.3", "framer-motion": "^10.12.8", @@ -145,6 +149,7 @@ "remix-themes": "^2.0.4", "satori": "^0.12.1", "sharp": "0.32.6", + "skia-canvas": "^3.0.8", "tailwindcss": "^3.4.15", "ts-pattern": "^5.0.5", "ua-parser-js": "^1.0.37", @@ -187,6 +192,92 @@ "integrity": "sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==", "license": "MIT" }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.3.tgz", + "integrity": "sha512-/vCoMKtod+A74/BbkWsaAflWKz1ovhX5lmJpIaXQXtd6gyexZncjotBTbFM8rVJT9LKJ/Kx7iVVo3vh+KT+IJg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.14", + "@vercel/oidc": "3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/google": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.25.tgz", + "integrity": "sha512-tH2rA3428jnY6COoPfKB/BoQMs57sv9t+PEdyIB9ePtlV9dnVUbfKcdKoEcAaVffNZ6pzk8otrQYnu67pyn8TQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.14" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.14.tgz", + "integrity": "sha512-CYRU6L7IlR7KslSBVxvlqlybQvXJln/PI57O8swhOaDIURZbjRP2AY3igKgUsrmWqqnFFUHP+AwTN8xqJAknnA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.82", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.82.tgz", + "integrity": "sha512-InaGqykKGFq/XA6Vhh2Hyy38nzeMpqp8eWxjTNEQA5Gwcal0BVNuZyTbTIL5t5VNXV+pQPDhe9ak1+mc9qxjog==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.14", + "ai": "5.0.82", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -11178,6 +11269,12 @@ "node": ">=18.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@swagger-api/apidom-ast": { "version": "1.0.0-beta.39", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.39.tgz", @@ -11866,6 +11963,15 @@ "zod": "3.22.4" } }, + "node_modules/@team-plain/typescript-sdk/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@theguild/remark-mermaid": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz", @@ -13130,6 +13236,15 @@ "win32" ] }, + "node_modules/@vercel/oidc": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", + "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@vvo/tzdb": { "version": "6.161.0", "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.161.0.tgz", @@ -13271,6 +13386,33 @@ "node": ">=8" } }, + "node_modules/ai": { + "version": "5.0.82", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.82.tgz", + "integrity": "sha512-wmZZfsU40qB77umrcj3YzMSk6cUP5gxLXZDPfiSQLBLegTVXPUdSJC603tR7JB5JkhBDzN5VLaliuRKQGKpUXg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.3", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.14", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ai/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -18595,6 +18737,15 @@ "dev": true, "license": "MIT" }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -21307,6 +21458,15 @@ "node": ">=6" } }, + "node_modules/inngest/node_modules/zod": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.5.tgz", + "integrity": "sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -22405,6 +22565,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -32722,6 +32888,19 @@ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwind-merge": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", @@ -33059,6 +33238,18 @@ "real-require": "^0.2.0" } }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -36088,9 +36279,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -36163,7 +36354,7 @@ "superjson": "^1.13.1", "swagger-ui-react": "^5.21.0", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } }, "packages/api/node_modules/ts-pattern": { @@ -36384,7 +36575,7 @@ "luxon": "^3.5.0", "nanoid": "^5.1.5", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } }, "packages/auth/node_modules/ts-pattern": { @@ -36404,7 +36595,7 @@ "micro": "^10.0.1", "react": "^18", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } }, "packages/ee/node_modules/ts-pattern": { @@ -36734,7 +36925,7 @@ "skia-canvas": "^3.0.8", "stripe": "^12.7.0", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" }, "devDependencies": { "@playwright/browser-chromium": "1.52.0", @@ -37164,7 +37355,7 @@ "superjson": "^1.13.1", "trpc-to-openapi": "2.0.4", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } }, "packages/trpc/node_modules/ts-pattern": { @@ -37235,7 +37426,7 @@ "tailwind-merge": "^1.12.0", "tailwindcss-animate": "^1.0.5", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" }, "devDependencies": { "@documenso/tailwind-config": "*", diff --git a/package.json b/package.json index 1d75e7239..d85cdee6f 100644 --- a/package.json +++ b/package.json @@ -71,15 +71,13 @@ "@documenso/prisma": "^0.0.0", "@lingui/conf": "^5.2.0", "@lingui/core": "^5.2.0", + "ai": "^5.0.82", "inngest-cli": "^0.29.1", "luxon": "^3.5.0", "mupdf": "^1.0.0", "react": "^18", "typescript": "5.6.2", - "zod": "3.24.1" - }, - "overrides": { - "zod": "3.24.1" + "zod": "3.25.76" }, "trigger.dev": { "endpointId": "documenso-app" diff --git a/packages/api/package.json b/packages/api/package.json index 437198247..453f4df7b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,6 +25,6 @@ "superjson": "^1.13.1", "swagger-ui-react": "^5.21.0", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } } \ No newline at end of file diff --git a/packages/auth/package.json b/packages/auth/package.json index 41090db70..a1a5baedb 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -20,6 +20,6 @@ "luxon": "^3.5.0", "nanoid": "^5.1.5", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } } \ No newline at end of file diff --git a/packages/ee/package.json b/packages/ee/package.json index 4dc669053..c5acdaa17 100644 --- a/packages/ee/package.json +++ b/packages/ee/package.json @@ -19,6 +19,6 @@ "micro": "^10.0.1", "react": "^18", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } } \ No newline at end of file diff --git a/packages/lib/package.json b/packages/lib/package.json index eaab3057b..1c73b23fb 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -55,7 +55,7 @@ "skia-canvas": "^3.0.8", "stripe": "^12.7.0", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" }, "devDependencies": { "@playwright/browser-chromium": "1.52.0", diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 891c0ab00..7875a8133 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -21,6 +21,6 @@ "superjson": "^1.13.1", "trpc-to-openapi": "2.0.4", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } } diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index d4c23cd18..e840b6dee 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -80,6 +80,11 @@ declare namespace NodeJS { NEXT_PRIVATE_INNGEST_APP_ID?: string; NEXT_PRIVATE_INNGEST_EVENT_KEY?: string; + /** + * Google Generative AI (Gemini) + */ + GOOGLE_GENERATIVE_AI_API_KEY?: string; + POSTGRES_URL?: string; DATABASE_URL?: string; POSTGRES_PRISMA_URL?: string; diff --git a/packages/ui/package.json b/packages/ui/package.json index 880308853..c0ee55b42 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -78,6 +78,6 @@ "tailwind-merge": "^1.12.0", "tailwindcss-animate": "^1.0.5", "ts-pattern": "^5.0.5", - "zod": "3.24.1" + "zod": "3.25.76" } } \ No newline at end of file