Improve AI validate revert visuals, fix some regressions
This commit is contained in:
@@ -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') {
|
||||
@@ -1141,6 +1125,9 @@ export const ValidationStep = <T extends string>({
|
||||
}: Props<T>) => {
|
||||
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({
|
||||
@@ -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 => ({
|
||||
...prev,
|
||||
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
|
||||
promptLength: debugData.promptLength
|
||||
}));
|
||||
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,30 +2285,37 @@ 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 && (
|
||||
<div className="text-center text-sm">
|
||||
{(() => {
|
||||
// Calculate time remaining using the elapsed seconds
|
||||
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
|
||||
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
|
||||
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds);
|
||||
|
||||
// Format time remaining
|
||||
if (remainingSeconds < 60) {
|
||||
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`;
|
||||
} else {
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const seconds = Math.round(remainingSeconds % 60);
|
||||
return `Approximately ${minutes}m ${seconds}s remaining`;
|
||||
}
|
||||
})()}
|
||||
{aiValidationProgress.promptLength && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(() => {
|
||||
|
||||
|
||||
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
|
||||
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
|
||||
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
|
||||
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds);
|
||||
|
||||
// Format time remaining
|
||||
if (remainingSeconds < 60) {
|
||||
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`;
|
||||
} else {
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const seconds = Math.round(remainingSeconds % 60);
|
||||
return `Approximately ${minutes}m ${seconds}s remaining`;
|
||||
}
|
||||
})()}
|
||||
{aiValidationProgress.promptLength && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters
|
||||
</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">
|
||||
<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`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Revert Change
|
||||
</Button>
|
||||
{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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user