🔧 Gemini AI Integration: - Added Google Gemini API as default OCR service - Auto-configures from GEMINI_AI_API_KEY environment variable - Fixed Puter.js authentication issues - Enhanced OCR settings with connection testing 🎨 Redesigned Scanner Queue: - New thumbnail + content layout with checkbox overlay - Smart quantity management (duplicates increment quantity) - Complete card information display from database - Two-row action layout (primary/secondary actions) - Floating bottom toolbar for bulk actions - Real card images from database �� Enhanced User Experience: - Fixed Canvas2D performance warnings - Better error handling and fallbacks - Improved responsive design - Database confirmation indicators - Professional card scanning workflow 📱 Mobile Ready: - Optimized layouts for mobile scanning - Touch-friendly controls and interactions - Improved visual feedback and status indicators
212 lines
No EOL
7.5 KiB
JavaScript
212 lines
No EOL
7.5 KiB
JavaScript
import { sql } from '@vercel/postgres';
|
|
import { getUserFromRequest } from '../../../lib/permission-middleware';
|
|
|
|
export default async function handler(req, res) {
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
|
}
|
|
|
|
try {
|
|
const user = await getUserFromRequest(req);
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const {
|
|
name,
|
|
set,
|
|
setCode,
|
|
cardNumber,
|
|
game,
|
|
cardType,
|
|
rarity,
|
|
hp,
|
|
manaCost,
|
|
ocrData
|
|
} = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Card name is required' });
|
|
}
|
|
|
|
console.log(`🔍 Looking for card: "${name}" | Set: "${set || setCode}" | Number: "${cardNumber}" | Game: "${game}"`);
|
|
|
|
// First, try exact match by name, set, and card number (most specific)
|
|
let existingCard = null;
|
|
|
|
if ((set || setCode) && cardNumber) {
|
|
console.log('🎯 Trying exact match with card number...');
|
|
const exactQuery = sql`
|
|
SELECT * FROM cards
|
|
WHERE LOWER(name) = LOWER(${name})
|
|
AND (LOWER(set_name) = LOWER(${set || setCode}) OR LOWER(set_code) = LOWER(${setCode || set}))
|
|
AND LOWER(card_number) = LOWER(${cardNumber})
|
|
LIMIT 1
|
|
`;
|
|
|
|
const exactResult = await exactQuery;
|
|
if (exactResult.rows.length > 0) {
|
|
existingCard = exactResult.rows[0];
|
|
console.log('✅ Found exact match with card number:', existingCard.name);
|
|
}
|
|
}
|
|
|
|
// Second, try exact match by name and set (without card number)
|
|
if (!existingCard && (set || setCode)) {
|
|
console.log('🎯 Trying exact match by name and set...');
|
|
const setQuery = set ?
|
|
sql`SELECT * FROM cards WHERE LOWER(name) = LOWER(${name}) AND (LOWER(set_name) = LOWER(${set}) OR LOWER(set_code) = LOWER(${setCode || set})) LIMIT 1` :
|
|
sql`SELECT * FROM cards WHERE LOWER(name) = LOWER(${name}) AND LOWER(set_code) = LOWER(${setCode}) LIMIT 1`;
|
|
|
|
const setResult = await setQuery;
|
|
if (setResult.rows.length > 0) {
|
|
existingCard = setResult.rows[0];
|
|
console.log('✅ Found exact match by name and set:', existingCard.name);
|
|
}
|
|
}
|
|
|
|
// Third, try exact name match (any set)
|
|
if (!existingCard) {
|
|
console.log('🎯 Trying exact name match (any set)...');
|
|
const nameQuery = sql`
|
|
SELECT * FROM cards
|
|
WHERE LOWER(name) = LOWER(${name})
|
|
ORDER BY
|
|
CASE WHEN game = ${game || 'UNKNOWN'} THEN 1 ELSE 2 END,
|
|
created_at DESC
|
|
LIMIT 1
|
|
`;
|
|
|
|
const nameResult = await nameQuery;
|
|
if (nameResult.rows.length > 0) {
|
|
existingCard = nameResult.rows[0];
|
|
console.log('✅ Found exact name match:', existingCard.name);
|
|
}
|
|
}
|
|
|
|
// Fourth, try fuzzy name matching with game preference
|
|
if (!existingCard) {
|
|
console.log('🎯 Trying fuzzy name matching...');
|
|
const fuzzyResult = await sql`
|
|
SELECT * FROM cards
|
|
WHERE LOWER(name) ILIKE LOWER(${`%${name}%`})
|
|
ORDER BY
|
|
CASE
|
|
WHEN LOWER(name) = LOWER(${name}) THEN 1
|
|
WHEN LOWER(name) LIKE LOWER(${name + '%'}) THEN 2
|
|
WHEN LOWER(name) LIKE LOWER(${'%' + name + '%'}) THEN 3
|
|
ELSE 4
|
|
END,
|
|
CASE WHEN game = ${game || 'UNKNOWN'} THEN 1 ELSE 2 END,
|
|
LENGTH(name)
|
|
LIMIT 5
|
|
`;
|
|
|
|
if (fuzzyResult.rows.length > 0) {
|
|
console.log(`🔍 Found ${fuzzyResult.rows.length} fuzzy matches`);
|
|
|
|
// If we have high confidence and an exact match, use it
|
|
const exactFuzzyMatch = fuzzyResult.rows.find(row =>
|
|
row.name.toLowerCase() === name.toLowerCase()
|
|
);
|
|
|
|
if (exactFuzzyMatch && ocrData?.confidence >= 80) {
|
|
existingCard = exactFuzzyMatch;
|
|
console.log('✅ Using high-confidence fuzzy exact match:', existingCard.name);
|
|
} else if (ocrData?.confidence < 80 && fuzzyResult.rows.length > 1) {
|
|
// Low confidence with multiple matches - let user choose
|
|
console.log('⚠️ Multiple matches with low confidence - requiring user selection');
|
|
return res.status(200).json({
|
|
card: null,
|
|
matches: fuzzyResult.rows.map(card => ({
|
|
id: card.id,
|
|
name: card.name,
|
|
set_name: card.set_name,
|
|
set_code: card.set_code,
|
|
card_number: card.card_number,
|
|
game: card.game,
|
|
rarity: card.rarity,
|
|
image_url: card.image_url
|
|
})),
|
|
needsUserSelection: true,
|
|
message: `Found ${fuzzyResult.rows.length} possible matches for "${name}". Please select the correct card.`
|
|
});
|
|
} else {
|
|
// Use the best match
|
|
existingCard = fuzzyResult.rows[0];
|
|
console.log('✅ Using best fuzzy match:', existingCard.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found an existing card, return it
|
|
if (existingCard) {
|
|
console.log('🎉 Returning existing card:', existingCard.name);
|
|
return res.status(200).json({
|
|
card: existingCard,
|
|
isExisting: true,
|
|
message: `Found existing card: "${existingCard.name}"`
|
|
});
|
|
}
|
|
|
|
// If no existing card found, decide whether to create a new one
|
|
const confidenceThreshold = 75; // Increased threshold for better accuracy
|
|
|
|
if (!ocrData || ocrData.confidence < confidenceThreshold) {
|
|
console.log(`❌ No match found and confidence too low (${ocrData?.confidence || 0}% < ${confidenceThreshold}%)`);
|
|
return res.status(200).json({
|
|
card: null,
|
|
matches: [],
|
|
needsUserInput: true,
|
|
message: `Could not find card "${name}" in database and confidence is low (${ocrData?.confidence || 0}%). Please verify the card name and try again.`
|
|
});
|
|
}
|
|
|
|
// Create new card entry with enhanced data
|
|
console.log('🆕 Creating new card from OCR data...');
|
|
const newCardResult = await sql`
|
|
INSERT INTO cards (
|
|
name, set_name, set_code, card_number, rarity, game,
|
|
mana_cost, cmc, card_type, colors, oracle_text,
|
|
power, toughness, image_url, stock_image_url,
|
|
current_price, market_price, scryfall_id, verified
|
|
) VALUES (
|
|
${name.trim()},
|
|
${set || null},
|
|
${setCode || null},
|
|
${cardNumber || null},
|
|
${rarity || null},
|
|
${game || 'UNKNOWN'},
|
|
${manaCost || null},
|
|
${null}, -- cmc (calculated from mana cost)
|
|
${cardType || null},
|
|
${null}, -- colors (unknown from OCR)
|
|
${ocrData?.rawText || null}, -- Store OCR text in oracle_text temporarily
|
|
${hp || null}, -- power (HP for Pokemon)
|
|
${null}, -- toughness
|
|
${null}, -- image_url (to be fetched later)
|
|
${null}, -- stock_image_url
|
|
${null}, -- current_price
|
|
${null}, -- market_price
|
|
${null}, -- scryfall_id (to be populated later)
|
|
${false} -- not verified since it's from OCR
|
|
)
|
|
RETURNING *
|
|
`;
|
|
|
|
const newCard = newCardResult.rows[0];
|
|
|
|
// Log the OCR creation for potential review
|
|
console.log(`✅ Created new card from OCR: ${name} (${game}) - Confidence: ${ocrData?.confidence}%`);
|
|
|
|
return res.status(201).json({
|
|
card: newCard,
|
|
isExisting: false,
|
|
message: `Created new card "${name}" from scan data. This card may need verification.`
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error in find-or-create card:', error);
|
|
return res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
}
|