From fbb200c4ee0527e3ffd51fd830400444db6a08fa Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Feb 2025 14:16:32 -0500 Subject: [PATCH] Highlight diffs on validation changes --- inventory-server/package-lock.json | 211 +----------- inventory-server/package.json | 2 + inventory-server/src/routes/ai-validation.js | 20 +- inventory/package-lock.json | 10 + inventory/package.json | 1 + .../steps/ValidationStep/ValidationStep.tsx | 303 +++++++++++++----- package-lock.json | 8 + package.json | 1 + 8 files changed, 268 insertions(+), 288 deletions(-) diff --git a/inventory-server/package-lock.json b/inventory-server/package-lock.json index f73617e..972b969 100755 --- a/inventory-server/package-lock.json +++ b/inventory-server/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/diff": "^7.0.1", "bcrypt": "^5.1.1", "cors": "^2.8.5", "csv-parse": "^5.6.0", + "diff": "^7.0.0", "dotenv": "^16.4.7", "express": "^4.18.2", "multer": "^1.4.5-lts.1", @@ -23,8 +25,6 @@ "uuid": "^9.0.1" }, "devDependencies": { - "@types/express": "^4.17.21", - "@types/pg": "^8.11.2", "nodemon": "^3.0.2" } }, @@ -391,65 +391,10 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, + "node_modules/@types/diff": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz", + "integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==", "license": "MIT" }, "node_modules/@types/node": { @@ -471,117 +416,6 @@ "form-data": "^4.0.0" } }, - "node_modules/@types/pg": { - "version": "8.11.11", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", - "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^4.0.1" - } - }, - "node_modules/@types/pg/node_modules/pg-types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", - "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "dev": true, - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/pg/node_modules/postgres-array": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", - "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/pg/node_modules/postgres-bytea": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", - "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@types/pg/node_modules/postgres-date": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", - "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/pg/node_modules/postgres-interval": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", - "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1285,6 +1119,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -2755,13 +2598,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2953,16 +2789,6 @@ "node": ">=4.0.0" } }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", - "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, "node_modules/pg-pool": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", @@ -3285,13 +3111,6 @@ "node": ">=0.10.0" } }, - "node_modules/postgres-range": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", - "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "dev": true, - "license": "MIT" - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/inventory-server/package.json b/inventory-server/package.json index 5f74ad6..eeba2de 100755 --- a/inventory-server/package.json +++ b/inventory-server/package.json @@ -18,9 +18,11 @@ "author": "", "license": "ISC", "dependencies": { + "@types/diff": "^7.0.1", "bcrypt": "^5.1.1", "cors": "^2.8.5", "csv-parse": "^5.6.0", + "diff": "^7.0.0", "dotenv": "^16.4.7", "express": "^4.18.2", "multer": "^1.4.5-lts.1", diff --git a/inventory-server/src/routes/ai-validation.js b/inventory-server/src/routes/ai-validation.js index 58bddac..08f7174 100644 --- a/inventory-server/src/routes/ai-validation.js +++ b/inventory-server/src/routes/ai-validation.js @@ -770,12 +770,18 @@ router.post("/validate", async (req, res) => { // Compare original and corrected data if (aiResponse.correctedData) { console.log("📊 Changes summary:"); + + // Debug: Log the first product's fields + if (products.length > 0) { + console.log("🔍 First product fields:", Object.keys(products[0])); + } + products.forEach((original, index) => { const corrected = aiResponse.correctedData[index]; if (corrected) { const productChanges = { productIndex: index, - title: original.title || `Product ${index + 1}`, + title: original.name || original.title || `Product ${index + 1}`, changes: [] }; @@ -817,8 +823,7 @@ router.post("/validate", async (req, res) => { const endTime = new Date(); let performanceMetrics = { promptLength, - productCount: products.length, - processingTimeSeconds: (endTime - startTime) / 1000 + productCount: products.length }; try { @@ -833,14 +838,13 @@ router.post("/validate", async (req, res) => { // Insert performance data into the local PostgreSQL database await pool.query( `INSERT INTO ai_validation_performance - (prompt_length, product_count, start_time, end_time, duration_seconds) - VALUES ($1, $2, $3, $4, $5)`, + (prompt_length, product_count, start_time, end_time) + VALUES ($1, $2, $3, $4)`, [ promptLength, products.length, - startTime, - endTime, - (endTime - startTime) / 1000 + startTime.toISOString(), + endTime.toISOString() ] ); diff --git a/inventory/package-lock.json b/inventory/package-lock.json index d8da22b..7c678c6 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -65,6 +65,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", + "diff": "^7.0.0", "framer-motion": "^12.4.4", "js-levenshtein": "^1.1.6", "lodash": "^4.17.21", @@ -6502,6 +6503,15 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", diff --git a/inventory/package.json b/inventory/package.json index bd93c3d..822a045 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -67,6 +67,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", + "diff": "^7.0.0", "framer-motion": "^12.4.4", "js-levenshtein": "^1.1.6", "lodash": "^4.17.21", diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index 9505779..5f5214b 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -78,6 +78,7 @@ import type { GlobalSelections } from "../MatchColumnsStep/MatchColumnsStep" import { useQuery } from "@tanstack/react-query" import { Badge } from "@/components/ui/badge" import { CheckIcon } from "lucide-react" +import * as Diff from 'diff' // Template interface interface Template { @@ -2128,6 +2129,61 @@ export const ValidationStep = ({ return String(value); }; + // Function to highlight differences between two text values using the diff library + const highlightDifferences = (original: string | null | undefined, corrected: string | null | undefined): { originalHtml: string, correctedHtml: string } => { + // Handle null/undefined values + let originalStr = original === null || original === undefined ? '' : String(original); + let correctedStr = corrected === null || corrected === undefined ? '' : String(corrected); + + // If they're identical, return without highlighting + if (originalStr === correctedStr) { + return { + originalHtml: originalStr, + correctedHtml: correctedStr + }; + } + + const diff = Diff.diffWords(originalStr, correctedStr); + + let originalHtml = ''; + let correctedHtml = ''; + + diff.forEach((part: Diff.Change) => { + // Create escaped HTML + const escapedValue = part.value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + + if (part.added) { + // Added parts only show in the corrected version (green) + correctedHtml += `${escapedValue}`; + } else if (part.removed) { + // Removed parts only show in the original version (red) + originalHtml += `${escapedValue}`; + } else { + // Unchanged parts show in both versions + originalHtml += escapedValue; + correctedHtml += escapedValue; + } + }); + + return { + originalHtml, + correctedHtml + }; + }; + + // Function to get field display value with highlighted differences + const getFieldDisplayValueWithHighlight = (fieldKey: string, originalValue: any, correctedValue: any): { originalHtml: string, correctedHtml: string } => { + const originalDisplay = getFieldDisplayValue(fieldKey, originalValue); + const correctedDisplay = getFieldDisplayValue(fieldKey, correctedValue); + + return highlightDifferences(originalDisplay, correctedDisplay); + }; + // Add a function to revert a specific AI validation change const revertAiChange = (productIndex: number, fieldKey: string) => { // Ensure we have the original data @@ -2334,92 +2390,171 @@ export const ValidationStep = ({ {aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? (

Detailed Changes:

- {aiValidationDetails.changeDetails.map((product, i) => ( -
-

{product.title}

- - - - Field - Original Value - Corrected Value - Action - - - - {product.changes.map((change, j) => { - const field = fields.find(f => f.key === change.field); - const isReverted = isChangeReverted(product.productIndex, change.field); - - return ( - - - {field?.label || change.field} - {isReverted && ( - - Reverted - - )} - - -
-
- {getFieldDisplayValue(change.field, change.original)} -
- {/* Show raw value if it's an ID */} - {change.original && typeof change.original === 'string' && - !isNaN(Number(change.original)) && - getFieldDisplayValue(change.field, change.original) !== change.original && ( -
ID: {change.original}
- )} -
-
- -
-
- {getFieldDisplayValue(change.field, change.corrected)} -
- {/* Show raw value if it's an ID */} - {change.corrected && typeof change.corrected === 'string' && - !isNaN(Number(change.corrected)) && - getFieldDisplayValue(change.field, change.corrected) !== change.corrected && ( -
ID: {change.corrected}
- )} -
-
- -
- {isReverted ? ( -
+ + + Field + Original Value + Corrected Value + Action + + + + {product.changes.map((change: ChangeDetail, j: number) => { + const field = fields.find(f => f.key === change.field); + const isReverted = isChangeReverted(product.productIndex, change.field); + + return ( + + + {field?.label || change.field} + {isReverted && ( + Reverted - - ) : ( - + )} - - - - ); - })} - -
-
- ))} + + +
+ {isReverted ? ( +
+ {getFieldDisplayValue(change.field, change.original)} +
+ ) : ( +
+ )} + {/* Show raw value if it's an ID */} + {change.original && typeof change.original === 'string' && + !isNaN(Number(change.original)) && + getFieldDisplayValue(change.field, change.original) !== change.original && ( +
ID: {change.original}
+ )} +
+ + +
+ {isReverted ? ( +
+ {getFieldDisplayValue(change.field, change.corrected)} +
+ ) : ( +
+ )} + {/* Show raw value if it's an ID */} + {change.corrected && typeof change.corrected === 'string' && + !isNaN(Number(change.corrected)) && + getFieldDisplayValue(change.field, change.corrected) !== change.corrected && ( +
ID: {change.corrected}
+ )} +
+ + +
+ {isReverted ? ( + + ) : ( + + )} +
+
+ + ); + })} + + +
+ ); + })}
) : ( aiValidationDetails.changes.length > 0 && ( diff --git a/package-lock.json b/package-lock.json index b114ea5..631125b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,17 @@ "shadcn": "^1.0.0" }, "devDependencies": { + "@types/diff": "^7.0.1", "ts-essentials": "^10.0.4" } }, + "node_modules/@types/diff": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz", + "integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==", + "dev": true, + "license": "MIT" + }, "node_modules/shadcn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-1.0.0.tgz", diff --git a/package.json b/package.json index 65d0e40..171bbc7 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "shadcn": "^1.0.0" }, "devDependencies": { + "@types/diff": "^7.0.1", "ts-essentials": "^10.0.4" } }