More validate step changes to get closer to original, made the default step now
This commit is contained in:
@@ -50,7 +50,6 @@ export type GlobalSelections = {
|
||||
company?: string
|
||||
line?: string
|
||||
subline?: string
|
||||
useNewValidation?: boolean
|
||||
}
|
||||
|
||||
export enum ColumnType {
|
||||
@@ -1708,26 +1707,13 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<Switch
|
||||
id="use-new-validation"
|
||||
checked={!!globalSelections.useNewValidation}
|
||||
onCheckedChange={(checked) =>
|
||||
setGlobalSelections(prev => ({ ...prev, useNewValidation: checked }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="use-new-validation">Use new validation component</Label>
|
||||
</div>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
disabled={isLoading}
|
||||
onClick={handleOnContinue}
|
||||
>
|
||||
{translations.matchColumnsStep.nextButtonTitle}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
disabled={isLoading}
|
||||
onClick={handleOnContinue}
|
||||
>
|
||||
{translations.matchColumnsStep.nextButtonTitle}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -200,37 +200,9 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
||||
/>
|
||||
)
|
||||
case StepType.validateData:
|
||||
// Check if new validation component should be used
|
||||
if (state.globalSelections?.useNewValidation) {
|
||||
// Use the new ValidationStepNew component
|
||||
return (
|
||||
<ValidationStepNew
|
||||
initialData={state.data}
|
||||
file={uploadedFile!}
|
||||
onBack={() => {
|
||||
if (onBack) {
|
||||
// When going back, preserve the global selections
|
||||
setPersistedGlobalSelections(state.globalSelections)
|
||||
onBack()
|
||||
}
|
||||
}}
|
||||
onNext={(validatedData) => {
|
||||
// Go to image upload step with the validated data
|
||||
onNext({
|
||||
type: StepType.imageUpload,
|
||||
data: validatedData,
|
||||
file: uploadedFile!,
|
||||
globalSelections: state.globalSelections
|
||||
});
|
||||
}}
|
||||
isFromScratch={state.isFromScratch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, use the original ValidationStep component
|
||||
// Always use the new ValidationStepNew component
|
||||
return (
|
||||
<ValidationStep
|
||||
<ValidationStepNew
|
||||
initialData={state.data}
|
||||
file={uploadedFile!}
|
||||
onBack={() => {
|
||||
@@ -249,7 +221,6 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
||||
globalSelections: state.globalSelections
|
||||
});
|
||||
}}
|
||||
globalSelections={state.globalSelections}
|
||||
isFromScratch={state.isFromScratch}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1939,6 +1939,7 @@ export const ValidationStep = <T extends string>({
|
||||
file,
|
||||
onBack,
|
||||
onNext,
|
||||
globalSelections,
|
||||
isFromScratch
|
||||
}: Props<T>) => {
|
||||
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>();
|
||||
@@ -1971,6 +1972,93 @@ export const ValidationStep = <T extends string>({
|
||||
// Reference to store hook timeouts to prevent race conditions
|
||||
const hookTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Fetch product lines when company is selected
|
||||
const { data: productLines } = useQuery({
|
||||
queryKey: ["product-lines", data.length > 0 ? data[0]?.company : null],
|
||||
queryFn: async () => {
|
||||
if (!data.length || !data[0]?.company) return [];
|
||||
console.log('Fetching product lines for company:', data[0].company);
|
||||
const response = await fetch(`${config.apiUrl}/import/product-lines/${data[0].company}`);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch product lines:', response.status, response.statusText);
|
||||
throw new Error("Failed to fetch product lines");
|
||||
}
|
||||
const productLinesData = await response.json();
|
||||
console.log('Received product lines:', productLinesData);
|
||||
return productLinesData;
|
||||
},
|
||||
enabled: !!(data.length > 0 && data[0]?.company),
|
||||
staleTime: 30000, // Cache for 30 seconds
|
||||
});
|
||||
|
||||
// Fetch sublines when line is selected
|
||||
const { data: sublines } = useQuery({
|
||||
queryKey: ["sublines", data.length > 0 ? data[0]?.line : null],
|
||||
queryFn: async () => {
|
||||
if (!data.length || !data[0]?.line) return [];
|
||||
console.log('Fetching sublines for line:', data[0].line);
|
||||
const response = await fetch(`${config.apiUrl}/import/sublines/${data[0].line}`);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch sublines:', response.status, response.statusText);
|
||||
throw new Error("Failed to fetch sublines");
|
||||
}
|
||||
const sublinesData = await response.json();
|
||||
console.log('Received sublines:', sublinesData);
|
||||
return sublinesData;
|
||||
},
|
||||
enabled: !!(data.length > 0 && data[0]?.line),
|
||||
staleTime: 30000, // Cache for 30 seconds
|
||||
});
|
||||
|
||||
// Update field options with fetched data
|
||||
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,
|
||||
// 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
|
||||
: []
|
||||
},
|
||||
// The line field should only be disabled if no company is selected AND no product lines available
|
||||
disabled: !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,
|
||||
// Use fetched sublines if available, otherwise keep existing options
|
||||
options: hasSublines
|
||||
? sublines
|
||||
: (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')
|
||||
? field.fieldType.options
|
||||
: []
|
||||
},
|
||||
// The subline field should only be disabled if no line is selected AND no sublines available
|
||||
disabled: !hasSublines
|
||||
} as Field<T>;
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}, [fields, productLines, sublines]);
|
||||
|
||||
// Define updateData function for validation hooks
|
||||
const updateData = useCallback(
|
||||
async (rows: (Data<T> & ExtendedMeta)[], indexes?: number[]) => {
|
||||
@@ -2217,96 +2305,6 @@ export const ValidationStep = <T extends string>({
|
||||
// Track which rows are currently being validated to maintain loading state
|
||||
const rowsBeingValidatedRef = useRef<Set<number>>(new Set());
|
||||
|
||||
// Fetch product lines when company is selected
|
||||
const { data: productLines } = useQuery({
|
||||
queryKey: ["product-lines", data.length > 0 ? data[0]?.company : null],
|
||||
queryFn: async () => {
|
||||
if (!data.length || !data[0]?.company) return [];
|
||||
console.log('Fetching product lines for company:', data[0].company);
|
||||
const response = await fetch(`${config.apiUrl}/import/product-lines/${data[0].company}`);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch product lines:', response.status, response.statusText);
|
||||
throw new Error("Failed to fetch product lines");
|
||||
}
|
||||
const productLinesData = await response.json();
|
||||
console.log('Received product lines:', productLinesData);
|
||||
return productLinesData;
|
||||
},
|
||||
enabled: !!(data.length > 0 && data[0]?.company),
|
||||
staleTime: 30000, // Cache for 30 seconds
|
||||
});
|
||||
|
||||
// Fetch sublines when line is selected
|
||||
const { data: sublines } = useQuery({
|
||||
queryKey: ["sublines", data.length > 0 ? data[0]?.line : null],
|
||||
queryFn: async () => {
|
||||
if (!data.length || !data[0]?.line) return [];
|
||||
console.log('Fetching sublines for line:', data[0].line);
|
||||
const response = await fetch(`${config.apiUrl}/import/sublines/${data[0].line}`);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch sublines:', response.status, response.statusText);
|
||||
throw new Error("Failed to fetch sublines");
|
||||
}
|
||||
const sublinesData = await response.json();
|
||||
console.log('Received sublines:', sublinesData);
|
||||
return sublinesData;
|
||||
},
|
||||
enabled: !!(data.length > 0 && data[0]?.line),
|
||||
staleTime: 30000, // Cache for 30 seconds
|
||||
});
|
||||
|
||||
// Helper function to safely set a field value and update options if needed
|
||||
// This function is used when setting field values
|
||||
|
||||
// Update field options with fetched data
|
||||
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,
|
||||
// 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
|
||||
: []
|
||||
},
|
||||
// The line field should only be disabled if no company is selected AND no product lines available
|
||||
disabled: !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,
|
||||
// Use fetched sublines if available, otherwise keep existing options
|
||||
options: hasSublines
|
||||
? sublines
|
||||
: (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')
|
||||
? field.fieldType.options
|
||||
: []
|
||||
},
|
||||
// The subline field should only be disabled if no line is selected AND no sublines available
|
||||
disabled: !hasSublines
|
||||
} as Field<T>;
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}, [fields, productLines, sublines]);
|
||||
|
||||
// Define the validation function - not using useCallback to avoid dependency issues
|
||||
const validateUpcAndGenerateItemNumbers = async (forceValidation = false) => {
|
||||
let newData = [...data];
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2, CheckIcon } from 'lucide-react';
|
||||
import { Code } from '@/components/ui/code';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { AiValidationDetails, AiValidationProgress, CurrentPrompt, ProductChangeDetail } from '../hooks/useAiValidation';
|
||||
|
||||
interface AiValidationDialogsProps {
|
||||
aiValidationProgress: AiValidationProgress;
|
||||
aiValidationDetails: AiValidationDetails;
|
||||
currentPrompt: CurrentPrompt;
|
||||
setAiValidationProgress: React.Dispatch<React.SetStateAction<AiValidationProgress>>;
|
||||
setAiValidationDetails: React.Dispatch<React.SetStateAction<AiValidationDetails>>;
|
||||
setCurrentPrompt: React.Dispatch<React.SetStateAction<CurrentPrompt>>;
|
||||
revertAiChange: (productIndex: number, fieldKey: string) => void;
|
||||
isChangeReverted: (productIndex: number, fieldKey: string) => boolean;
|
||||
getFieldDisplayValueWithHighlight: (fieldKey: string, originalValue: any, correctedValue: any) => { originalHtml: string, correctedHtml: string };
|
||||
fields: readonly any[];
|
||||
}
|
||||
|
||||
export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
||||
aiValidationProgress,
|
||||
aiValidationDetails,
|
||||
currentPrompt,
|
||||
setAiValidationProgress,
|
||||
setAiValidationDetails,
|
||||
setCurrentPrompt,
|
||||
revertAiChange,
|
||||
isChangeReverted,
|
||||
getFieldDisplayValueWithHighlight,
|
||||
fields
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* Current Prompt Dialog */}
|
||||
<Dialog
|
||||
open={currentPrompt.isOpen}
|
||||
onOpenChange={(open) => setCurrentPrompt(prev => ({ ...prev, isOpen: open }))}
|
||||
>
|
||||
<DialogContent className="max-w-4xl h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Current AI Prompt</DialogTitle>
|
||||
<DialogDescription>
|
||||
This is the exact prompt that would be sent to the AI for validation
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="flex-1">
|
||||
{currentPrompt.isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<Code className="whitespace-pre-wrap p-4">{currentPrompt.prompt}</Code>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* AI Validation Progress Dialog */}
|
||||
<Dialog
|
||||
open={aiValidationProgress.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
// Only allow closing if validation failed
|
||||
if (!open && aiValidationProgress.step === -1) {
|
||||
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Validation Progress</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-500"
|
||||
style={{
|
||||
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 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
{aiValidationProgress.status}
|
||||
</p>
|
||||
{(() => {
|
||||
// Only show time remaining if we have an estimate and are in progress
|
||||
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>
|
||||
|
||||
{/* AI Validation Results Dialog */}
|
||||
<Dialog
|
||||
open={aiValidationDetails.isOpen}
|
||||
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
|
||||
>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Validation Results</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review the changes and warnings suggested by the AI
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[60vh]">
|
||||
{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) => {
|
||||
// Find the title change if it exists
|
||||
const titleChange = product.changes.find(c => c.field === 'title');
|
||||
const titleValue = titleChange ? titleChange.corrected : product.title;
|
||||
|
||||
return (
|
||||
<div key={`product-${i}`} className="border rounded-md p-4">
|
||||
<h4 className="font-medium text-base mb-3">
|
||||
{titleValue || `Product ${product.productIndex + 1}`}
|
||||
</h4>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[180px]">Field</TableHead>
|
||||
<TableHead>Original Value</TableHead>
|
||||
<TableHead>Corrected Value</TableHead>
|
||||
<TableHead className="text-right">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{product.changes.map((change, j) => {
|
||||
const field = fields.find(f => f.key === change.field);
|
||||
const fieldLabel = field ? field.label : change.field;
|
||||
const isReverted = isChangeReverted(product.productIndex, change.field);
|
||||
|
||||
// Get highlighted differences
|
||||
const { originalHtml, correctedHtml } = getFieldDisplayValueWithHighlight(
|
||||
change.field,
|
||||
change.original,
|
||||
change.corrected
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={`change-${j}`}>
|
||||
<TableCell className="font-medium">{fieldLabel}</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: originalHtml }}
|
||||
className={isReverted ? "font-medium" : ""}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: correctedHtml }}
|
||||
className={!isReverted ? "font-medium" : ""}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="mt-2">
|
||||
{isReverted ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
|
||||
disabled
|
||||
>
|
||||
<CheckIcon className="w-4 h-4 mr-1" />
|
||||
Reverted
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Call the revert function directly
|
||||
revertAiChange(product.productIndex, change.field);
|
||||
}}
|
||||
>
|
||||
Revert Change
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
{aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? (
|
||||
<div>
|
||||
<p className="mb-4">No changes were made, but the AI provided some warnings:</p>
|
||||
<ul className="list-disc pl-8 text-left">
|
||||
{aiValidationDetails.warnings.map((warning, i) => (
|
||||
<li key={`warning-${i}`} className="mb-2">{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p>No changes or warnings were suggested by the AI.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,12 +2,14 @@ import React, { useState } from 'react'
|
||||
import { useValidationState, Props } from '../hooks/useValidationState'
|
||||
import ValidationTable from './ValidationTable'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Loader2, X, Plus, Edit3 } from 'lucide-react'
|
||||
import { Loader2, X, Plus, Edit3, Sparkles, FileText } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useRsi } from '../../../hooks/useRsi'
|
||||
import { ProductSearchDialog } from '@/components/products/ProductSearchDialog'
|
||||
import SearchableTemplateSelect from './SearchableTemplateSelect'
|
||||
import { useAiValidation } from '../hooks/useAiValidation'
|
||||
import { AiValidationDialogs } from './AiValidationDialogs'
|
||||
|
||||
/**
|
||||
* ValidationContainer component - the main wrapper for the validation step
|
||||
@@ -53,9 +55,19 @@ const ValidationContainer = <T extends string>({
|
||||
templateState,
|
||||
saveTemplate,
|
||||
loadTemplates,
|
||||
setData
|
||||
setData,
|
||||
fields
|
||||
} = validationState
|
||||
|
||||
// Use AI validation hook
|
||||
const aiValidation = useAiValidation<T>(
|
||||
data,
|
||||
setData,
|
||||
fields,
|
||||
validationState.rowHook,
|
||||
validationState.tableHook
|
||||
);
|
||||
|
||||
const { translations } = useRsi<T>()
|
||||
|
||||
// State for product search dialog
|
||||
@@ -232,6 +244,37 @@ const ValidationContainer = <T extends string>({
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Show Prompt Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={aiValidation.showCurrentPrompt}
|
||||
disabled={aiValidation.isAiValidating}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Show Prompt
|
||||
</Button>
|
||||
|
||||
{/* AI Validate Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={aiValidation.handleAiValidation}
|
||||
disabled={aiValidation.isAiValidating || data.length === 0}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{aiValidation.isAiValidating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Validating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
AI Validate
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={hasErrors}
|
||||
onClick={handleNext}
|
||||
@@ -242,6 +285,20 @@ const ValidationContainer = <T extends string>({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Validation Dialogs */}
|
||||
<AiValidationDialogs
|
||||
aiValidationProgress={aiValidation.aiValidationProgress}
|
||||
aiValidationDetails={aiValidation.aiValidationDetails}
|
||||
currentPrompt={aiValidation.currentPrompt}
|
||||
setAiValidationProgress={aiValidation.setAiValidationProgress}
|
||||
setAiValidationDetails={aiValidation.setAiValidationDetails}
|
||||
setCurrentPrompt={aiValidation.setCurrentPrompt}
|
||||
revertAiChange={aiValidation.revertAiChange}
|
||||
isChangeReverted={aiValidation.isChangeReverted}
|
||||
getFieldDisplayValueWithHighlight={aiValidation.getFieldDisplayValueWithHighlight}
|
||||
fields={fields}
|
||||
/>
|
||||
|
||||
{/* Product Search Dialog */}
|
||||
<ProductSearchDialog
|
||||
isOpen={isProductSearchDialogOpen}
|
||||
|
||||
@@ -0,0 +1,640 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getApiUrl, RowData } from './useValidationState';
|
||||
import { Fields } from '../../../types';
|
||||
import { addErrorsAndRunHooks } from '../../ValidationStep/utils/dataMutations';
|
||||
import * as Diff from 'diff';
|
||||
|
||||
// Define interfaces for AI validation
|
||||
export interface ChangeDetail {
|
||||
field: string;
|
||||
original: any;
|
||||
corrected: any;
|
||||
}
|
||||
|
||||
export interface ProductChangeDetail {
|
||||
productIndex: number;
|
||||
title: string;
|
||||
changes: ChangeDetail[];
|
||||
}
|
||||
|
||||
export interface AiValidationDetails {
|
||||
changes: string[];
|
||||
warnings: string[];
|
||||
changeDetails: ProductChangeDetail[];
|
||||
isOpen: boolean;
|
||||
originalData?: RowData<string>[];
|
||||
}
|
||||
|
||||
export interface AiValidationProgress {
|
||||
isOpen: boolean;
|
||||
status: string;
|
||||
step: number;
|
||||
estimatedSeconds?: number;
|
||||
startTime?: Date;
|
||||
promptLength?: number;
|
||||
elapsedSeconds?: number;
|
||||
progressPercent?: number;
|
||||
}
|
||||
|
||||
export interface CurrentPrompt {
|
||||
isOpen: boolean;
|
||||
prompt: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// Declare global interface for the timer
|
||||
declare global {
|
||||
interface Window {
|
||||
aiValidationTimer?: NodeJS.Timeout;
|
||||
}
|
||||
}
|
||||
|
||||
export const useAiValidation = <T extends string>(
|
||||
data: RowData<T>[],
|
||||
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
|
||||
fields: Fields<T>,
|
||||
rowHook?: (row: RowData<T>) => Promise<RowData<T>>,
|
||||
tableHook?: (data: RowData<T>[]) => Promise<RowData<T>[]>
|
||||
) => {
|
||||
// State for AI validation
|
||||
const [isAiValidating, setIsAiValidating] = useState(false);
|
||||
const [aiValidationDetails, setAiValidationDetails] = useState<AiValidationDetails>({
|
||||
changes: [],
|
||||
warnings: [],
|
||||
changeDetails: [],
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
const [aiValidationProgress, setAiValidationProgress] = useState<AiValidationProgress>({
|
||||
isOpen: false,
|
||||
status: "",
|
||||
step: 0,
|
||||
});
|
||||
|
||||
const [currentPrompt, setCurrentPrompt] = useState<CurrentPrompt>({
|
||||
isOpen: false,
|
||||
prompt: null,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
// Track reverted changes
|
||||
const [revertedChanges, setRevertedChanges] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get field display value
|
||||
const getFieldDisplayValue = useCallback((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);
|
||||
}, [fields]);
|
||||
|
||||
// Function to highlight differences between two text values
|
||||
const highlightDifferences = useCallback((original: string | null | undefined, corrected: string | null | undefined): { originalHtml: string, correctedHtml: string } => {
|
||||
// Handle null/undefined values
|
||||
let originalStr = original === null || original === undefined ? '' : String(original);
|
||||
let correctedStr = corrected === null || corrected === undefined ? '' : String(corrected);
|
||||
|
||||
// If they're identical, return without highlighting
|
||||
if (originalStr === correctedStr) {
|
||||
return {
|
||||
originalHtml: originalStr,
|
||||
correctedHtml: correctedStr
|
||||
};
|
||||
}
|
||||
|
||||
const diff = Diff.diffWords(originalStr, correctedStr);
|
||||
|
||||
let originalHtml = '';
|
||||
let correctedHtml = '';
|
||||
|
||||
diff.forEach((part: Diff.Change) => {
|
||||
// Create escaped HTML
|
||||
const escapedValue = part.value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
if (part.added) {
|
||||
// Added parts only show in the corrected version (green)
|
||||
correctedHtml += `<span class="text-green-600 font-medium">${escapedValue}</span>`;
|
||||
} else if (part.removed) {
|
||||
// Removed parts only show in the original version (red)
|
||||
originalHtml += `<span class="text-red-600 font-medium">${escapedValue}</span>`;
|
||||
} else {
|
||||
// Unchanged parts show in both versions
|
||||
originalHtml += escapedValue;
|
||||
correctedHtml += escapedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
originalHtml,
|
||||
correctedHtml
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Function to get field display value with highlighted differences
|
||||
const getFieldDisplayValueWithHighlight = useCallback((fieldKey: string, originalValue: any, correctedValue: any): { originalHtml: string, correctedHtml: string } => {
|
||||
const originalDisplay = getFieldDisplayValue(fieldKey, originalValue);
|
||||
const correctedDisplay = getFieldDisplayValue(fieldKey, correctedValue);
|
||||
|
||||
return highlightDifferences(originalDisplay, correctedDisplay);
|
||||
}, [getFieldDisplayValue, highlightDifferences]);
|
||||
|
||||
// Function to check if a change has been reverted
|
||||
const isChangeReverted = useCallback((productIndex: number, fieldKey: string): boolean => {
|
||||
const revertKey = `${productIndex}:${fieldKey}`;
|
||||
return revertedChanges.has(revertKey);
|
||||
}, [revertedChanges]);
|
||||
|
||||
// Function to revert a specific AI validation change
|
||||
const revertAiChange = useCallback((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 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 ${currentDataIndex}`, {
|
||||
originalIndex: productIndex,
|
||||
currentIndex: currentDataIndex,
|
||||
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];
|
||||
newData[currentDataIndex] = {
|
||||
...newData[currentDataIndex],
|
||||
[fieldKey]: originalValue
|
||||
};
|
||||
|
||||
// Update the data state
|
||||
setData(newData);
|
||||
|
||||
// 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.success("Change reverted");
|
||||
}, [aiValidationDetails.originalData, data, setData]);
|
||||
|
||||
// Function to show current prompt
|
||||
const showCurrentPrompt = useCallback(async () => {
|
||||
try {
|
||||
setCurrentPrompt(prev => ({ ...prev, isLoading: true, isOpen: true }));
|
||||
|
||||
// Debug log the data being sent
|
||||
console.log('Sending products data:', {
|
||||
dataLength: data.length,
|
||||
firstProduct: data[0],
|
||||
lastProduct: data[data.length - 1]
|
||||
});
|
||||
|
||||
// Clean the data to ensure we only send what's needed
|
||||
const cleanedData = data.map(item => {
|
||||
const { __errors, __index, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
|
||||
console.log('Cleaned data sample:', {
|
||||
length: cleanedData.length,
|
||||
firstProduct: cleanedData[0],
|
||||
lastProduct: cleanedData[cleanedData.length - 1]
|
||||
});
|
||||
|
||||
// Use POST to send products in request body
|
||||
const response = await fetch(`${getApiUrl()}/ai-validation/debug`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ products: cleanedData })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get prompt: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Debug response:', result);
|
||||
|
||||
// Check for different possible property names based on the API response
|
||||
const promptContent = result.sampleFullPrompt || result.basePrompt || result.prompt;
|
||||
|
||||
if (promptContent) {
|
||||
setCurrentPrompt(prev => ({
|
||||
...prev,
|
||||
prompt: promptContent,
|
||||
isLoading: false
|
||||
}));
|
||||
} else {
|
||||
throw new Error('No prompt returned from server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting prompt:', error);
|
||||
toast.error("Failed to get current prompt");
|
||||
setCurrentPrompt(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
prompt: "Error loading prompt"
|
||||
}));
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
// Main AI validation function
|
||||
const handleAiValidation = useCallback(async () => {
|
||||
try {
|
||||
if (isAiValidating) return;
|
||||
|
||||
// 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({
|
||||
isOpen: true,
|
||||
status: "Preparing data for validation...",
|
||||
step: 1,
|
||||
startTime,
|
||||
elapsedSeconds: 0,
|
||||
progressPercent: 0,
|
||||
...(aiValidationProgress.estimatedSeconds ? {
|
||||
estimatedSeconds: aiValidationProgress.estimatedSeconds,
|
||||
promptLength: aiValidationProgress.promptLength
|
||||
} : {})
|
||||
});
|
||||
|
||||
console.log('Sending data for validation:', data);
|
||||
|
||||
// Set up progress update interval
|
||||
window.aiValidationTimer = setInterval(() => {
|
||||
setAiValidationProgress(prev => {
|
||||
if (!prev.startTime) return prev;
|
||||
|
||||
const now = new Date();
|
||||
const elapsedSeconds = Math.floor((now.getTime() - prev.startTime.getTime()) / 1000);
|
||||
|
||||
// Calculate progress percentage based on step and elapsed time
|
||||
let progressPercent = 0;
|
||||
|
||||
if (prev.estimatedSeconds && prev.estimatedSeconds > 0) {
|
||||
// If we have an estimated time, use that for progress calculation
|
||||
progressPercent = Math.min(95, (elapsedSeconds / prev.estimatedSeconds) * 100);
|
||||
console.log('Using time-based progress:', progressPercent);
|
||||
} else {
|
||||
// Otherwise use step-based progress with some time-based adjustment
|
||||
const baseProgress = (prev.step / 5) * 100;
|
||||
const timeAdjustment = Math.min(20, elapsedSeconds * 0.5);
|
||||
const stepProgress = prev.step === 1 ? timeAdjustment : 0;
|
||||
progressPercent = Math.min(95, baseProgress + stepProgress);
|
||||
console.log('Using step-based progress:', progressPercent);
|
||||
}
|
||||
|
||||
// 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) as unknown as NodeJS.Timeout;
|
||||
|
||||
// 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(`${getApiUrl()}/ai-validation/debug`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ products: cleanedData })
|
||||
});
|
||||
|
||||
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) {
|
||||
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);
|
||||
// Continue without an estimate
|
||||
}
|
||||
}
|
||||
|
||||
setAiValidationProgress(prev => ({
|
||||
...prev,
|
||||
status: "Sending data to AI service...",
|
||||
step: 2,
|
||||
estimatedSeconds: prev.estimatedSeconds,
|
||||
promptLength: prev.promptLength
|
||||
}));
|
||||
|
||||
const response = await fetch(`${getApiUrl()}/ai-validation/validate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ products: cleanedData }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('AI validation response:', result);
|
||||
|
||||
if (!result.success) {
|
||||
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.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
|
||||
}));
|
||||
}
|
||||
|
||||
// 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, 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') {
|
||||
// Split comma-separated values and trim each value
|
||||
processed[key] = processed[key].split(',').map((v: string) => v.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle select fields (convert labels to values if needed)
|
||||
if ((fieldConfig?.fieldType.type === 'select' || fieldConfig?.fieldType.type === 'multi-select') &&
|
||||
'options' in fieldConfig.fieldType) {
|
||||
const options = fieldConfig.fieldType.options || [];
|
||||
|
||||
// Handle single value (select)
|
||||
if (typeof processed[key] === 'string' && fieldConfig.fieldType.type === 'select') {
|
||||
// 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 (case insensitive)
|
||||
const matchingOption = options.find(opt =>
|
||||
typeof processed[key] === 'string' && typeof opt.label === 'string' &&
|
||||
opt.label.toLowerCase() === processed[key].toLowerCase()
|
||||
);
|
||||
if (matchingOption) {
|
||||
// Convert label to ID
|
||||
processed[key] = matchingOption.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (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;
|
||||
}
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return processed;
|
||||
});
|
||||
|
||||
console.log('About to update data with AI corrections:', {
|
||||
originalDataSample: data.slice(0, 2),
|
||||
processedDataSample: processedData.slice(0, 2),
|
||||
correctionCount: result.changes?.length || 0
|
||||
});
|
||||
|
||||
// First validate the new data to ensure all validation rules are applied
|
||||
try {
|
||||
// Validate the data with the hooks
|
||||
const validatedData = await addErrorsAndRunHooks<T>(
|
||||
processedData,
|
||||
fields,
|
||||
rowHook,
|
||||
tableHook
|
||||
);
|
||||
|
||||
// Update the component state with the validated data
|
||||
setData(validatedData as RowData<T>[]);
|
||||
|
||||
// Force a small delay to ensure React updates the state before showing the results
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
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(processedData);
|
||||
|
||||
// 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,
|
||||
status: "Validation complete!",
|
||||
step: 5,
|
||||
estimatedSeconds: prev.estimatedSeconds,
|
||||
promptLength: prev.promptLength
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('AI Validation Error:', error);
|
||||
toast.error(error instanceof Error ? error.message : "An error occurred during AI validation");
|
||||
setAiValidationProgress(prev => ({
|
||||
...prev,
|
||||
status: "Validation failed",
|
||||
step: -1,
|
||||
estimatedSeconds: prev.estimatedSeconds,
|
||||
promptLength: prev.promptLength
|
||||
}));
|
||||
} 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
|
||||
estimatedSeconds: prev.estimatedSeconds,
|
||||
promptLength: prev.promptLength,
|
||||
elapsedSeconds: prev.elapsedSeconds
|
||||
}));
|
||||
}
|
||||
}, [isAiValidating, data, aiValidationProgress.estimatedSeconds, aiValidationProgress.promptLength, fields, rowHook, tableHook]);
|
||||
|
||||
return {
|
||||
isAiValidating,
|
||||
aiValidationDetails,
|
||||
aiValidationProgress,
|
||||
currentPrompt,
|
||||
handleAiValidation,
|
||||
showCurrentPrompt,
|
||||
setAiValidationDetails,
|
||||
setAiValidationProgress,
|
||||
setCurrentPrompt,
|
||||
getFieldDisplayValue,
|
||||
getFieldDisplayValueWithHighlight,
|
||||
revertAiChange,
|
||||
isChangeReverted
|
||||
};
|
||||
};
|
||||
@@ -782,6 +782,10 @@ export const useValidationState = <T extends string>({
|
||||
isValidatingUpc,
|
||||
|
||||
// Fields reference
|
||||
fields
|
||||
fields,
|
||||
|
||||
// Hooks
|
||||
rowHook,
|
||||
tableHook
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
10
package-lock.json
generated
10
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"diff": "^7.0.0",
|
||||
"shadcn": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -75,6 +76,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"diff": "^7.0.0",
|
||||
"shadcn": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user