|
|
|
|
@@ -171,16 +171,34 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
|
|
|
|
|
|
|
|
|
|
// Determine if the field should be disabled based on its key and context
|
|
|
|
|
const isFieldDisabled = useMemo(() => {
|
|
|
|
|
// If the field is already disabled by the parent component, respect that
|
|
|
|
|
if (field.disabled) return true;
|
|
|
|
|
|
|
|
|
|
// Special handling for line and subline fields
|
|
|
|
|
if (field.key === 'line') {
|
|
|
|
|
// Enable the line field if we have product lines available
|
|
|
|
|
return !productLines || productLines.length === 0;
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
if (field.key === 'subline') {
|
|
|
|
|
// Enable subline field if we have sublines available
|
|
|
|
|
return !sublines || sublines.length === 0;
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For other fields, use the disabled property
|
|
|
|
|
return field.disabled;
|
|
|
|
|
}, [field.key, field.disabled, productLines, sublines]);
|
|
|
|
|
}, [field.key, field.disabled, field.fieldType, productLines, sublines, value]);
|
|
|
|
|
|
|
|
|
|
// For debugging
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
@@ -269,21 +287,71 @@ const EditableCell = memo(({ value, onChange, error, field, productLines, sublin
|
|
|
|
|
if (fieldType.type === "select" || fieldType.type === "multi-select") {
|
|
|
|
|
if (fieldType.type === "select") {
|
|
|
|
|
// For line and subline fields, ensure we're using the latest options
|
|
|
|
|
if (field.key === 'line' && productLines?.length) {
|
|
|
|
|
const option = productLines.find((opt: SelectOption) => opt.value === value);
|
|
|
|
|
return option?.label || value;
|
|
|
|
|
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) {
|
|
|
|
|
const matchingOption = productLines.find((opt: SelectOption) =>
|
|
|
|
|
String(opt.value) === String(value));
|
|
|
|
|
if (matchingOption) {
|
|
|
|
|
console.log('Found line in productLines:', value, '->', matchingOption.label);
|
|
|
|
|
return matchingOption.label;
|
|
|
|
|
}
|
|
|
|
|
if (field.key === 'subline' && sublines?.length) {
|
|
|
|
|
const option = sublines.find((opt: SelectOption) => opt.value === value);
|
|
|
|
|
return option?.label || value;
|
|
|
|
|
}
|
|
|
|
|
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value;
|
|
|
|
|
// Fall back to fieldType options if productLines not available yet
|
|
|
|
|
if (fieldType.options?.length) {
|
|
|
|
|
const fallbackOptionLine = fieldType.options.find((opt: SelectOption) =>
|
|
|
|
|
String(opt.value) === String(value));
|
|
|
|
|
if (fallbackOptionLine) {
|
|
|
|
|
console.log('Found line in fallback options:', value, '->', fallbackOptionLine.label);
|
|
|
|
|
return fallbackOptionLine.label;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log('Unable to find display value for line:', value);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
if (field.key === 'subline') {
|
|
|
|
|
// Log current state for debugging
|
|
|
|
|
console.log('Getting display value for subline:', {
|
|
|
|
|
value,
|
|
|
|
|
sublines: sublines?.length ?? 0,
|
|
|
|
|
options: fieldType.options?.length ?? 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// First try to find in sublines if available
|
|
|
|
|
if (sublines?.length) {
|
|
|
|
|
const matchingOption = sublines.find((opt: SelectOption) =>
|
|
|
|
|
String(opt.value) === String(value));
|
|
|
|
|
if (matchingOption) {
|
|
|
|
|
console.log('Found subline in sublines:', value, '->', matchingOption.label);
|
|
|
|
|
return matchingOption.label;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Fall back to fieldType options if sublines not available yet
|
|
|
|
|
if (fieldType.options?.length) {
|
|
|
|
|
const fallbackOptionSubline = fieldType.options.find((opt: SelectOption) =>
|
|
|
|
|
String(opt.value) === String(value));
|
|
|
|
|
if (fallbackOptionSubline) {
|
|
|
|
|
console.log('Found subline in fallback options:', value, '->', fallbackOptionSubline.label);
|
|
|
|
|
return fallbackOptionSubline.label;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log('Unable to find display value for subline:', value);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
return fieldType.options?.find((opt: SelectOption) => String(opt.value) === String(value))?.label || value;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
const options = field.key === 'line' && productLines?.length ? productLines :
|
|
|
|
|
field.key === 'subline' && sublines?.length ? sublines :
|
|
|
|
|
fieldType.options;
|
|
|
|
|
return value.map(v => options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ");
|
|
|
|
|
return value.map(v => options.find((opt: SelectOption) => String(opt.value) === String(v))?.label || v).join(", ");
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
@@ -881,21 +949,23 @@ function useTemplates<T extends string>(
|
|
|
|
|
const { __index, __errors, __template, ...templateData } = selectedRow;
|
|
|
|
|
|
|
|
|
|
// Clean numeric values and prepare template data
|
|
|
|
|
const cleanedData = Object.entries(templateData).reduce((acc, [key, value]) => {
|
|
|
|
|
const cleanedData: Record<string, any> = {};
|
|
|
|
|
|
|
|
|
|
// Process each key-value pair
|
|
|
|
|
Object.entries(templateData).forEach(([key, value]) => {
|
|
|
|
|
// Handle numeric values with dollar signs
|
|
|
|
|
if (typeof value === 'string' && value.includes('$')) {
|
|
|
|
|
acc[key] = value.replace(/[$,\s]/g, '').trim();
|
|
|
|
|
cleanedData[key] = value.replace(/[$,\s]/g, '').trim();
|
|
|
|
|
}
|
|
|
|
|
// Handle array values (like categories or ship_restrictions)
|
|
|
|
|
else if (Array.isArray(value)) {
|
|
|
|
|
acc[key] = value;
|
|
|
|
|
cleanedData[key] = value;
|
|
|
|
|
}
|
|
|
|
|
// Handle other values
|
|
|
|
|
else {
|
|
|
|
|
acc[key] = value;
|
|
|
|
|
cleanedData[key] = value;
|
|
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, any>);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Log the cleaned data before sending
|
|
|
|
|
console.log('Saving template with cleaned data:', {
|
|
|
|
|
@@ -1049,6 +1119,19 @@ const SaveTemplateDialog = memo(({
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add a new interface to handle the AI validation details response
|
|
|
|
|
interface ChangeDetail {
|
|
|
|
|
field: string;
|
|
|
|
|
original: any;
|
|
|
|
|
corrected: any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ProductChangeDetail {
|
|
|
|
|
productIndex: number;
|
|
|
|
|
title: string;
|
|
|
|
|
changes: ChangeDetail[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ValidationStep = <T extends string>({
|
|
|
|
|
initialData,
|
|
|
|
|
file,
|
|
|
|
|
@@ -1083,13 +1166,18 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
queryKey: ["sublines", globalSelections?.line],
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
if (!globalSelections?.line) return [];
|
|
|
|
|
console.log('Fetching sublines for line:', globalSelections.line);
|
|
|
|
|
const response = await fetch(`${config.apiUrl}/import/sublines/${globalSelections.line}`);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
console.error('Failed to fetch sublines:', response.status, response.statusText);
|
|
|
|
|
throw new Error("Failed to fetch sublines");
|
|
|
|
|
}
|
|
|
|
|
return response.json();
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
console.log('Received sublines:', data);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
enabled: !!globalSelections?.line,
|
|
|
|
|
staleTime: 30000, // Cache for 30 seconds
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Apply global selections to initial data and validate
|
|
|
|
|
@@ -1163,7 +1251,9 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
...field.fieldType,
|
|
|
|
|
options: productLines || (field.fieldType.type === 'select' ? field.fieldType.options : []),
|
|
|
|
|
},
|
|
|
|
|
disabled: (!productLines || productLines.length === 0) && !globalSelections?.line
|
|
|
|
|
// 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)
|
|
|
|
|
} as Field<T>;
|
|
|
|
|
}
|
|
|
|
|
if (field.key === 'subline') {
|
|
|
|
|
@@ -1179,7 +1269,7 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
}
|
|
|
|
|
return field;
|
|
|
|
|
});
|
|
|
|
|
}, [fields, productLines, sublines, globalSelections?.line]);
|
|
|
|
|
}, [fields, productLines, sublines, globalSelections?.company, globalSelections?.line]);
|
|
|
|
|
|
|
|
|
|
const [data, setData] = useState<RowData<T>[]>(initialDataWithGlobals);
|
|
|
|
|
|
|
|
|
|
@@ -1208,10 +1298,13 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
const [aiValidationDetails, setAiValidationDetails] = useState<{
|
|
|
|
|
changes: string[];
|
|
|
|
|
warnings: string[];
|
|
|
|
|
changeDetails: ProductChangeDetail[];
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
originalData?: (Data<T> & ExtendedMeta)[]; // Store original data for reverting changes
|
|
|
|
|
}>({
|
|
|
|
|
changes: [],
|
|
|
|
|
warnings: [],
|
|
|
|
|
changeDetails: [],
|
|
|
|
|
isOpen: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -1219,6 +1312,11 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
status: string;
|
|
|
|
|
step: number;
|
|
|
|
|
estimatedSeconds?: number;
|
|
|
|
|
startTime?: Date;
|
|
|
|
|
promptLength?: number;
|
|
|
|
|
elapsedSeconds?: number;
|
|
|
|
|
progressPercent?: number;
|
|
|
|
|
}>({
|
|
|
|
|
isOpen: false,
|
|
|
|
|
status: "",
|
|
|
|
|
@@ -1259,15 +1357,18 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
async (rows: (Data<T> & ExtendedMeta)[], indexes?: number[]) => {
|
|
|
|
|
setData(rows);
|
|
|
|
|
|
|
|
|
|
// Use fieldsWithUpdatedOptions to ensure we have the latest field definitions with proper options
|
|
|
|
|
const currentFields = fieldsWithUpdatedOptions as unknown as Fields<T>;
|
|
|
|
|
|
|
|
|
|
if (rowHook?.constructor.name === "AsyncFunction" || tableHook?.constructor.name === "AsyncFunction") {
|
|
|
|
|
const updatedData = await addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook, indexes);
|
|
|
|
|
const updatedData = await addErrorsAndRunHooks<T>(rows, currentFields, rowHook, tableHook, indexes);
|
|
|
|
|
setData(updatedData as (Data<T> & ExtendedMeta)[]);
|
|
|
|
|
} else {
|
|
|
|
|
const result = await addErrorsAndRunHooks<T>(rows, fields, rowHook, tableHook, indexes);
|
|
|
|
|
const result = await addErrorsAndRunHooks<T>(rows, currentFields, rowHook, tableHook, indexes);
|
|
|
|
|
setData(result as (Data<T> & ExtendedMeta)[]);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[rowHook, tableHook, fields],
|
|
|
|
|
[rowHook, tableHook, fieldsWithUpdatedOptions],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const updateRows = useCallback(
|
|
|
|
|
@@ -1544,20 +1645,97 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
}
|
|
|
|
|
}, [data, submitData]);
|
|
|
|
|
|
|
|
|
|
// Add AI validation function
|
|
|
|
|
// Update the AI validation function
|
|
|
|
|
const handleAiValidation = async () => {
|
|
|
|
|
try {
|
|
|
|
|
if (isAiValidating) return;
|
|
|
|
|
|
|
|
|
|
// Store the original data before any changes
|
|
|
|
|
const originalDataCopy = [...data];
|
|
|
|
|
|
|
|
|
|
setIsAiValidating(true);
|
|
|
|
|
const startTime = new Date();
|
|
|
|
|
setAiValidationProgress({
|
|
|
|
|
isOpen: true,
|
|
|
|
|
status: "Preparing data for validation...",
|
|
|
|
|
step: 1
|
|
|
|
|
step: 1,
|
|
|
|
|
startTime,
|
|
|
|
|
elapsedSeconds: 0,
|
|
|
|
|
progressPercent: 0,
|
|
|
|
|
...(aiValidationProgress.estimatedSeconds ? {
|
|
|
|
|
estimatedSeconds: aiValidationProgress.estimatedSeconds,
|
|
|
|
|
promptLength: aiValidationProgress.promptLength
|
|
|
|
|
} : {})
|
|
|
|
|
});
|
|
|
|
|
console.log('Sending data for validation:', data);
|
|
|
|
|
|
|
|
|
|
// Set up an interval to update progress
|
|
|
|
|
window.aiValidationTimer = setInterval(() => {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const elapsedSeconds = Math.floor((now.getTime() - startTime.getTime()) / 1000);
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
} 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)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract the base status message without any time information
|
|
|
|
|
const baseStatus = prev.status.replace(/\s\(\d+[ms].+\)$/, '').replace(/\s\(\d+m \d+s.+\)$/, '');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
elapsedSeconds,
|
|
|
|
|
progressPercent,
|
|
|
|
|
// Just use the base status message without time information
|
|
|
|
|
status: baseStatus
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
// Clean the data to ensure we only send what's needed
|
|
|
|
|
const cleanedData = data.map(item => {
|
|
|
|
|
const { __errors, __index, ...cleanProduct } = item;
|
|
|
|
|
return cleanProduct;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('Cleaned data for validation:', cleanedData);
|
|
|
|
|
|
|
|
|
|
// If we don't have an estimated time yet, try to get one
|
|
|
|
|
if (!aiValidationProgress.estimatedSeconds) {
|
|
|
|
|
try {
|
|
|
|
|
const debugResponse = await fetch(`${config.apiUrl}/ai-validation/debug`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ products: cleanedData })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (debugResponse.ok) {
|
|
|
|
|
const debugData = await debugResponse.json();
|
|
|
|
|
if (debugData.estimatedProcessingTime?.seconds) {
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
status: "Sending data to AI service and awaiting response...",
|
|
|
|
|
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
|
|
|
|
|
promptLength: debugData.promptLength
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (estimateError) {
|
|
|
|
|
console.error('Error getting time estimate:', estimateError);
|
|
|
|
|
// Continue without an estimate
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
status: "Sending data to AI service...",
|
|
|
|
|
step: 2
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
@@ -1566,19 +1744,19 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ products: data }),
|
|
|
|
|
body: JSON.stringify({ products: cleanedData }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('AI validation failed');
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
console.error('AI validation error response:', {
|
|
|
|
|
status: response.status,
|
|
|
|
|
statusText: response.statusText,
|
|
|
|
|
body: errorText
|
|
|
|
|
});
|
|
|
|
|
throw new Error(`AI validation failed: ${response.status} ${response.statusText}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
status: "Processing AI response...",
|
|
|
|
|
step: 3
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
console.log('AI validation response:', result);
|
|
|
|
|
|
|
|
|
|
@@ -1586,50 +1764,171 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
throw new Error(result.error || 'AI validation failed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update progress with actual processing time if available
|
|
|
|
|
if (result.performanceMetrics) {
|
|
|
|
|
console.log('Performance metrics:', result.performanceMetrics);
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
status: "Processing AI response...",
|
|
|
|
|
step: 3,
|
|
|
|
|
// Update with actual metrics from the server
|
|
|
|
|
estimatedSeconds: result.performanceMetrics.processingTimeSeconds || prev.estimatedSeconds,
|
|
|
|
|
promptLength: result.performanceMetrics.promptLength || prev.promptLength,
|
|
|
|
|
progressPercent: 75 // 75% complete when we're processing the AI response
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
status: "Processing AI response...",
|
|
|
|
|
step: 3,
|
|
|
|
|
progressPercent: 75
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
status: "Applying corrections...",
|
|
|
|
|
step: 4
|
|
|
|
|
step: 4,
|
|
|
|
|
progressPercent: 90 // 90% complete when applying corrections
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Update the data with AI suggestions
|
|
|
|
|
if (result.correctedData && Array.isArray(result.correctedData)) {
|
|
|
|
|
// Log the differences
|
|
|
|
|
data.forEach((original, index) => {
|
|
|
|
|
const corrected = result.correctedData[index];
|
|
|
|
|
if (corrected) {
|
|
|
|
|
const changes = Object.keys(corrected).filter(key => {
|
|
|
|
|
const originalValue = original[key as keyof typeof original];
|
|
|
|
|
const correctedValue = corrected[key as keyof typeof corrected];
|
|
|
|
|
return JSON.stringify(originalValue) !== JSON.stringify(correctedValue);
|
|
|
|
|
// Process data to properly handle comma-separated values for multi-select fields
|
|
|
|
|
const processedData = result.correctedData.map((corrected: any) => {
|
|
|
|
|
const processed = { ...corrected };
|
|
|
|
|
|
|
|
|
|
// Process each field
|
|
|
|
|
Object.keys(processed).forEach(key => {
|
|
|
|
|
const fieldConfig = fields.find(f => f.key === key);
|
|
|
|
|
|
|
|
|
|
// Handle multi-select fields (comma-separated values)
|
|
|
|
|
if (fieldConfig?.fieldType.type === 'multi-select' && typeof processed[key] === 'string') {
|
|
|
|
|
// Split comma-separated values and trim each value
|
|
|
|
|
processed[key] = processed[key].split(',').map((v: string) => v.trim()).filter(Boolean);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 || [];
|
|
|
|
|
|
|
|
|
|
// If the value is a string that matches a label but not a value, convert it to the corresponding ID
|
|
|
|
|
if (!Array.isArray(processed[key]) && typeof processed[key] === 'string') {
|
|
|
|
|
// Check if the value is already an ID
|
|
|
|
|
const isAlreadyId = options.some(opt => String(opt.value) === String(processed[key]));
|
|
|
|
|
|
|
|
|
|
if (!isAlreadyId) {
|
|
|
|
|
// Try to find the option by label
|
|
|
|
|
const matchingOption = options.find(opt => opt.label === processed[key]);
|
|
|
|
|
if (matchingOption) {
|
|
|
|
|
// Convert label to ID
|
|
|
|
|
const originalValue = processed[key];
|
|
|
|
|
processed[key] = matchingOption.value;
|
|
|
|
|
console.log(`Converted label "${originalValue}" to ID "${matchingOption.value}" for field "${key}"`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle array of values (multi-select)
|
|
|
|
|
if (Array.isArray(processed[key])) {
|
|
|
|
|
processed[key] = processed[key].map((val: string | number) => {
|
|
|
|
|
// 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);
|
|
|
|
|
if (matchingOption) {
|
|
|
|
|
// Convert label to ID
|
|
|
|
|
return matchingOption.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
});
|
|
|
|
|
if (changes.length > 0) {
|
|
|
|
|
console.log(`Changes for row ${index + 1}:`, changes.map(key => ({
|
|
|
|
|
field: key,
|
|
|
|
|
original: original[key as keyof typeof original],
|
|
|
|
|
corrected: corrected[key as keyof typeof corrected]
|
|
|
|
|
})));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return processed;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Preserve the __index and __errors from the original data
|
|
|
|
|
const newData = result.correctedData.map((item: any, idx: number) => ({
|
|
|
|
|
const newData = processedData.map((item: any, idx: number) => ({
|
|
|
|
|
...item,
|
|
|
|
|
__index: data[idx]?.__index,
|
|
|
|
|
__errors: data[idx]?.__errors,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Update the data and run validations
|
|
|
|
|
await updateData(newData);
|
|
|
|
|
}
|
|
|
|
|
console.log('About to update data with AI corrections:', {
|
|
|
|
|
originalDataSample: data.slice(0, 2),
|
|
|
|
|
newDataSample: newData.slice(0, 2),
|
|
|
|
|
correctionCount: result.changes?.length || 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Show changes and warnings in dialog
|
|
|
|
|
// First validate the new data to ensure all validation rules are applied
|
|
|
|
|
try {
|
|
|
|
|
// Use the current fields with updated options for validation
|
|
|
|
|
const currentFields = fieldsWithUpdatedOptions as unknown as Fields<T>;
|
|
|
|
|
|
|
|
|
|
// Validate the data with the hooks
|
|
|
|
|
const validatedData = await addErrorsAndRunHooks<T>(
|
|
|
|
|
newData,
|
|
|
|
|
currentFields,
|
|
|
|
|
rowHook,
|
|
|
|
|
tableHook
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update the component state with the validated data
|
|
|
|
|
setData(validatedData as (Data<T> & ExtendedMeta)[]);
|
|
|
|
|
|
|
|
|
|
// Force a small delay to ensure React updates the state before showing the results
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
|
|
|
|
|
|
// Log a sample of the state after update, focus on line/subline fields
|
|
|
|
|
console.log('State after AI validation:', {
|
|
|
|
|
sampleLineValues: validatedData.slice(0, 3).map(row => ({
|
|
|
|
|
line: row.line,
|
|
|
|
|
subline: row.subline,
|
|
|
|
|
lineDisplay: getFieldDisplayValue('line', row.line),
|
|
|
|
|
sublineDisplay: getFieldDisplayValue('subline', row.subline)
|
|
|
|
|
})),
|
|
|
|
|
fieldsWithOptions: fieldsWithUpdatedOptions
|
|
|
|
|
.filter(f => f.fieldType.type === 'select' && ['line', 'subline'].includes(f.key as string))
|
|
|
|
|
.map(f => ({
|
|
|
|
|
key: f.key,
|
|
|
|
|
options: 'options' in f.fieldType ? f.fieldType.options?.length || 0 : 0
|
|
|
|
|
}))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('Data updated after AI validation:', {
|
|
|
|
|
dataLength: validatedData.length,
|
|
|
|
|
hasErrors: validatedData.some(row => row.__errors && Object.keys(row.__errors).length > 0)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Show changes and warnings in dialog after data is updated
|
|
|
|
|
setAiValidationDetails({
|
|
|
|
|
changes: result.changes || [],
|
|
|
|
|
warnings: result.warnings || [],
|
|
|
|
|
changeDetails: result.changeDetails || [],
|
|
|
|
|
isOpen: true,
|
|
|
|
|
originalData: originalDataCopy // Use the stored original data
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error validating AI corrections:', error);
|
|
|
|
|
// Fall back to basic update without validation
|
|
|
|
|
setData(newData);
|
|
|
|
|
|
|
|
|
|
// Still show the result dialog even if validation failed
|
|
|
|
|
setAiValidationDetails({
|
|
|
|
|
changes: result.changes || [],
|
|
|
|
|
warnings: result.warnings || [],
|
|
|
|
|
changeDetails: result.changeDetails || [],
|
|
|
|
|
isOpen: true,
|
|
|
|
|
originalData: originalDataCopy, // Use the stored original data
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
@@ -1640,7 +1939,6 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('AI Validation Error:', error);
|
|
|
|
|
toast({
|
|
|
|
|
@@ -1654,7 +1952,18 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
step: -1
|
|
|
|
|
}));
|
|
|
|
|
} finally {
|
|
|
|
|
// Clear the interval when we're done (success or error)
|
|
|
|
|
if (window.aiValidationTimer) {
|
|
|
|
|
clearInterval(window.aiValidationTimer);
|
|
|
|
|
window.aiValidationTimer = undefined;
|
|
|
|
|
}
|
|
|
|
|
setIsAiValidating(false);
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -1705,7 +2014,8 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
// Log the response stats
|
|
|
|
|
console.log('Debug response stats:', {
|
|
|
|
|
promptLength: debugData.promptLength,
|
|
|
|
|
taxonomyStats: debugData.taxonomyStats
|
|
|
|
|
taxonomyStats: debugData.taxonomyStats,
|
|
|
|
|
estimatedProcessingTime: debugData.estimatedProcessingTime
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setCurrentPrompt(prev => ({
|
|
|
|
|
@@ -1713,6 +2023,15 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
prompt: debugData.sampleFullPrompt,
|
|
|
|
|
isLoading: false
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Store the estimated processing time for later use
|
|
|
|
|
if (debugData.estimatedProcessingTime?.seconds) {
|
|
|
|
|
setAiValidationProgress(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
estimatedSeconds: debugData.estimatedProcessingTime.seconds,
|
|
|
|
|
promptLength: debugData.promptLength
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching prompt:', error);
|
|
|
|
|
toast({
|
|
|
|
|
@@ -1724,6 +2043,83 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Function to get display value for field values (including IDs)
|
|
|
|
|
const getFieldDisplayValue = (fieldKey: string, value: any): string => {
|
|
|
|
|
const field = fields.find(f => f.key === fieldKey);
|
|
|
|
|
if (!field) return String(value);
|
|
|
|
|
|
|
|
|
|
// Handle different field types
|
|
|
|
|
if (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') {
|
|
|
|
|
const options = field.fieldType.options || [];
|
|
|
|
|
|
|
|
|
|
// Handle array of values (multi-select)
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.map(v => {
|
|
|
|
|
const option = options.find(opt => String(opt.value) === String(v));
|
|
|
|
|
return option ? option.label : v;
|
|
|
|
|
}).join(', ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle single value
|
|
|
|
|
const option = options.find(opt => String(opt.value) === String(value));
|
|
|
|
|
return option ? option.label : String(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return String(value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add a function to revert a specific AI validation change
|
|
|
|
|
const revertAiChange = (productIndex: number, fieldKey: string) => {
|
|
|
|
|
// Ensure we have the original data
|
|
|
|
|
if (!aiValidationDetails.originalData) {
|
|
|
|
|
console.error('Cannot revert: original data not available');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the original product and current product
|
|
|
|
|
const originalProduct = aiValidationDetails.originalData[productIndex];
|
|
|
|
|
if (!originalProduct) {
|
|
|
|
|
console.error(`Cannot revert: original product at index ${productIndex} not found`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentProduct = data[productIndex];
|
|
|
|
|
if (!currentProduct) {
|
|
|
|
|
console.error(`Cannot revert: current product at index ${productIndex} not found`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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}`, {
|
|
|
|
|
original: originalValue,
|
|
|
|
|
current: fieldKey in currentProduct ? (currentProduct as Record<string, any>)[fieldKey] : undefined
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create a new data array with the reverted field
|
|
|
|
|
const newData = [...data];
|
|
|
|
|
|
|
|
|
|
// Create a new product object with the reverted field
|
|
|
|
|
newData[productIndex] = {
|
|
|
|
|
...newData[productIndex],
|
|
|
|
|
[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]);
|
|
|
|
|
|
|
|
|
|
// Show success notification
|
|
|
|
|
toast({
|
|
|
|
|
title: "Change reverted",
|
|
|
|
|
description: `Reverted the change to "${fieldKey}"`,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
|
|
|
|
<CopyDownDialog
|
|
|
|
|
@@ -1795,19 +2191,43 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
<div
|
|
|
|
|
className="h-full bg-primary transition-all duration-500"
|
|
|
|
|
style={{
|
|
|
|
|
width: `${(aiValidationProgress.step / 5) * 100}%`,
|
|
|
|
|
width: `${aiValidationProgress.progressPercent ?? (aiValidationProgress.step / 5) * 100}%`,
|
|
|
|
|
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-muted-foreground w-12 text-right">
|
|
|
|
|
{aiValidationProgress.step === -1 ? '❌' : `${Math.round((aiValidationProgress.step / 5) * 100)}%`}
|
|
|
|
|
{aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
@@ -1815,7 +2235,7 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
open={aiValidationDetails.isOpen}
|
|
|
|
|
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
|
|
|
|
|
>
|
|
|
|
|
<DialogContent className="max-w-2xl">
|
|
|
|
|
<DialogContent className="max-w-4xl">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>AI Validation Results</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
@@ -1823,7 +2243,90 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<ScrollArea className="max-h-[60vh]">
|
|
|
|
|
{aiValidationDetails.changes.length > 0 && (
|
|
|
|
|
{aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? (
|
|
|
|
|
<div className="mb-6 space-y-6">
|
|
|
|
|
<h3 className="font-semibold text-lg">Detailed Changes:</h3>
|
|
|
|
|
{aiValidationDetails.changeDetails.map((product, i) => (
|
|
|
|
|
<div key={i} className="border rounded-md p-4">
|
|
|
|
|
<h4 className="font-medium text-base mb-3">{product.title}</h4>
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead className="w-1/4">Field</TableHead>
|
|
|
|
|
<TableHead className="w-3/8">Original Value</TableHead>
|
|
|
|
|
<TableHead className="w-3/8">Corrected Value</TableHead>
|
|
|
|
|
<TableHead className="w-1/8 text-right">Action</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{product.changes.map((change, j) => {
|
|
|
|
|
const field = fields.find(f => f.key === change.field);
|
|
|
|
|
return (
|
|
|
|
|
<TableRow key={j}>
|
|
|
|
|
<TableCell className="font-medium">{field?.label || change.field}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<div>{getFieldDisplayValue(change.field, change.original)}</div>
|
|
|
|
|
{/* Show raw value if it's an ID */}
|
|
|
|
|
{change.original && typeof change.original === 'string' &&
|
|
|
|
|
!isNaN(Number(change.original)) &&
|
|
|
|
|
getFieldDisplayValue(change.field, change.original) !== change.original && (
|
|
|
|
|
<div className="text-xs text-muted-foreground">ID: {change.original}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<div>{getFieldDisplayValue(change.field, change.corrected)}</div>
|
|
|
|
|
{/* Show raw value if it's an ID */}
|
|
|
|
|
{change.corrected && typeof change.corrected === 'string' &&
|
|
|
|
|
!isNaN(Number(change.corrected)) &&
|
|
|
|
|
getFieldDisplayValue(change.field, change.corrected) !== change.corrected && (
|
|
|
|
|
<div className="text-xs text-muted-foreground">ID: {change.corrected}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
aiValidationDetails.changes.length > 0 && (
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<h3 className="font-semibold mb-2">Changes Made:</h3>
|
|
|
|
|
<ul className="space-y-2">
|
|
|
|
|
@@ -1835,6 +2338,7 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
{aiValidationDetails.warnings.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
@@ -2063,3 +2567,10 @@ export const ValidationStep = <T extends string>({
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add TypeScript declaration for our global timer variable
|
|
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
aiValidationTimer?: NodeJS.Timeout;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|