AI tweaks and make column name matching case insensitive

This commit is contained in:
2025-02-20 15:49:48 -05:00
parent 45a52cbc33
commit 7f7e6fdd1f
4 changed files with 142 additions and 46 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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">

View File

@@ -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: [