Improve AI validate revert visuals, fix some regressions

This commit is contained in:
2025-02-26 11:24:05 -05:00
parent 6b101a91f6
commit b96a9f412a
3 changed files with 296 additions and 163 deletions

View File

@@ -111,23 +111,44 @@ router.post("/debug", async (req, res) => {
}
try {
const avgTimeResults = await pool.query(
`SELECT AVG(duration_seconds) as avg_duration,
COUNT(*) as sample_count
FROM ai_validation_performance
WHERE prompt_length BETWEEN $1 * 0.8 AND $1 * 1.2`,
[debugResponse.promptLength]
// Instead of looking for similar prompt lengths, calculate an average processing rate
const rateResults = await pool.query(
`SELECT
AVG(duration_seconds / prompt_length) as avg_rate_per_char,
COUNT(*) as sample_count,
AVG(duration_seconds) as avg_duration
FROM ai_validation_performance`
);
// Add estimated time to the response
if (avgTimeResults.rows && avgTimeResults.rows[0]) {
if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) {
// Calculate estimated time based on the rate and current prompt length
const rate = rateResults.rows[0].avg_rate_per_char;
const estimatedSeconds = Math.max(15, Math.round(rate * debugResponse.promptLength));
debugResponse.estimatedProcessingTime = {
seconds: avgTimeResults.rows[0].avg_duration || null,
sampleCount: avgTimeResults.rows[0].sample_count || 0
seconds: estimatedSeconds,
sampleCount: rateResults.rows[0].sample_count || 0,
avgRate: rate,
calculationMethod: "rate-based"
};
console.log("📊 Retrieved processing time estimate:", debugResponse.estimatedProcessingTime);
console.log("📊 Calculated time estimate using rate-based method:", {
rate: rate,
promptLength: debugResponse.promptLength,
estimatedSeconds: estimatedSeconds,
sampleCount: rateResults.rows[0].sample_count
});
} else {
console.log("📊 No processing time estimates available for prompt length:", debugResponse.promptLength);
// Fallback: Calculate a simple estimate based on prompt length (1 second per 1000 characters)
const estimatedSeconds = Math.max(15, Math.round(debugResponse.promptLength / 1000));
console.log("📊 No rate data available, using fallback calculation");
debugResponse.estimatedProcessingTime = {
seconds: estimatedSeconds,
sampleCount: 0,
isEstimate: true,
calculationMethod: "fallback"
};
console.log("📊 Fallback time estimate:", debugResponse.estimatedProcessingTime);
}
} catch (queryError) {
console.error("⚠️ Failed to query performance metrics:", queryError);
@@ -812,29 +833,38 @@ 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)
VALUES ($1, $2, $3, $4)`,
[promptLength, products.length, startTime, endTime]
(prompt_length, product_count, start_time, end_time, duration_seconds)
VALUES ($1, $2, $3, $4, $5)`,
[
promptLength,
products.length,
startTime,
endTime,
(endTime - startTime) / 1000
]
);
console.log("📊 Performance metrics inserted into database");
// Query for average processing time based on similar prompt lengths
try {
const avgTimeResults = await pool.query(
`SELECT AVG(duration_seconds) as avg_duration,
COUNT(*) as sample_count
FROM ai_validation_performance
WHERE prompt_length BETWEEN $1 * 0.8 AND $1 * 1.2`,
[promptLength]
const rateResults = await pool.query(
`SELECT
AVG(duration_seconds / prompt_length) as avg_rate_per_char,
COUNT(*) as sample_count,
AVG(duration_seconds) as avg_duration
FROM ai_validation_performance`
);
if (avgTimeResults.rows && avgTimeResults.rows[0]) {
performanceMetrics.avgDuration = avgTimeResults.rows[0].avg_duration;
performanceMetrics.sampleCount = avgTimeResults.rows[0].sample_count;
if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) {
const rate = rateResults.rows[0].avg_rate_per_char;
performanceMetrics.avgRate = rate;
performanceMetrics.estimatedSeconds = Math.round(rate * promptLength);
performanceMetrics.sampleCount = rateResults.rows[0].sample_count;
performanceMetrics.calculationMethod = "rate-based";
}
console.log("📊 Performance metrics retrieved:", performanceMetrics);
console.log("📊 Performance metrics with rate calculation:", performanceMetrics);
} catch (queryError) {
console.error("⚠️ Failed to query performance metrics:", queryError);
}
@@ -854,7 +884,13 @@ router.post("/validate", async (req, res) => {
res.json({
success: true,
changeDetails: changeDetails,
performanceMetrics,
performanceMetrics: performanceMetrics || {
// Fallback: calculate a simple estimate
promptLength: promptLength,
processingTimeSeconds: Math.max(15, Math.round(promptLength / 1000)),
isEstimate: true,
productCount: products.length
},
...aiResponse,
});
} catch (parseError) {

View File

@@ -76,6 +76,8 @@ import {
} from "@/components/ui/select"
import type { GlobalSelections } from "../MatchColumnsStep/MatchColumnsStep"
import { useQuery } from "@tanstack/react-query"
import { Badge } from "@/components/ui/badge"
import { CheckIcon } from "lucide-react"
// Template interface
interface Template {
@@ -179,40 +181,29 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
// Never disable line field if it already has a value
if (value) return false;
// The line field should be enabled if:
// 1. We have a company selected (even if product lines are still loading)
// 2. We have product lines available
return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
field.fieldType.options?.length) && !productLines?.length;
// The line field should be enabled if we have a company selected or product lines available
const hasCompanySelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
field.fieldType.options && field.fieldType.options.length > 0;
const hasFetchedProductLines = productLines && productLines.length > 0;
return !(hasCompanySelected || hasFetchedProductLines);
}
if (field.key === 'subline') {
// Never disable subline field if it already has a value
if (value) return false;
// The subline field should be enabled if:
// 1. We have a line selected (even if sublines are still loading)
// 2. We have sublines available
return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
field.fieldType.options?.length) && !sublines?.length;
// The subline field should be enabled if we have a line selected or sublines available
const hasLineSelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
field.fieldType.options && field.fieldType.options.length > 0;
const hasFetchedSublines = sublines && sublines.length > 0;
return !(hasLineSelected || hasFetchedSublines);
}
// For other fields, use the disabled property
return field.disabled;
}, [field.key, field.disabled, field.fieldType, productLines, sublines, value]);
// For debugging
useEffect(() => {
if (field.key === 'subline') {
console.log('Subline field state:', {
disabled: field.disabled,
isFieldDisabled,
value,
options: field.fieldType.type === 'select' ? field.fieldType.options : [],
sublines,
hasSublines: sublines && sublines.length > 0
});
}
}, [field, value, sublines, isFieldDisabled]);
const handleWheel = useCallback((e: React.WheelEvent) => {
const commandList = e.currentTarget;
@@ -288,12 +279,6 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
if (fieldType.type === "select") {
// For line and subline fields, ensure we're using the latest options
if (field.key === 'line') {
// Log current state for debugging
console.log('Getting display value for line:', {
value,
productLines: productLines?.length ?? 0,
options: fieldType.options?.length ?? 0
});
// First try to find in productLines if available
if (productLines?.length) {
@@ -313,7 +298,6 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
return fallbackOptionLine.label;
}
}
console.log('Unable to find display value for line:', value);
return value;
}
if (field.key === 'subline') {
@@ -1142,6 +1126,9 @@ export const ValidationStep = <T extends string>({
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>();
const { toast } = useToast();
// Track which changes have been reverted
const [revertedChanges, setRevertedChanges] = useState<Set<string>>(new Set());
// Fetch product lines when company is selected
const { data: productLines } = useQuery({
queryKey: ["product-lines", globalSelections?.company],
@@ -1245,28 +1232,47 @@ export const ValidationStep = <T extends string>({
const fieldsWithUpdatedOptions = useMemo(() => {
return Array.from(fields as ReadonlyFields<T>).map(field => {
if (field.key === 'line') {
// Check if we have product lines available
const hasProductLines = productLines && productLines.length > 0;
// For line field, ensure we have the proper options
return {
...field,
fieldType: {
...field.fieldType,
options: productLines || (field.fieldType.type === 'select' ? field.fieldType.options : []),
// Use fetched product lines if available, otherwise keep existing options
options: hasProductLines
? productLines
: (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')
? field.fieldType.options
: []
},
// Only disable if no company is selected or if product lines failed to load
// when a company is selected
disabled: !globalSelections?.company || (globalSelections?.company && productLines !== undefined && productLines.length === 0)
// The line field should only be disabled if no company is selected AND no product lines available
disabled: !globalSelections?.company && !hasProductLines
} as Field<T>;
}
if (field.key === 'subline') {
// Check if we have sublines available
const hasSublines = sublines && sublines.length > 0;
// For subline field, ensure we have the proper options
return {
...field,
fieldType: {
...field.fieldType,
options: sublines || (field.fieldType.type === 'select' ? field.fieldType.options : []),
// Use fetched sublines if available, otherwise keep existing options
options: hasSublines
? sublines
: (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')
? field.fieldType.options
: []
},
// Enable subline field if we have a global line selection or if we have sublines available
disabled: !globalSelections?.line && (!sublines || sublines.length === 0)
// The subline field should only be disabled if no line is selected AND no sublines available
disabled: !globalSelections?.line && !hasSublines
} as Field<T>;
}
return field;
});
}, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]);
@@ -1653,6 +1659,9 @@ export const ValidationStep = <T extends string>({
// Store the original data before any changes
const originalDataCopy = [...data];
// Reset reverted changes when starting a new validation
setRevertedChanges(new Set());
setIsAiValidating(true);
const startTime = new Date();
setAiValidationProgress({
@@ -1677,12 +1686,28 @@ export const ValidationStep = <T extends string>({
setAiValidationProgress(prev => {
// Calculate progress percentage
let progressPercent = 0;
if (prev.estimatedSeconds && prev.estimatedSeconds > 0) {
// Cap at 99% if we exceed estimated time but aren't done yet
progressPercent = Math.min(99, Math.floor((elapsedSeconds / prev.estimatedSeconds) * 100));
// Log current state for debugging
console.log('Timer update - Current state:', {
step: prev.step,
hasEstimatedTime: !!prev.estimatedSeconds,
estimatedSeconds: prev.estimatedSeconds,
elapsedSeconds
});
// Progress is based on step, not just time
if (prev.step === 5) { // Completed step
progressPercent = 100;
} else if (prev.estimatedSeconds && prev.estimatedSeconds > 0) {
// Cap at 95% if we exceed estimated time but aren't done yet
progressPercent = Math.min(95, Math.floor((elapsedSeconds / prev.estimatedSeconds) * 100));
console.log('Using time-based progress:', progressPercent);
} else {
// If no estimate, use step-based progress (25% per step), also capped at 99%
progressPercent = Math.min(99, (prev.step * 25) + Math.min(24, Math.floor((elapsedSeconds % 10) / 10 * 25)));
// If no estimate, use step-based progress, each step is 20% progress plus some time-based progress within step
const baseProgress = (prev.step - 1) * 20;
const stepProgress = Math.min(20, Math.floor((elapsedSeconds % 30) / 30 * 20));
progressPercent = Math.min(95, baseProgress + stepProgress);
console.log('Using step-based progress:', progressPercent);
}
// Extract the base status message without any time information
@@ -1696,7 +1721,7 @@ export const ValidationStep = <T extends string>({
status: baseStatus
};
});
}, 1000);
}, 1000) as unknown as NodeJS.Timeout;
// Clean the data to ensure we only send what's needed
const cleanedData = data.map(item => {
@@ -1719,13 +1744,30 @@ export const ValidationStep = <T extends string>({
if (debugResponse.ok) {
const debugData = await debugResponse.json();
console.log('Debug response details:', {
hasEstimatedTime: !!debugData.estimatedProcessingTime,
estimatedTimeSeconds: debugData.estimatedProcessingTime?.seconds,
calculationMethod: debugData.estimatedProcessingTime?.calculationMethod || 'unknown',
avgRate: debugData.estimatedProcessingTime?.avgRate,
promptLength: debugData.promptLength,
fullResponse: debugData
});
if (debugData.estimatedProcessingTime?.seconds) {
setAiValidationProgress(prev => ({
console.log('Setting estimated time:', debugData.estimatedProcessingTime.seconds);
setAiValidationProgress(prev => {
const newState = {
...prev,
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
promptLength: debugData.promptLength
}));
};
console.log('New progress state with time:', newState);
return newState;
});
} else {
console.log('No estimated time in debug response');
}
} else {
console.error('Debug response not OK:', debugResponse.status);
}
} catch (estimateError) {
console.error('Error getting time estimate:', estimateError);
@@ -1736,7 +1778,9 @@ export const ValidationStep = <T extends string>({
setAiValidationProgress(prev => ({
...prev,
status: "Sending data to AI service...",
step: 2
step: 2,
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength
}));
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, {
@@ -1772,7 +1816,9 @@ export const ValidationStep = <T extends string>({
status: "Processing AI response...",
step: 3,
// Update with actual metrics from the server
estimatedSeconds: result.performanceMetrics.processingTimeSeconds || prev.estimatedSeconds,
estimatedSeconds: result.performanceMetrics.estimatedSeconds ||
result.performanceMetrics.processingTimeSeconds ||
prev.estimatedSeconds,
promptLength: result.performanceMetrics.promptLength || prev.promptLength,
progressPercent: 75 // 75% complete when we're processing the AI response
}));
@@ -1789,18 +1835,25 @@ export const ValidationStep = <T extends string>({
...prev,
status: "Applying corrections...",
step: 4,
progressPercent: 90 // 90% complete when applying corrections
progressPercent: 90, // 90% complete when applying corrections
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength
}));
// Update the data with AI suggestions
if (result.correctedData && Array.isArray(result.correctedData)) {
// Process data to properly handle comma-separated values for multi-select fields
const processedData = result.correctedData.map((corrected: any) => {
const processed = { ...corrected };
const processedData = result.correctedData.map((corrected: any, index: number) => {
// Start with original data to preserve __index and other metadata
const original = data[index] || {};
const processed = { ...original, ...corrected };
// Process each field
Object.keys(processed).forEach(key => {
if (key.startsWith('__')) return; // Skip metadata fields
const fieldConfig = fields.find(f => f.key === key);
if (!fieldConfig) return; // Skip if no matching field found
// Handle multi-select fields (comma-separated values)
if (fieldConfig?.fieldType.type === 'multi-select' && typeof processed[key] === 'string') {
@@ -1809,7 +1862,6 @@ export const ValidationStep = <T extends string>({
}
// For select and multi-select fields, ensure we're working with IDs
// We don't convert IDs to display names here because we want to preserve IDs in the data
if (fieldConfig?.fieldType.type === 'select' || fieldConfig?.fieldType.type === 'multi-select') {
const options = fieldConfig.fieldType.options || [];
@@ -1820,7 +1872,10 @@ export const ValidationStep = <T extends string>({
if (!isAlreadyId) {
// Try to find the option by label
const matchingOption = options.find(opt => opt.label === processed[key]);
const matchingOption = options.find(opt =>
opt.label.toLowerCase() === processed[key].toLowerCase() ||
String(opt.label) === String(processed[key])
);
if (matchingOption) {
// Convert label to ID
const originalValue = processed[key];
@@ -1833,12 +1888,17 @@ export const ValidationStep = <T extends string>({
// Handle array of values (multi-select)
if (Array.isArray(processed[key])) {
processed[key] = processed[key].map((val: string | number) => {
if (val === null || val === undefined) return val;
// Check if the value is already an ID
const isAlreadyId = options.some(opt => String(opt.value) === String(val));
if (!isAlreadyId) {
// Try to find the option by label
const matchingOption = options.find(opt => opt.label === val);
// Try to find the option by label (case insensitive)
const matchingOption = options.find(opt =>
typeof val === 'string' && typeof opt.label === 'string' &&
opt.label.toLowerCase() === val.toLowerCase()
);
if (matchingOption) {
// Convert label to ID
return matchingOption.value;
@@ -1853,16 +1913,9 @@ export const ValidationStep = <T extends string>({
return processed;
});
// Preserve the __index and __errors from the original data
const newData = processedData.map((item: any, idx: number) => ({
...item,
__index: data[idx]?.__index,
__errors: data[idx]?.__errors,
}));
console.log('About to update data with AI corrections:', {
originalDataSample: data.slice(0, 2),
newDataSample: newData.slice(0, 2),
processedDataSample: processedData.slice(0, 2),
correctionCount: result.changes?.length || 0
});
@@ -1873,7 +1926,7 @@ export const ValidationStep = <T extends string>({
// Validate the data with the hooks
const validatedData = await addErrorsAndRunHooks<T>(
newData,
processedData,
currentFields,
rowHook,
tableHook
@@ -1917,7 +1970,7 @@ export const ValidationStep = <T extends string>({
} catch (error) {
console.error('Error validating AI corrections:', error);
// Fall back to basic update without validation
setData(newData);
setData(processedData);
// Still show the result dialog even if validation failed
setAiValidationDetails({
@@ -1933,7 +1986,9 @@ export const ValidationStep = <T extends string>({
setAiValidationProgress(prev => ({
...prev,
status: "Validation complete!",
step: 5
step: 5,
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength
}));
setTimeout(() => {
@@ -1949,7 +2004,9 @@ export const ValidationStep = <T extends string>({
setAiValidationProgress(prev => ({
...prev,
status: "Validation failed",
step: -1
step: -1,
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength
}));
} finally {
// Clear the interval when we're done (success or error)
@@ -1962,7 +2019,10 @@ export const ValidationStep = <T extends string>({
// Only set to 100% when actually complete (or in error state)
setAiValidationProgress(prev => ({
...prev,
progressPercent: prev.step === -1 ? prev.progressPercent : 100 // Only show 100% if successful completion
progressPercent: prev.step === -1 ? prev.progressPercent : 100, // Only show 100% if successful completion
estimatedSeconds: prev.estimatedSeconds,
promptLength: prev.promptLength,
elapsedSeconds: prev.elapsedSeconds
}));
}
};
@@ -2083,17 +2143,20 @@ export const ValidationStep = <T extends string>({
return;
}
const currentProduct = data[productIndex];
if (!currentProduct) {
console.error(`Cannot revert: current product at index ${productIndex} not found`);
const currentDataIndex = data.findIndex(d => d.__index === originalProduct.__index);
if (currentDataIndex === -1) {
console.error(`Cannot revert: current product with __index ${originalProduct.__index} not found`);
return;
}
const currentProduct = data[currentDataIndex];
// Get the original value in a type-safe way
const originalValue = fieldKey in originalProduct ?
(originalProduct as Record<string, any>)[fieldKey] : undefined;
console.log(`Reverting change to field "${fieldKey}" for product at index ${productIndex}`, {
console.log(`Reverting change to field "${fieldKey}" for product at index ${currentDataIndex}`, {
originalIndex: productIndex,
currentIndex: currentDataIndex,
original: originalValue,
current: fieldKey in currentProduct ? (currentProduct as Record<string, any>)[fieldKey] : undefined
});
@@ -2101,25 +2164,43 @@ export const ValidationStep = <T extends string>({
// Create a new data array with the reverted field
const newData = [...data];
// Get the field configuration
// Create a new product object with the reverted field
newData[productIndex] = {
...newData[productIndex],
newData[currentDataIndex] = {
...newData[currentDataIndex],
[fieldKey]: originalValue
} as Data<T> & ExtendedMeta; // Cast to ensure type safety
};
// Update the data state
setData(newData);
// Re-validate to update error states
updateData(newData, [productIndex]);
updateData(newData, [currentDataIndex]);
// Add to the set of reverted changes
const revertKey = `${productIndex}:${fieldKey}`;
setRevertedChanges(prev => {
const newSet = new Set(prev);
newSet.add(revertKey);
return newSet;
});
// Show success notification
toast({
title: "Change reverted",
description: `Reverted the change to "${fieldKey}"`,
variant: "default", // Make sure it's visible
duration: 3000, // Show for 3 seconds
});
};
// Function to check if a change has been reverted
const isChangeReverted = (productIndex: number, fieldKey: string): boolean => {
const revertKey = `${productIndex}:${fieldKey}`;
return revertedChanges.has(revertKey);
};
return (
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
<CopyDownDialog
@@ -2204,7 +2285,13 @@ export const ValidationStep = <T extends string>({
<p className="text-center text-sm text-muted-foreground">
{aiValidationProgress.status}
</p>
{aiValidationProgress.estimatedSeconds && aiValidationProgress.elapsedSeconds !== undefined && aiValidationProgress.step > 0 && aiValidationProgress.step < 5 && (
{(() => {
return aiValidationProgress.estimatedSeconds &&
aiValidationProgress.elapsedSeconds !== undefined &&
aiValidationProgress.step > 0 &&
aiValidationProgress.step < 5 && (
<div className="text-center text-sm">
{(() => {
// Calculate time remaining using the elapsed seconds
@@ -2227,7 +2314,8 @@ export const ValidationStep = <T extends string>({
</p>
)}
</div>
)}
);
})()}
</div>
</DialogContent>
</Dialog>
@@ -2261,12 +2349,23 @@ export const ValidationStep = <T extends string>({
<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}>
<TableCell className="font-medium">{field?.label || change.field}</TableCell>
<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>{getFieldDisplayValue(change.field, change.original)}</div>
<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)) &&
@@ -2277,7 +2376,9 @@ export const ValidationStep = <T extends string>({
</TableCell>
<TableCell>
<div className="space-y-1">
<div>{getFieldDisplayValue(change.field, change.corrected)}</div>
<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)) &&
@@ -2288,33 +2389,28 @@ export const ValidationStep = <T extends string>({
</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={() => {
// Find the product in the current data
const currentDataIndex = data.findIndex(
d => d.__index === data[product.productIndex]?.__index
);
if (currentDataIndex >= 0) {
// Create new data array with the original value for this field
const newData = [...data];
newData[currentDataIndex] = {
...newData[currentDataIndex],
[change.field]: change.original
};
// Update the data
updateData(newData, [currentDataIndex]);
// Show toast
toast({
title: "Change reverted",
description: `Reverted ${change.field} to original value`,
});
}
// Call the revert function directly
revertAiChange(product.productIndex, change.field);
}}
>
Revert Change
</Button>
)}
</div>
</TableCell>
</TableRow>

View File

@@ -27,7 +27,7 @@ const BASE_IMPORT_FIELDS = [
label: "UPC",
key: "upc",
description: "Universal Product Code/Barcode",
alternateMatches: ["barcode", "bar code", "jan", "ean"],
alternateMatches: ["barcode", "bar code", "jan", "ean", "upc code"],
fieldType: { type: "input" },
width: 140,
validations: [
@@ -52,7 +52,7 @@ const BASE_IMPORT_FIELDS = [
label: "Notions #",
key: "notions_no",
description: "Internal notions number",
alternateMatches: ["notions #"],
alternateMatches: ["notions #","nmc"],
fieldType: { type: "input" },
width: 110,
validations: [
@@ -65,7 +65,7 @@ const BASE_IMPORT_FIELDS = [
label: "Name",
key: "name",
description: "Product name/title",
alternateMatches: ["sku description"],
alternateMatches: ["sku description","product name"],
fieldType: { type: "input" },
width: 500,
validations: [
@@ -137,7 +137,7 @@ const BASE_IMPORT_FIELDS = [
label: "Case Pack",
key: "case_qty",
description: "Number of units per case",
alternateMatches: ["mc qty","case qty","case pack"],
alternateMatches: ["mc qty","case qty","case pack","box ct"],
fieldType: { type: "input" },
width: 50,
validations: [
@@ -170,6 +170,7 @@ const BASE_IMPORT_FIELDS = [
label: "Line",
key: "line",
description: "Product line",
alternateMatches: ["collection"],
fieldType: {
type: "select",
options: [], // Will be populated dynamically based on company selection