deckhearth/pages/api/cards/find-or-create.js
Randall Stillwell afb79c57d9 Major Scanner Improvements
🔧 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
2025-07-29 14:19:48 -05:00

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' });
}
}