Highlight diffs on validation changes
This commit is contained in:
10
inventory/package-lock.json
generated
10
inventory/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = <T extends string>({
|
||||
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, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
if (part.added) {
|
||||
// Added parts only show in the corrected version (green)
|
||||
correctedHtml += `<span class="text-green-600 font-medium">${escapedValue}</span>`;
|
||||
} else if (part.removed) {
|
||||
// Removed parts only show in the original version (red)
|
||||
originalHtml += `<span class="text-red-600 font-medium">${escapedValue}</span>`;
|
||||
} 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 = <T extends string>({
|
||||
{aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? (
|
||||
<div className="mb-6 space-y-6">
|
||||
<h3 className="font-semibold text-lg">Detailed Changes:</h3>
|
||||
{aiValidationDetails.changeDetails.map((product, i) => (
|
||||
<div key={i} className="border rounded-md p-4">
|
||||
<h4 className="font-medium text-base mb-3">{product.title}</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/4">Field</TableHead>
|
||||
<TableHead className="w-3/8">Original Value</TableHead>
|
||||
<TableHead className="w-3/8">Corrected Value</TableHead>
|
||||
<TableHead className="w-1/8 text-right">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{product.changes.map((change, j) => {
|
||||
const field = fields.find(f => f.key === change.field);
|
||||
const isReverted = isChangeReverted(product.productIndex, change.field);
|
||||
|
||||
return (
|
||||
<TableRow key={j} className={isReverted ? "bg-muted/30" : ""}>
|
||||
<TableCell className="font-medium">
|
||||
{field?.label || change.field}
|
||||
{isReverted && (
|
||||
<Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
|
||||
Reverted
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className={isReverted ? "font-medium" : ""}>
|
||||
{getFieldDisplayValue(change.field, change.original)}
|
||||
</div>
|
||||
{/* 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 && (
|
||||
<div className="text-xs text-muted-foreground">ID: {change.original}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className={isReverted ? "line-through text-muted-foreground" : ""}>
|
||||
{getFieldDisplayValue(change.field, change.corrected)}
|
||||
</div>
|
||||
{/* 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 && (
|
||||
<div className="text-xs text-muted-foreground">ID: {change.corrected}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="mt-2">
|
||||
{isReverted ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
|
||||
disabled
|
||||
>
|
||||
<CheckIcon className="w-4 h-4 mr-1" />
|
||||
{aiValidationDetails.changeDetails.map((product, i) => {
|
||||
// Find the title change if it exists
|
||||
const titleChange = product.changes.find(change => change.field === 'title');
|
||||
|
||||
// Get the best title to display:
|
||||
// 1. Use corrected title if available
|
||||
// 2. Use original title if available
|
||||
// 3. Try to find a descriptive field like SKU, barcode, or description
|
||||
// 4. Fall back to "Product X" only as a last resort
|
||||
let displayTitle = "Product";
|
||||
|
||||
// First check for a name field, which is commonly used
|
||||
const nameChange = product.changes.find(change =>
|
||||
change.field.toLowerCase() === 'name' ||
|
||||
change.field.toLowerCase() === 'title');
|
||||
|
||||
// Debug: Log the product changes to see what fields are available
|
||||
console.log(`Product ${i} changes:`, product.changes.map(c => c.field));
|
||||
console.log(`Product ${i} title:`, product.title);
|
||||
|
||||
// First use the product title directly if it's available and not a generic "Product X"
|
||||
if (product.title && !product.title.startsWith('Product ')) {
|
||||
displayTitle = product.title;
|
||||
} else if (nameChange && nameChange.corrected) {
|
||||
// Next option: Use the corrected name/title
|
||||
displayTitle = String(nameChange.corrected);
|
||||
} else if (nameChange && nameChange.original) {
|
||||
// Next option: Use the original name/title
|
||||
displayTitle = String(nameChange.original);
|
||||
} else {
|
||||
// Try to find another identifying field
|
||||
const identifyingFields = ['sku', 'barcode', 'upc', 'description', 'shortdescription'];
|
||||
for (const fieldName of identifyingFields) {
|
||||
const fieldChange = product.changes.find(change =>
|
||||
change.field.toLowerCase() === fieldName.toLowerCase());
|
||||
|
||||
if (fieldChange) {
|
||||
const value = fieldChange.corrected || fieldChange.original;
|
||||
if (value) {
|
||||
// Use the field name and value as the title
|
||||
displayTitle = `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}: ${String(value).substring(0, 30)}${String(value).length > 30 ? '...' : ''}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a good title, use the index
|
||||
if (displayTitle === "Product") {
|
||||
displayTitle = `Product ${i + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={i} className="border rounded-md p-4">
|
||||
<h4 className="font-medium text-base mb-3">
|
||||
{displayTitle}
|
||||
</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/5">Field</TableHead>
|
||||
<TableHead className="w-2/5">Original Value</TableHead>
|
||||
<TableHead className="w-2/5">Corrected Value</TableHead>
|
||||
<TableHead className="w-1/12 text-right">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{product.changes.map((change: ChangeDetail, j: number) => {
|
||||
const field = fields.find(f => f.key === change.field);
|
||||
const isReverted = isChangeReverted(product.productIndex, change.field);
|
||||
|
||||
return (
|
||||
<TableRow key={j} className={isReverted ? "bg-muted/30" : ""}>
|
||||
<TableCell className="font-medium">
|
||||
{field?.label || change.field}
|
||||
{isReverted && (
|
||||
<Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
|
||||
Reverted
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Call the revert function directly
|
||||
revertAiChange(product.productIndex, change.field);
|
||||
}}
|
||||
>
|
||||
Revert Change
|
||||
</Button>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
{isReverted ? (
|
||||
<div className="font-medium">
|
||||
{getFieldDisplayValue(change.field, change.original)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getFieldDisplayValueWithHighlight(
|
||||
change.field,
|
||||
change.original,
|
||||
change.corrected
|
||||
).originalHtml
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* 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 && (
|
||||
<div className="text-xs text-muted-foreground">ID: {change.original}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
{isReverted ? (
|
||||
<div className="line-through text-muted-foreground">
|
||||
{getFieldDisplayValue(change.field, change.corrected)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getFieldDisplayValueWithHighlight(
|
||||
change.field,
|
||||
change.original,
|
||||
change.corrected
|
||||
).correctedHtml
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* 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 && (
|
||||
<div className="text-xs text-muted-foreground">ID: {change.corrected}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="mt-2">
|
||||
{isReverted ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
|
||||
disabled
|
||||
>
|
||||
<CheckIcon className="w-4 h-4 mr-1" />
|
||||
Reverted
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Call the revert function directly
|
||||
revertAiChange(product.productIndex, change.field);
|
||||
}}
|
||||
>
|
||||
Revert Change
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
aiValidationDetails.changes.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user