diff --git a/src/api/Metadata.js b/src/api/Metadata.js index 8951aa8a35..a3272f460e 100644 --- a/src/api/Metadata.js +++ b/src/api/Metadata.js @@ -25,6 +25,7 @@ import { } from './utils'; import File from './File'; import { + AI_ACCEPTED_PROCESS, HEADER_CONTENT_TYPE, METADATA_SCOPE_ENTERPRISE, METADATA_SCOPE_GLOBAL, @@ -1121,6 +1122,7 @@ class Metadata extends File { * @param {Object} template - Metadata Redesign template * @param {Function} successCallback - Success callback * @param {Function} errorCallback - Error callback + * @param {boolean} isConfidenceScoreEnabled - whether to include confidence score details in the payload * @return {Promise} */ async createMetadataRedesign( @@ -1128,6 +1130,7 @@ class Metadata extends File { template: MetadataTemplateInstance, successCallback: Function, errorCallback: ElementsErrorCallback, + isConfidenceScoreEnabled: boolean = false, ): Promise { this.errorCode = ERROR_CODE_CREATE_METADATA; if (!file || !template) { @@ -1177,6 +1180,34 @@ class Metadata extends File { return acc; }, {}); + if (isConfidenceScoreEnabled) { + const details = template.fields.reduce((acc, field) => { + if (field.confidenceScore) { + const entry: { + confidenceScore: number, + confidenceLevel: string, + process?: string, + targetLocation?: string, + } = { + confidenceScore: field.confidenceScore.value, + confidenceLevel: field.confidenceScore.level, + }; + if (field.confidenceScore.isAccepted) { + entry.process = AI_ACCEPTED_PROCESS; + } + if (field.targetLocation) { + entry.targetLocation = JSON.stringify(field.targetLocation); + } + acc[field.key] = entry; + } + return acc; + }, {}); + + if (Object.keys(details).length > 0) { + fieldsValues.$details = details; + } + } + const metadata = await this.xhr.post({ url: this.getMetadataUrl(id, template.scope, template.templateKey), id: getTypedFileId(id), diff --git a/src/api/__tests__/Metadata.test.js b/src/api/__tests__/Metadata.test.js index 2ab45267cf..b130ef9887 100644 --- a/src/api/__tests__/Metadata.test.js +++ b/src/api/__tests__/Metadata.test.js @@ -2918,6 +2918,518 @@ describe('api/Metadata', () => { }); expect(metadata.errorHandler).toHaveBeenCalledWith(xhrError); }); + + test('should not include $details when isConfidenceScoreEnabled is false', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'invoiceDate', + type: 'string', + value: '2024-01-15', + confidenceScore: { value: 1, level: 'HIGH', isAccepted: false }, + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { invoiceDate: '2024-01-15' }, + }); + }); + + test('should include $details with confidence scores when isConfidenceScoreEnabled is true and fields have confidenceScore', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'invoiceDate', + type: 'string', + value: '2024-01-15', + confidenceScore: { value: 1, level: 'HIGH', isAccepted: false }, + }, + { + key: 'totalAmount', + type: 'float', + value: '100.50', + confidenceScore: { value: 0.85, level: 'MEDIUM', isAccepted: true }, + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + invoiceDate: '2024-01-15', + totalAmount: 100.5, + $details: { + invoiceDate: { + confidenceScore: 1, + confidenceLevel: 'HIGH', + }, + totalAmount: { + confidenceScore: 0.85, + confidenceLevel: 'MEDIUM', + process: 'AI_ACCEPTED', + }, + }, + }, + }); + }); + + test('should include process as AI_ACCEPTED only when isAccepted is true', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'vendorName', + type: 'string', + value: 'Acme Corp', + confidenceScore: { value: 0.95, level: 'HIGH', isAccepted: true }, + }, + { + key: 'invoiceNumber', + type: 'string', + value: 'INV-001', + confidenceScore: { value: 0.5, level: 'LOW', isAccepted: false }, + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + vendorName: 'Acme Corp', + invoiceNumber: 'INV-001', + $details: { + vendorName: { + confidenceScore: 0.95, + confidenceLevel: 'HIGH', + process: 'AI_ACCEPTED', + }, + invoiceNumber: { + confidenceScore: 0.5, + confidenceLevel: 'LOW', + }, + }, + }, + }); + }); + + test('should not include $details when isConfidenceScoreEnabled is true but no fields have confidenceScore', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'invoiceDate', + type: 'string', + value: '2024-01-15', + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { invoiceDate: '2024-01-15' }, + }); + }); + + test('should include stringified targetLocation in $details when field has targetLocation', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const targetLocation = [ + { + itemId: '123', + page: 1, + text: 'California', + boundingBox: { left: 0.1, top: 0.2, right: 0.3, bottom: 0.4 }, + }, + ]; + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'state', + type: 'string', + value: 'California', + confidenceScore: { value: 0.9, level: 'HIGH', isAccepted: false }, + targetLocation, + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + state: 'California', + $details: { + state: { + confidenceScore: 0.9, + confidenceLevel: 'HIGH', + targetLocation: JSON.stringify(targetLocation), + }, + }, + }, + }); + }); + + test('should include targetLocation with process when isAccepted is true', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const targetLocation = [ + { + itemId: '456', + page: 2, + text: '$500.00', + boundingBox: { left: 0.5, top: 0.6, right: 0.7, bottom: 0.8 }, + }, + ]; + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'totalAmount', + type: 'float', + value: '500', + confidenceScore: { value: 0.95, level: 'HIGH', isAccepted: true }, + targetLocation, + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + totalAmount: 500, + $details: { + totalAmount: { + confidenceScore: 0.95, + confidenceLevel: 'HIGH', + process: 'AI_ACCEPTED', + targetLocation: JSON.stringify(targetLocation), + }, + }, + }, + }); + }); + + test('should not include targetLocation in $details when field has no targetLocation', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'invoiceDate', + type: 'string', + value: '2024-01-15', + confidenceScore: { value: 1, level: 'HIGH', isAccepted: false }, + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + invoiceDate: '2024-01-15', + $details: { + invoiceDate: { + confidenceScore: 1, + confidenceLevel: 'HIGH', + }, + }, + }, + }); + }); + + test('should handle mix of fields with and without targetLocation', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const targetLocation = [ + { + itemId: '789', + page: 1, + text: 'Acme Corp', + boundingBox: { left: 0.1, top: 0.1, right: 0.5, bottom: 0.2 }, + }, + ]; + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'vendorName', + type: 'string', + value: 'Acme Corp', + confidenceScore: { value: 0.99, level: 'HIGH', isAccepted: true }, + targetLocation, + }, + { + key: 'invoiceDate', + type: 'string', + value: '2024-06-01', + confidenceScore: { value: 0.7, level: 'MEDIUM', isAccepted: false }, + }, + { + key: 'notes', + type: 'string', + value: 'Some notes', + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + vendorName: 'Acme Corp', + invoiceDate: '2024-06-01', + notes: 'Some notes', + $details: { + vendorName: { + confidenceScore: 0.99, + confidenceLevel: 'HIGH', + process: 'AI_ACCEPTED', + targetLocation: JSON.stringify(targetLocation), + }, + invoiceDate: { + confidenceScore: 0.7, + confidenceLevel: 'MEDIUM', + }, + }, + }, + }); + }); + + test('should only add $details for fields that have confidenceScore, skipping those without', async () => { + const success = jest.fn(); + const error = jest.fn(); + const file = { + id: 'id', + permissions: { + can_upload: true, + }, + }; + const cache = new Cache(); + const template = { + scope: 'scope', + templateKey: 'templateKey', + fields: [ + { + key: 'invoiceDate', + type: 'string', + value: '2024-01-15', + confidenceScore: { value: 1, level: 'HIGH', isAccepted: false }, + }, + { + key: 'notes', + type: 'string', + value: 'Some notes', + }, + ], + }; + + cache.set('metadata_id', { templateInstances: [] }); + + metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('url'); + metadata.xhr.post = jest.fn().mockReturnValueOnce({ data: { $type: 'type' } }); + metadata.isDestroyed = jest.fn().mockReturnValueOnce(false); + metadata.getCache = jest.fn().mockReturnValueOnce(cache); + metadata.getMetadataCacheKey = jest.fn().mockReturnValueOnce('metadata_id'); + metadata.successHandler = jest.fn(); + metadata.errorHandler = jest.fn(); + + await metadata.createMetadataRedesign(file, template, success, error, true); + + expect(metadata.xhr.post).toHaveBeenCalledWith({ + url: 'url', + id: 'file_id', + data: { + invoiceDate: '2024-01-15', + notes: 'Some notes', + $details: { + invoiceDate: { + confidenceScore: 1, + confidenceLevel: 'HIGH', + }, + }, + }, + }); + }); }); describe('deleteMetadata()', () => { diff --git a/src/constants.js b/src/constants.js index f689dab270..20f15a444f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -444,6 +444,7 @@ export const SIDEBAR_VIEW_BOXAI: 'boxai' = 'boxai'; export const SIDEBAR_VIEW_ACTIVITY: 'activity' = 'activity'; export const SIDEBAR_VIEW_VERSIONS: 'versions' = 'versions'; export const SIDEBAR_VIEW_DOCGEN: 'docgen' = 'docgen'; +export const AI_ACCEPTED_PROCESS: 'AI_ACCEPTED' = 'AI_ACCEPTED'; /* ------------------ HTTP Requests ---------------------- */ export const HTTP_GET: 'GET' = 'GET'; diff --git a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts index 030a5e2dcc..710b94448e 100644 --- a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts +++ b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts @@ -190,9 +190,10 @@ function useSidebarMetadataFetcher( }, (error: ElementsXhrError, code: string) => onApiError(error, code, messages.sidebarMetadataEditingErrorContent), + isConfidenceScoreEnabled, ); }, - [api, file, onApiError, onSuccess], + [api, file, onApiError, onSuccess, isConfidenceScoreEnabled], ); const handleUpdateMetadataInstance = React.useCallback(