AI tweaks and make column name matching case insensitive
This commit is contained in:
@@ -33,15 +33,19 @@ Respond in the following JSON format:
|
|||||||
router.post('/validate', async (req, res) => {
|
router.post('/validate', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { products } = req.body;
|
const { products } = req.body;
|
||||||
|
console.log('🔍 Received products for validation:', JSON.stringify(products, null, 2));
|
||||||
|
|
||||||
if (!Array.isArray(products)) {
|
if (!Array.isArray(products)) {
|
||||||
|
console.error('❌ Invalid input: products is not an array');
|
||||||
return res.status(400).json({ error: 'Products must be an array' });
|
return res.status(400).json({ error: 'Products must be an array' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = createValidationPrompt(products);
|
const prompt = createValidationPrompt(products);
|
||||||
|
console.log('📝 Generated prompt:', prompt);
|
||||||
|
|
||||||
|
console.log('🤖 Sending request to OpenAI...');
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4-turbo-preview",
|
model: "gpt-4o-mini",
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -52,19 +56,50 @@ router.post('/validate', async (req, res) => {
|
|||||||
content: prompt
|
content: prompt
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
temperature: 0.3, // Lower temperature for more consistent results
|
temperature: 0.3,
|
||||||
max_tokens: 4000,
|
max_tokens: 4000,
|
||||||
response_format: { type: "json_object" }
|
response_format: { type: "json_object" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const aiResponse = JSON.parse(completion.choices[0].message.content);
|
console.log('✅ Received response from OpenAI');
|
||||||
|
const rawResponse = completion.choices[0].message.content;
|
||||||
|
console.log('📄 Raw AI response:', rawResponse);
|
||||||
|
|
||||||
|
const aiResponse = JSON.parse(rawResponse);
|
||||||
|
console.log('🔄 Parsed AI response:', JSON.stringify(aiResponse, null, 2));
|
||||||
|
|
||||||
|
// Compare original and corrected data
|
||||||
|
if (aiResponse.correctedData) {
|
||||||
|
console.log('📊 Changes summary:');
|
||||||
|
products.forEach((original, index) => {
|
||||||
|
const corrected = aiResponse.correctedData[index];
|
||||||
|
if (corrected) {
|
||||||
|
const changes = Object.keys(corrected).filter(key =>
|
||||||
|
JSON.stringify(original[key]) !== JSON.stringify(corrected[key])
|
||||||
|
);
|
||||||
|
if (changes.length > 0) {
|
||||||
|
console.log(`\nProduct ${index + 1} changes:`);
|
||||||
|
changes.forEach(key => {
|
||||||
|
console.log(` ${key}:`);
|
||||||
|
console.log(` - Original: ${JSON.stringify(original[key])}`);
|
||||||
|
console.log(` - Corrected: ${JSON.stringify(corrected[key])}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
...aiResponse
|
...aiResponse
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI Validation Error:', error);
|
console.error('❌ AI Validation Error:', error);
|
||||||
|
console.error('Error details:', {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || 'Error during AI validation'
|
error: error.message || 'Error during AI validation'
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ export const findMatch = <T extends string>(
|
|||||||
fields: Fields<T>,
|
fields: Fields<T>,
|
||||||
autoMapDistance: number,
|
autoMapDistance: number,
|
||||||
): T | undefined => {
|
): T | undefined => {
|
||||||
|
const headerLower = header.toLowerCase()
|
||||||
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
|
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
|
||||||
const distance = Math.min(
|
const distance = Math.min(
|
||||||
...[
|
...[
|
||||||
lavenstein(field.key, header),
|
lavenstein(field.key.toLowerCase(), headerLower),
|
||||||
...(field.alternateMatches?.map((alternate) => lavenstein(alternate, header)) || []),
|
...(field.alternateMatches?.map((alternate) => lavenstein(alternate.toLowerCase(), headerLower)) || []),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return distance < acc.distance || acc.distance === undefined
|
return distance < acc.distance || acc.distance === undefined
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
import config from "@/config"
|
import config from "@/config"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
|
||||||
type Props<T extends string> = {
|
type Props<T extends string> = {
|
||||||
initialData: (Data<T> & Meta)[]
|
initialData: (Data<T> & Meta)[]
|
||||||
@@ -706,6 +714,15 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
|||||||
const [isSubmitting, setSubmitting] = useState(false)
|
const [isSubmitting, setSubmitting] = useState(false)
|
||||||
const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null)
|
const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null)
|
||||||
const [isAiValidating, setIsAiValidating] = useState(false)
|
const [isAiValidating, setIsAiValidating] = useState(false)
|
||||||
|
const [aiValidationDetails, setAiValidationDetails] = useState<{
|
||||||
|
changes: string[];
|
||||||
|
warnings: string[];
|
||||||
|
isOpen: boolean;
|
||||||
|
}>({
|
||||||
|
changes: [],
|
||||||
|
warnings: [],
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Memoize filtered data to prevent recalculation on every render
|
// Memoize filtered data to prevent recalculation on every render
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
@@ -949,76 +966,78 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
|||||||
// Add AI validation function
|
// Add AI validation function
|
||||||
const handleAiValidation = async () => {
|
const handleAiValidation = async () => {
|
||||||
try {
|
try {
|
||||||
setIsAiValidating(true)
|
setIsAiValidating(true);
|
||||||
|
console.log('Sending data for AI validation:', data);
|
||||||
|
|
||||||
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, {
|
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ products: data }),
|
body: JSON.stringify({ products: data }),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('AI validation failed')
|
throw new Error('AI validation failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json();
|
||||||
|
console.log('AI validation response:', result);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'AI validation failed')
|
throw new Error(result.error || 'AI validation failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the data with AI suggestions
|
// Update the data with AI suggestions
|
||||||
if (result.correctedData && Array.isArray(result.correctedData)) {
|
if (result.correctedData && Array.isArray(result.correctedData)) {
|
||||||
|
// Log the differences
|
||||||
|
data.forEach((original, index) => {
|
||||||
|
const corrected = result.correctedData[index];
|
||||||
|
if (corrected) {
|
||||||
|
const changes = Object.keys(corrected).filter(key => {
|
||||||
|
const originalValue = original[key as keyof typeof original];
|
||||||
|
const correctedValue = corrected[key as keyof typeof corrected];
|
||||||
|
return JSON.stringify(originalValue) !== JSON.stringify(correctedValue);
|
||||||
|
});
|
||||||
|
if (changes.length > 0) {
|
||||||
|
console.log(`Changes for row ${index + 1}:`, changes.map(key => ({
|
||||||
|
field: key,
|
||||||
|
original: original[key as keyof typeof original],
|
||||||
|
corrected: corrected[key as keyof typeof corrected]
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Preserve the __index and __errors from the original data
|
// Preserve the __index and __errors from the original data
|
||||||
const newData = result.correctedData.map((item: any, idx: number) => ({
|
const newData = result.correctedData.map((item: any, idx: number) => ({
|
||||||
...item,
|
...item,
|
||||||
__index: data[idx]?.__index,
|
__index: data[idx]?.__index,
|
||||||
__errors: data[idx]?.__errors,
|
__errors: data[idx]?.__errors,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
// Update the data and run validations
|
// Update the data and run validations
|
||||||
await updateData(newData)
|
await updateData(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show changes and warnings
|
// Show changes and warnings in dialog
|
||||||
if (result.changes?.length) {
|
setAiValidationDetails({
|
||||||
toast({
|
changes: result.changes || [],
|
||||||
title: "AI Validation Changes",
|
warnings: result.warnings || [],
|
||||||
description: (
|
isOpen: true,
|
||||||
<div className="mt-2 space-y-2">
|
});
|
||||||
{result.changes.map((change: string, i: number) => (
|
|
||||||
<div key={i} className="text-sm">• {change}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.warnings?.length) {
|
|
||||||
toast({
|
|
||||||
title: "AI Validation Warnings",
|
|
||||||
description: (
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
{result.warnings.map((warning: string, i: number) => (
|
|
||||||
<div key={i} className="text-sm">• {warning}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI Validation Error:', error)
|
console.error('AI Validation Error:', error);
|
||||||
toast({
|
toast({
|
||||||
title: "AI Validation Error",
|
title: "AI Validation Error",
|
||||||
description: error instanceof Error ? error.message : "An error occurred during AI validation",
|
description: error instanceof Error ? error.message : "An error occurred during AI validation",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsAiValidating(false)
|
setIsAiValidating(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
||||||
@@ -1055,6 +1074,47 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
<Dialog
|
||||||
|
open={aiValidationDetails.isOpen}
|
||||||
|
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>AI Validation Results</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Review the changes and warnings suggested by the AI
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<ScrollArea className="max-h-[60vh]">
|
||||||
|
{aiValidationDetails.changes.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="font-semibold mb-2">Changes Made:</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{aiValidationDetails.changes.map((change, i) => (
|
||||||
|
<li key={i} className="flex gap-2">
|
||||||
|
<span className="text-green-500">✓</span>
|
||||||
|
<span>{change}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{aiValidationDetails.warnings.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-2">Warnings:</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{aiValidationDetails.warnings.map((warning, i) => (
|
||||||
|
<li key={i} className="flex gap-2">
|
||||||
|
<span className="text-yellow-500">⚠</span>
|
||||||
|
<span>{warning}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="px-8 pt-6">
|
<div className="px-8 pt-6">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "UPC",
|
label: "UPC",
|
||||||
key: "upc",
|
key: "upc",
|
||||||
description: "Universal Product Code/Barcode",
|
description: "Universal Product Code/Barcode",
|
||||||
alternateMatches: ["barcode", "bar code", "JAN", "EAN"],
|
alternateMatches: ["upc","UPC","barcode", "bar code", "JAN", "EAN"],
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 140,
|
width: 140,
|
||||||
validations: [
|
validations: [
|
||||||
@@ -94,7 +94,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "MSRP",
|
label: "MSRP",
|
||||||
key: "msrp",
|
key: "msrp",
|
||||||
description: "Manufacturer's Suggested Retail Price",
|
description: "Manufacturer's Suggested Retail Price",
|
||||||
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. Retail"],
|
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. Retail","msrp","MSRP"],
|
||||||
fieldType: {
|
fieldType: {
|
||||||
type: "input",
|
type: "input",
|
||||||
price: true
|
price: true
|
||||||
@@ -136,7 +136,7 @@ const BASE_IMPORT_FIELDS = [
|
|||||||
label: "Case Pack",
|
label: "Case Pack",
|
||||||
key: "case_qty",
|
key: "case_qty",
|
||||||
description: "Number of units per case",
|
description: "Number of units per case",
|
||||||
alternateMatches: ["mc qty"],
|
alternateMatches: ["mc qty","MC Qty","case qty","Case Qty"],
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 50,
|
width: 50,
|
||||||
validations: [
|
validations: [
|
||||||
|
|||||||
Reference in New Issue
Block a user