Improve AI validate revert visuals, fix some regressions
This commit is contained in:
@@ -111,23 +111,44 @@ router.post("/debug", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const avgTimeResults = await pool.query(
|
// Instead of looking for similar prompt lengths, calculate an average processing rate
|
||||||
`SELECT AVG(duration_seconds) as avg_duration,
|
const rateResults = await pool.query(
|
||||||
COUNT(*) as sample_count
|
`SELECT
|
||||||
FROM ai_validation_performance
|
AVG(duration_seconds / prompt_length) as avg_rate_per_char,
|
||||||
WHERE prompt_length BETWEEN $1 * 0.8 AND $1 * 1.2`,
|
COUNT(*) as sample_count,
|
||||||
[debugResponse.promptLength]
|
AVG(duration_seconds) as avg_duration
|
||||||
|
FROM ai_validation_performance`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add estimated time to the response
|
// 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 = {
|
debugResponse.estimatedProcessingTime = {
|
||||||
seconds: avgTimeResults.rows[0].avg_duration || null,
|
seconds: estimatedSeconds,
|
||||||
sampleCount: avgTimeResults.rows[0].sample_count || 0
|
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 {
|
} 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) {
|
} catch (queryError) {
|
||||||
console.error("⚠️ Failed to query performance metrics:", 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
|
// Insert performance data into the local PostgreSQL database
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO ai_validation_performance
|
`INSERT INTO ai_validation_performance
|
||||||
(prompt_length, product_count, start_time, end_time)
|
(prompt_length, product_count, start_time, end_time, duration_seconds)
|
||||||
VALUES ($1, $2, $3, $4)`,
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
[promptLength, products.length, startTime, endTime]
|
[
|
||||||
|
promptLength,
|
||||||
|
products.length,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
(endTime - startTime) / 1000
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("📊 Performance metrics inserted into database");
|
console.log("📊 Performance metrics inserted into database");
|
||||||
|
|
||||||
// Query for average processing time based on similar prompt lengths
|
// Query for average processing time based on similar prompt lengths
|
||||||
try {
|
try {
|
||||||
const avgTimeResults = await pool.query(
|
const rateResults = await pool.query(
|
||||||
`SELECT AVG(duration_seconds) as avg_duration,
|
`SELECT
|
||||||
COUNT(*) as sample_count
|
AVG(duration_seconds / prompt_length) as avg_rate_per_char,
|
||||||
FROM ai_validation_performance
|
COUNT(*) as sample_count,
|
||||||
WHERE prompt_length BETWEEN $1 * 0.8 AND $1 * 1.2`,
|
AVG(duration_seconds) as avg_duration
|
||||||
[promptLength]
|
FROM ai_validation_performance`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (avgTimeResults.rows && avgTimeResults.rows[0]) {
|
if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) {
|
||||||
performanceMetrics.avgDuration = avgTimeResults.rows[0].avg_duration;
|
const rate = rateResults.rows[0].avg_rate_per_char;
|
||||||
performanceMetrics.sampleCount = avgTimeResults.rows[0].sample_count;
|
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) {
|
} catch (queryError) {
|
||||||
console.error("⚠️ Failed to query performance metrics:", queryError);
|
console.error("⚠️ Failed to query performance metrics:", queryError);
|
||||||
}
|
}
|
||||||
@@ -854,7 +884,13 @@ router.post("/validate", async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
changeDetails: changeDetails,
|
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,
|
...aiResponse,
|
||||||
});
|
});
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ import {
|
|||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import type { GlobalSelections } from "../MatchColumnsStep/MatchColumnsStep"
|
import type { GlobalSelections } from "../MatchColumnsStep/MatchColumnsStep"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
// Template interface
|
// Template interface
|
||||||
interface Template {
|
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
|
// Never disable line field if it already has a value
|
||||||
if (value) return false;
|
if (value) return false;
|
||||||
|
|
||||||
// The line field should be enabled if:
|
// The line field should be enabled if we have a company selected or product lines available
|
||||||
// 1. We have a company selected (even if product lines are still loading)
|
const hasCompanySelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
|
||||||
// 2. We have product lines available
|
field.fieldType.options && field.fieldType.options.length > 0;
|
||||||
return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
|
const hasFetchedProductLines = productLines && productLines.length > 0;
|
||||||
field.fieldType.options?.length) && !productLines?.length;
|
|
||||||
|
return !(hasCompanySelected || hasFetchedProductLines);
|
||||||
}
|
}
|
||||||
if (field.key === 'subline') {
|
if (field.key === 'subline') {
|
||||||
// Never disable subline field if it already has a value
|
// Never disable subline field if it already has a value
|
||||||
if (value) return false;
|
if (value) return false;
|
||||||
|
|
||||||
// The subline field should be enabled if:
|
// The subline field should be enabled if we have a line selected or sublines available
|
||||||
// 1. We have a line selected (even if sublines are still loading)
|
const hasLineSelected = (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
|
||||||
// 2. We have sublines available
|
field.fieldType.options && field.fieldType.options.length > 0;
|
||||||
return !((field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') &&
|
const hasFetchedSublines = sublines && sublines.length > 0;
|
||||||
field.fieldType.options?.length) && !sublines?.length;
|
|
||||||
|
return !(hasLineSelected || hasFetchedSublines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other fields, use the disabled property
|
// For other fields, use the disabled property
|
||||||
return field.disabled;
|
return field.disabled;
|
||||||
}, [field.key, field.disabled, field.fieldType, productLines, sublines, value]);
|
}, [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 handleWheel = useCallback((e: React.WheelEvent) => {
|
||||||
const commandList = e.currentTarget;
|
const commandList = e.currentTarget;
|
||||||
@@ -288,12 +279,6 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
|
|||||||
if (fieldType.type === "select") {
|
if (fieldType.type === "select") {
|
||||||
// For line and subline fields, ensure we're using the latest options
|
// For line and subline fields, ensure we're using the latest options
|
||||||
if (field.key === 'line') {
|
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
|
// First try to find in productLines if available
|
||||||
if (productLines?.length) {
|
if (productLines?.length) {
|
||||||
@@ -313,7 +298,6 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
|
|||||||
return fallbackOptionLine.label;
|
return fallbackOptionLine.label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Unable to find display value for line:', value);
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (field.key === 'subline') {
|
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 { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Track which changes have been reverted
|
||||||
|
const [revertedChanges, setRevertedChanges] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Fetch product lines when company is selected
|
// Fetch product lines when company is selected
|
||||||
const { data: productLines } = useQuery({
|
const { data: productLines } = useQuery({
|
||||||
queryKey: ["product-lines", globalSelections?.company],
|
queryKey: ["product-lines", globalSelections?.company],
|
||||||
@@ -1245,28 +1232,47 @@ export const ValidationStep = <T extends string>({
|
|||||||
const fieldsWithUpdatedOptions = useMemo(() => {
|
const fieldsWithUpdatedOptions = useMemo(() => {
|
||||||
return Array.from(fields as ReadonlyFields<T>).map(field => {
|
return Array.from(fields as ReadonlyFields<T>).map(field => {
|
||||||
if (field.key === 'line') {
|
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 {
|
return {
|
||||||
...field,
|
...field,
|
||||||
fieldType: {
|
fieldType: {
|
||||||
...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
|
// The line field should only be disabled if no company is selected AND no product lines available
|
||||||
// when a company is selected
|
disabled: !globalSelections?.company && !hasProductLines
|
||||||
disabled: !globalSelections?.company || (globalSelections?.company && productLines !== undefined && productLines.length === 0)
|
|
||||||
} as Field<T>;
|
} as Field<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.key === 'subline') {
|
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 {
|
return {
|
||||||
...field,
|
...field,
|
||||||
fieldType: {
|
fieldType: {
|
||||||
...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
|
// The subline field should only be disabled if no line is selected AND no sublines available
|
||||||
disabled: !globalSelections?.line && (!sublines || sublines.length === 0)
|
disabled: !globalSelections?.line && !hasSublines
|
||||||
} as Field<T>;
|
} as Field<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return field;
|
return field;
|
||||||
});
|
});
|
||||||
}, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]);
|
}, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]);
|
||||||
@@ -1653,6 +1659,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
// Store the original data before any changes
|
// Store the original data before any changes
|
||||||
const originalDataCopy = [...data];
|
const originalDataCopy = [...data];
|
||||||
|
|
||||||
|
// Reset reverted changes when starting a new validation
|
||||||
|
setRevertedChanges(new Set());
|
||||||
|
|
||||||
setIsAiValidating(true);
|
setIsAiValidating(true);
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
setAiValidationProgress({
|
setAiValidationProgress({
|
||||||
@@ -1677,12 +1686,28 @@ export const ValidationStep = <T extends string>({
|
|||||||
setAiValidationProgress(prev => {
|
setAiValidationProgress(prev => {
|
||||||
// Calculate progress percentage
|
// Calculate progress percentage
|
||||||
let progressPercent = 0;
|
let progressPercent = 0;
|
||||||
if (prev.estimatedSeconds && prev.estimatedSeconds > 0) {
|
|
||||||
// Cap at 99% if we exceed estimated time but aren't done yet
|
// Log current state for debugging
|
||||||
progressPercent = Math.min(99, Math.floor((elapsedSeconds / prev.estimatedSeconds) * 100));
|
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 {
|
} else {
|
||||||
// If no estimate, use step-based progress (25% per step), also capped at 99%
|
// If no estimate, use step-based progress, each step is 20% progress plus some time-based progress within step
|
||||||
progressPercent = Math.min(99, (prev.step * 25) + Math.min(24, Math.floor((elapsedSeconds % 10) / 10 * 25)));
|
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
|
// Extract the base status message without any time information
|
||||||
@@ -1696,7 +1721,7 @@ export const ValidationStep = <T extends string>({
|
|||||||
status: baseStatus
|
status: baseStatus
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000) as unknown as NodeJS.Timeout;
|
||||||
|
|
||||||
// Clean the data to ensure we only send what's needed
|
// Clean the data to ensure we only send what's needed
|
||||||
const cleanedData = data.map(item => {
|
const cleanedData = data.map(item => {
|
||||||
@@ -1719,13 +1744,30 @@ export const ValidationStep = <T extends string>({
|
|||||||
|
|
||||||
if (debugResponse.ok) {
|
if (debugResponse.ok) {
|
||||||
const debugData = await debugResponse.json();
|
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) {
|
if (debugData.estimatedProcessingTime?.seconds) {
|
||||||
setAiValidationProgress(prev => ({
|
console.log('Setting estimated time:', debugData.estimatedProcessingTime.seconds);
|
||||||
...prev,
|
setAiValidationProgress(prev => {
|
||||||
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
|
const newState = {
|
||||||
promptLength: debugData.promptLength
|
...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) {
|
} catch (estimateError) {
|
||||||
console.error('Error getting time estimate:', estimateError);
|
console.error('Error getting time estimate:', estimateError);
|
||||||
@@ -1736,7 +1778,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
setAiValidationProgress(prev => ({
|
setAiValidationProgress(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
status: "Sending data to AI service...",
|
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`, {
|
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, {
|
||||||
@@ -1772,7 +1816,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
status: "Processing AI response...",
|
status: "Processing AI response...",
|
||||||
step: 3,
|
step: 3,
|
||||||
// Update with actual metrics from the server
|
// 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,
|
promptLength: result.performanceMetrics.promptLength || prev.promptLength,
|
||||||
progressPercent: 75 // 75% complete when we're processing the AI response
|
progressPercent: 75 // 75% complete when we're processing the AI response
|
||||||
}));
|
}));
|
||||||
@@ -1789,18 +1835,25 @@ export const ValidationStep = <T extends string>({
|
|||||||
...prev,
|
...prev,
|
||||||
status: "Applying corrections...",
|
status: "Applying corrections...",
|
||||||
step: 4,
|
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
|
// Update the data with AI suggestions
|
||||||
if (result.correctedData && Array.isArray(result.correctedData)) {
|
if (result.correctedData && Array.isArray(result.correctedData)) {
|
||||||
// Process data to properly handle comma-separated values for multi-select fields
|
// Process data to properly handle comma-separated values for multi-select fields
|
||||||
const processedData = result.correctedData.map((corrected: any) => {
|
const processedData = result.correctedData.map((corrected: any, index: number) => {
|
||||||
const processed = { ...corrected };
|
// Start with original data to preserve __index and other metadata
|
||||||
|
const original = data[index] || {};
|
||||||
|
const processed = { ...original, ...corrected };
|
||||||
|
|
||||||
// Process each field
|
// Process each field
|
||||||
Object.keys(processed).forEach(key => {
|
Object.keys(processed).forEach(key => {
|
||||||
|
if (key.startsWith('__')) return; // Skip metadata fields
|
||||||
|
|
||||||
const fieldConfig = fields.find(f => f.key === key);
|
const fieldConfig = fields.find(f => f.key === key);
|
||||||
|
if (!fieldConfig) return; // Skip if no matching field found
|
||||||
|
|
||||||
// Handle multi-select fields (comma-separated values)
|
// Handle multi-select fields (comma-separated values)
|
||||||
if (fieldConfig?.fieldType.type === 'multi-select' && typeof processed[key] === 'string') {
|
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
|
// 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') {
|
if (fieldConfig?.fieldType.type === 'select' || fieldConfig?.fieldType.type === 'multi-select') {
|
||||||
const options = fieldConfig.fieldType.options || [];
|
const options = fieldConfig.fieldType.options || [];
|
||||||
|
|
||||||
@@ -1820,7 +1872,10 @@ export const ValidationStep = <T extends string>({
|
|||||||
|
|
||||||
if (!isAlreadyId) {
|
if (!isAlreadyId) {
|
||||||
// Try to find the option by label
|
// 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) {
|
if (matchingOption) {
|
||||||
// Convert label to ID
|
// Convert label to ID
|
||||||
const originalValue = processed[key];
|
const originalValue = processed[key];
|
||||||
@@ -1833,12 +1888,17 @@ export const ValidationStep = <T extends string>({
|
|||||||
// Handle array of values (multi-select)
|
// Handle array of values (multi-select)
|
||||||
if (Array.isArray(processed[key])) {
|
if (Array.isArray(processed[key])) {
|
||||||
processed[key] = processed[key].map((val: string | number) => {
|
processed[key] = processed[key].map((val: string | number) => {
|
||||||
|
if (val === null || val === undefined) return val;
|
||||||
|
|
||||||
// Check if the value is already an ID
|
// Check if the value is already an ID
|
||||||
const isAlreadyId = options.some(opt => String(opt.value) === String(val));
|
const isAlreadyId = options.some(opt => String(opt.value) === String(val));
|
||||||
|
|
||||||
if (!isAlreadyId) {
|
if (!isAlreadyId) {
|
||||||
// Try to find the option by label
|
// Try to find the option by label (case insensitive)
|
||||||
const matchingOption = options.find(opt => opt.label === val);
|
const matchingOption = options.find(opt =>
|
||||||
|
typeof val === 'string' && typeof opt.label === 'string' &&
|
||||||
|
opt.label.toLowerCase() === val.toLowerCase()
|
||||||
|
);
|
||||||
if (matchingOption) {
|
if (matchingOption) {
|
||||||
// Convert label to ID
|
// Convert label to ID
|
||||||
return matchingOption.value;
|
return matchingOption.value;
|
||||||
@@ -1853,16 +1913,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
return processed;
|
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:', {
|
console.log('About to update data with AI corrections:', {
|
||||||
originalDataSample: data.slice(0, 2),
|
originalDataSample: data.slice(0, 2),
|
||||||
newDataSample: newData.slice(0, 2),
|
processedDataSample: processedData.slice(0, 2),
|
||||||
correctionCount: result.changes?.length || 0
|
correctionCount: result.changes?.length || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1873,7 +1926,7 @@ export const ValidationStep = <T extends string>({
|
|||||||
|
|
||||||
// Validate the data with the hooks
|
// Validate the data with the hooks
|
||||||
const validatedData = await addErrorsAndRunHooks<T>(
|
const validatedData = await addErrorsAndRunHooks<T>(
|
||||||
newData,
|
processedData,
|
||||||
currentFields,
|
currentFields,
|
||||||
rowHook,
|
rowHook,
|
||||||
tableHook
|
tableHook
|
||||||
@@ -1917,7 +1970,7 @@ export const ValidationStep = <T extends string>({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error validating AI corrections:', error);
|
console.error('Error validating AI corrections:', error);
|
||||||
// Fall back to basic update without validation
|
// Fall back to basic update without validation
|
||||||
setData(newData);
|
setData(processedData);
|
||||||
|
|
||||||
// Still show the result dialog even if validation failed
|
// Still show the result dialog even if validation failed
|
||||||
setAiValidationDetails({
|
setAiValidationDetails({
|
||||||
@@ -1933,7 +1986,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
setAiValidationProgress(prev => ({
|
setAiValidationProgress(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
status: "Validation complete!",
|
status: "Validation complete!",
|
||||||
step: 5
|
step: 5,
|
||||||
|
estimatedSeconds: prev.estimatedSeconds,
|
||||||
|
promptLength: prev.promptLength
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1949,7 +2004,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
setAiValidationProgress(prev => ({
|
setAiValidationProgress(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
status: "Validation failed",
|
status: "Validation failed",
|
||||||
step: -1
|
step: -1,
|
||||||
|
estimatedSeconds: prev.estimatedSeconds,
|
||||||
|
promptLength: prev.promptLength
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
// Clear the interval when we're done (success or error)
|
// 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)
|
// Only set to 100% when actually complete (or in error state)
|
||||||
setAiValidationProgress(prev => ({
|
setAiValidationProgress(prev => ({
|
||||||
...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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentProduct = data[productIndex];
|
const currentDataIndex = data.findIndex(d => d.__index === originalProduct.__index);
|
||||||
if (!currentProduct) {
|
if (currentDataIndex === -1) {
|
||||||
console.error(`Cannot revert: current product at index ${productIndex} not found`);
|
console.error(`Cannot revert: current product with __index ${originalProduct.__index} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const currentProduct = data[currentDataIndex];
|
||||||
|
|
||||||
// Get the original value in a type-safe way
|
// Get the original value in a type-safe way
|
||||||
const originalValue = fieldKey in originalProduct ?
|
const originalValue = fieldKey in originalProduct ?
|
||||||
(originalProduct as Record<string, any>)[fieldKey] : undefined;
|
(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,
|
original: originalValue,
|
||||||
current: fieldKey in currentProduct ? (currentProduct as Record<string, any>)[fieldKey] : undefined
|
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
|
// Create a new data array with the reverted field
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
|
|
||||||
|
// Get the field configuration
|
||||||
|
|
||||||
// Create a new product object with the reverted field
|
// Create a new product object with the reverted field
|
||||||
newData[productIndex] = {
|
newData[currentDataIndex] = {
|
||||||
...newData[productIndex],
|
...newData[currentDataIndex],
|
||||||
[fieldKey]: originalValue
|
[fieldKey]: originalValue
|
||||||
} as Data<T> & ExtendedMeta; // Cast to ensure type safety
|
};
|
||||||
|
|
||||||
// Update the data state
|
// Update the data state
|
||||||
setData(newData);
|
setData(newData);
|
||||||
|
|
||||||
// Re-validate to update error states
|
// 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
|
// Show success notification
|
||||||
toast({
|
toast({
|
||||||
title: "Change reverted",
|
title: "Change reverted",
|
||||||
description: `Reverted the change to "${fieldKey}"`,
|
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 (
|
return (
|
||||||
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
||||||
<CopyDownDialog
|
<CopyDownDialog
|
||||||
@@ -2204,30 +2285,37 @@ export const ValidationStep = <T extends string>({
|
|||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
{aiValidationProgress.status}
|
{aiValidationProgress.status}
|
||||||
</p>
|
</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 aiValidationProgress.estimatedSeconds &&
|
||||||
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`;
|
aiValidationProgress.elapsedSeconds !== undefined &&
|
||||||
} else {
|
aiValidationProgress.step > 0 &&
|
||||||
const minutes = Math.floor(remainingSeconds / 60);
|
aiValidationProgress.step < 5 && (
|
||||||
const seconds = Math.round(remainingSeconds % 60);
|
<div className="text-center text-sm">
|
||||||
return `Approximately ${minutes}m ${seconds}s remaining`;
|
{(() => {
|
||||||
}
|
// Calculate time remaining using the elapsed seconds
|
||||||
})()}
|
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
|
||||||
{aiValidationProgress.promptLength && (
|
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds);
|
||||||
Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters
|
|
||||||
</p>
|
// Format time remaining
|
||||||
)}
|
if (remainingSeconds < 60) {
|
||||||
</div>
|
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>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -2261,12 +2349,23 @@ export const ValidationStep = <T extends string>({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{product.changes.map((change, j) => {
|
{product.changes.map((change, j) => {
|
||||||
const field = fields.find(f => f.key === change.field);
|
const field = fields.find(f => f.key === change.field);
|
||||||
|
const isReverted = isChangeReverted(product.productIndex, change.field);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={j}>
|
<TableRow key={j} className={isReverted ? "bg-muted/30" : ""}>
|
||||||
<TableCell className="font-medium">{field?.label || change.field}</TableCell>
|
<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>
|
<TableCell>
|
||||||
<div className="space-y-1">
|
<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 */}
|
{/* Show raw value if it's an ID */}
|
||||||
{change.original && typeof change.original === 'string' &&
|
{change.original && typeof change.original === 'string' &&
|
||||||
!isNaN(Number(change.original)) &&
|
!isNaN(Number(change.original)) &&
|
||||||
@@ -2277,7 +2376,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="space-y-1">
|
<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 */}
|
{/* Show raw value if it's an ID */}
|
||||||
{change.corrected && typeof change.corrected === 'string' &&
|
{change.corrected && typeof change.corrected === 'string' &&
|
||||||
!isNaN(Number(change.corrected)) &&
|
!isNaN(Number(change.corrected)) &&
|
||||||
@@ -2288,33 +2389,28 @@ export const ValidationStep = <T extends string>({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Button
|
{isReverted ? (
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
// Find the product in the current data
|
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
|
||||||
const currentDataIndex = data.findIndex(
|
disabled
|
||||||
d => d.__index === data[product.productIndex]?.__index
|
>
|
||||||
);
|
<CheckIcon className="w-4 h-4 mr-1" />
|
||||||
if (currentDataIndex >= 0) {
|
Reverted
|
||||||
// Create new data array with the original value for this field
|
</Button>
|
||||||
const newData = [...data];
|
) : (
|
||||||
newData[currentDataIndex] = {
|
<Button
|
||||||
...newData[currentDataIndex],
|
variant="outline"
|
||||||
[change.field]: change.original
|
size="sm"
|
||||||
};
|
onClick={() => {
|
||||||
// Update the data
|
// Call the revert function directly
|
||||||
updateData(newData, [currentDataIndex]);
|
revertAiChange(product.productIndex, change.field);
|
||||||
// Show toast
|
}}
|
||||||
toast({
|
>
|
||||||
title: "Change reverted",
|
Revert Change
|
||||||
description: `Reverted ${change.field} to original value`,
|
</Button>
|
||||||
});
|
)}
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Revert Change
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "UPC",
|
label: "UPC",
|
||||||
key: "upc",
|
key: "upc",
|
||||||
description: "Universal Product Code/Barcode",
|
description: "Universal Product Code/Barcode",
|
||||||
alternateMatches: ["barcode", "bar code", "jan", "ean"],
|
alternateMatches: ["barcode", "bar code", "jan", "ean", "upc code"],
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 140,
|
width: 140,
|
||||||
validations: [
|
validations: [
|
||||||
@@ -52,7 +52,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "Notions #",
|
label: "Notions #",
|
||||||
key: "notions_no",
|
key: "notions_no",
|
||||||
description: "Internal notions number",
|
description: "Internal notions number",
|
||||||
alternateMatches: ["notions #"],
|
alternateMatches: ["notions #","nmc"],
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 110,
|
width: 110,
|
||||||
validations: [
|
validations: [
|
||||||
@@ -65,7 +65,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "Name",
|
label: "Name",
|
||||||
key: "name",
|
key: "name",
|
||||||
description: "Product name/title",
|
description: "Product name/title",
|
||||||
alternateMatches: ["sku description"],
|
alternateMatches: ["sku description","product name"],
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 500,
|
width: 500,
|
||||||
validations: [
|
validations: [
|
||||||
@@ -137,7 +137,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "Case Pack",
|
label: "Case Pack",
|
||||||
key: "case_qty",
|
key: "case_qty",
|
||||||
description: "Number of units per case",
|
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" },
|
fieldType: { type: "input" },
|
||||||
width: 50,
|
width: 50,
|
||||||
validations: [
|
validations: [
|
||||||
@@ -170,6 +170,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "Line",
|
label: "Line",
|
||||||
key: "line",
|
key: "line",
|
||||||
description: "Product line",
|
description: "Product line",
|
||||||
|
alternateMatches: ["collection"],
|
||||||
fieldType: {
|
fieldType: {
|
||||||
type: "select",
|
type: "select",
|
||||||
options: [], // Will be populated dynamically based on company selection
|
options: [], // Will be populated dynamically based on company selection
|
||||||
|
|||||||
Reference in New Issue
Block a user