From 7a43428e76100c1e34417ca7c2b484900addaf81 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 3 Mar 2025 21:46:22 -0500 Subject: [PATCH] More validate step changes to get closer to original, made the default step now --- .../MatchColumnsStep/MatchColumnsStep.tsx | 28 +- .../src/steps/UploadFlow.tsx | 33 +- .../steps/ValidationStep/ValidationStep.tsx | 178 +++-- .../components/AiValidationDialogs.tsx | 248 +++++++ .../components/ValidationContainer.tsx | 63 +- .../hooks/useAiValidation.tsx | 640 ++++++++++++++++++ .../hooks/useValidationState.tsx | 6 +- inventory/tsconfig.tsbuildinfo | 2 +- package-lock.json | 10 + package.json | 1 + 10 files changed, 1062 insertions(+), 147 deletions(-) create mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx create mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index 4a1407c..1cb8110 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -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(({ )} - -
-
- - setGlobalSelections(prev => ({ ...prev, useNewValidation: checked })) - } - /> - -
- -
+ diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx index 4c4659c..fffe1c4 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/UploadFlow.tsx @@ -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 ( - { - 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 ( - { @@ -249,7 +221,6 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { globalSelections: state.globalSelections }); }} - globalSelections={state.globalSelections} isFromScratch={state.isFromScratch} /> ) diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index 820b6b8..3e8aca5 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -1939,6 +1939,7 @@ export const ValidationStep = ({ file, onBack, onNext, + globalSelections, isFromScratch }: Props) => { const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi(); @@ -1971,6 +1972,93 @@ export const ValidationStep = ({ // Reference to store hook timeouts to prevent race conditions const hookTimeoutRef = useRef(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).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; + } + + 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; + } + + return field; + }); + }, [fields, productLines, sublines]); + // Define updateData function for validation hooks const updateData = useCallback( async (rows: (Data & ExtendedMeta)[], indexes?: number[]) => { @@ -2217,96 +2305,6 @@ export const ValidationStep = ({ // Track which rows are currently being validated to maintain loading state const rowsBeingValidatedRef = useRef>(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).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; - } - - 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; - } - - return field; - }); - }, [fields, productLines, sublines]); - // Define the validation function - not using useCallback to avoid dependency issues const validateUpcAndGenerateItemNumbers = async (forceValidation = false) => { let newData = [...data]; diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx new file mode 100644 index 0000000..64215e6 --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx @@ -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>; + setAiValidationDetails: React.Dispatch>; + setCurrentPrompt: React.Dispatch>; + 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 = ({ + aiValidationProgress, + aiValidationDetails, + currentPrompt, + setAiValidationProgress, + setAiValidationDetails, + setCurrentPrompt, + revertAiChange, + isChangeReverted, + getFieldDisplayValueWithHighlight, + fields +}) => { + return ( + <> + {/* Current Prompt Dialog */} + setCurrentPrompt(prev => ({ ...prev, isOpen: open }))} + > + + + Current AI Prompt + + This is the exact prompt that would be sent to the AI for validation + + + + {currentPrompt.isLoading ? ( +
+ +
+ ) : ( + {currentPrompt.prompt} + )} +
+
+
+ + {/* AI Validation Progress Dialog */} + { + // Only allow closing if validation failed + if (!open && aiValidationProgress.step === -1) { + setAiValidationProgress(prev => ({ ...prev, isOpen: false })); + } + }} + > + + + AI Validation Progress + +
+
+
+
+
+
+
+
+ {aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`} +
+
+

+ {aiValidationProgress.status} +

+ {(() => { + // 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 && ( +
+ {(() => { + // 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 && ( +

+ Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters +

+ )} +
+ ); + })()} +
+ +
+ + {/* AI Validation Results Dialog */} + setAiValidationDetails(prev => ({ ...prev, isOpen: open }))} + > + + + AI Validation Results + + Review the changes and warnings suggested by the AI + + + + {aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? ( +
+

Detailed Changes:

+ {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 ( +
+

+ {titleValue || `Product ${product.productIndex + 1}`} +

+ + + + Field + Original Value + Corrected Value + Action + + + + {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 ( + + {fieldLabel} + +
+ + +
+ + +
+ {isReverted ? ( + + ) : ( + + )} +
+
+ + ); + })} + +
+
+ ); + })} +
+ ) : ( +
+ {aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? ( +
+

No changes were made, but the AI provided some warnings:

+
    + {aiValidationDetails.warnings.map((warning, i) => ( +
  • {warning}
  • + ))} +
+
+ ) : ( +

No changes or warnings were suggested by the AI.

+ )} +
+ )} +
+
+
+ + ); +}; \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx index 19e6fe2..9240f1b 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -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 = ({ templateState, saveTemplate, loadTemplates, - setData + setData, + fields } = validationState + // Use AI validation hook + const aiValidation = useAiValidation( + data, + setData, + fields, + validationState.rowHook, + validationState.tableHook + ); + const { translations } = useRsi() // State for product search dialog @@ -232,6 +244,37 @@ const ValidationContainer = ({ )}
+ {/* Show Prompt Button */} + + + {/* AI Validate Button */} + +
+ {/* AI Validation Dialogs */} + + {/* Product Search Dialog */} ({ ) } -export default ValidationContainer \ No newline at end of file +export default ValidationContainer \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx new file mode 100644 index 0000000..17385fd --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx @@ -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[]; +} + +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 = ( + data: RowData[], + setData: React.Dispatch[]>>, + fields: Fields, + rowHook?: (row: RowData) => Promise>, + tableHook?: (data: RowData[]) => Promise[]> +) => { + // State for AI validation + const [isAiValidating, setIsAiValidating] = useState(false); + const [aiValidationDetails, setAiValidationDetails] = useState({ + changes: [], + warnings: [], + changeDetails: [], + isOpen: false, + }); + + const [aiValidationProgress, setAiValidationProgress] = useState({ + isOpen: false, + status: "", + step: 0, + }); + + const [currentPrompt, setCurrentPrompt] = useState({ + isOpen: false, + prompt: null, + isLoading: false, + }); + + // Track reverted changes + const [revertedChanges, setRevertedChanges] = useState>(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, '''); + + if (part.added) { + // Added parts only show in the corrected version (green) + correctedHtml += `${escapedValue}`; + } else if (part.removed) { + // Removed parts only show in the original version (red) + originalHtml += `${escapedValue}`; + } 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)[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)[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( + processedData, + fields, + rowHook, + tableHook + ); + + // Update the component state with the validated data + setData(validatedData as RowData[]); + + // 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 + }; +}; \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx index 5444ea9..8b2d9fd 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx @@ -782,6 +782,10 @@ export const useValidationState = ({ isValidatingUpc, // Fields reference - fields + fields, + + // Hooks + rowHook, + tableHook } } \ No newline at end of file diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 2bbb564..a526d66 100644 --- a/inventory/tsconfig.tsbuildinfo +++ b/inventory/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/requireauth.tsx","./src/components/dashboard/bestsellers.tsx","./src/components/dashboard/forecastmetrics.tsx","./src/components/dashboard/inventoryhealthsummary.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/keymetricscharts.tsx","./src/components/dashboard/lowstockalerts.tsx","./src/components/dashboard/overstockmetrics.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/purchasemetrics.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/replenishmentmetrics.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/salesmetrics.tsx","./src/components/dashboard/stockmetrics.tsx","./src/components/dashboard/topoverstockedproducts.tsx","./src/components/dashboard/topreplenishproducts.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/dashboard/vendorperformance.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/forecasting.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/vendors.tsx","./src/types/products.ts","./src/types/status-codes.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/requireauth.tsx","./src/components/dashboard/bestsellers.tsx","./src/components/dashboard/forecastmetrics.tsx","./src/components/dashboard/inventoryhealthsummary.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/keymetricscharts.tsx","./src/components/dashboard/lowstockalerts.tsx","./src/components/dashboard/overstockmetrics.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/purchasemetrics.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/replenishmentmetrics.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/salesmetrics.tsx","./src/components/dashboard/stockmetrics.tsx","./src/components/dashboard/topoverstockedproducts.tsx","./src/components/dashboard/topreplenishproducts.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/dashboard/vendorperformance.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsearchdialog.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/utils.ts","./src/lib/react-spreadsheet-import/src/reactspreadsheetimport.tsx","./src/lib/react-spreadsheet-import/src/index.ts","./src/lib/react-spreadsheet-import/src/theme.ts","./src/lib/react-spreadsheet-import/src/translationsrsiprops.ts","./src/lib/react-spreadsheet-import/src/types.ts","./src/lib/react-spreadsheet-import/src/components/modalwrapper.tsx","./src/lib/react-spreadsheet-import/src/components/providers.tsx","./src/lib/react-spreadsheet-import/src/components/table.tsx","./src/lib/react-spreadsheet-import/src/hooks/usersi.ts","./src/lib/react-spreadsheet-import/src/steps/steps.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadflow.tsx","./src/lib/react-spreadsheet-import/src/steps/imageuploadstep/imageuploadstep.tsx","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/components/columngrid.tsx","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/components/matchicon.tsx","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/components/usertablecolumn.tsx","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/findmatch.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/setcolumn.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/lib/react-spreadsheet-import/src/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/lib/react-spreadsheet-import/src/steps/selectheaderstep/selectheaderstep.tsx","./src/lib/react-spreadsheet-import/src/steps/selectheaderstep/components/selectheadertable.tsx","./src/lib/react-spreadsheet-import/src/steps/selectheaderstep/components/columns.tsx","./src/lib/react-spreadsheet-import/src/steps/selectsheetstep/selectsheetstep.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadstep/uploadstep.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadstep/components/dropzone.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadstep/components/exampletable.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadstep/components/fadingoverlay.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadstep/components/columns.tsx","./src/lib/react-spreadsheet-import/src/steps/uploadstep/utils/generateexamplerow.ts","./src/lib/react-spreadsheet-import/src/steps/uploadstep/utils/getdropzoneborder.ts","./src/lib/react-spreadsheet-import/src/steps/uploadstep/utils/readfilesasync.ts","./src/lib/react-spreadsheet-import/src/steps/validationstep/validationstep.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstep/types.ts","./src/lib/react-spreadsheet-import/src/steps/validationstep/utils/datamutations.ts","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/index.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/savetemplatedialog.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/templatemanager.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/validationcell.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/validationcontainer.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/validationtable.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/cells/inputcell.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/cells/multiinputcell.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/components/cells/selectcell.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/hooks/useaivalidation.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/hooks/usefilters.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/hooks/usetemplates.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/hooks/usevalidation.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/types/index.ts","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/utils/errorutils.ts","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/utils/upcvalidation.ts","./src/lib/react-spreadsheet-import/src/steps/validationstepnew/utils/validationutils.ts","./src/lib/react-spreadsheet-import/src/utils/exceedsmaxrecords.ts","./src/lib/react-spreadsheet-import/src/utils/mapdata.ts","./src/lib/react-spreadsheet-import/src/utils/mapworkbook.ts","./src/lib/react-spreadsheet-import/src/utils/steps.ts","./src/pages/aivalidationdebug.tsx","./src/pages/analytics.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/forecasting.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/vendors.tsx","./src/types/globals.d.ts","./src/types/products.ts","./src/types/status-codes.ts"],"errors":true,"version":"5.6.3"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 78c491b..4fa1ae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 61f7aa1..4ca29a5 100644 --- a/package.json +++ b/package.json @@ -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": {