import { useState, useRef, useEffect, useMemo } from 'react'; import { toast } from 'react-hot-toast'; import { inventoryApi } from '@/lib/api'; import { Item } from '@/lib/db'; export function useAIExtraction(inventory: Item[], onComplete: (itemData: any) => void) { const [image, setImage] = useState(null); const [uploading, setUploading] = useState(false); const [extractedItems, setExtractedItems] = useState([]); const [editingIndex, setEditingIndex] = useState(null); const [mode, setMode] = useState<'item' | 'box'>('item'); const [isLive, setIsLive] = useState(false); const [extractedImageBlob, setExtractedImageBlob] = useState(null); const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); const fileInputRef = useRef(null); const existingTypes = useMemo( () => Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[], [inventory] ); const existingBoxes = useMemo( () => Array.from(new Set(inventory.map(i => i.box_label).filter(Boolean))).sort() as string[], [inventory] ); const startLiveCamera = async () => { try { setIsLive(true); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false }); if (videoRef.current) { videoRef.current.srcObject = stream; streamRef.current = stream; } } catch (err) { console.error("Camera access error:", err); toast.error("Could not access camera for live scan."); setIsLive(false); } }; const stopLiveCamera = () => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); streamRef.current = null; } setIsLive(false); }; const captureSnapshot = () => { if (videoRef.current && canvasRef.current) { const video = videoRef.current; const canvas = canvasRef.current; canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); setImage(dataUrl); stopLiveCamera(); } } }; const processImage = async () => { if (!image) return; setUploading(true); try { const blob = await (await fetch(image)).blob(); setExtractedImageBlob(blob); const formData = new FormData(); formData.append('file', blob, 'label.jpg'); const data = await inventoryApi.analyzeLabel(formData, mode); if (data.error) { toast.error(`AI Error: ${data.error}`); setUploading(false); return; } let parsedData = data; if (typeof data === 'string') { try { parsedData = JSON.parse(data); } catch (e) {} } const d = parsedData; // Find ANY array in the response if it's not a direct array let items: any[] = []; if (Array.isArray(d)) { items = d; } else { const potentialArrayKey = Object.keys(d).find(k => Array.isArray(d[k])); if (potentialArrayKey) { items = d[potentialArrayKey]; } else { // Check for singular object (must have at least name or Item or PN) const target = d.data || d; if (target.name || target.Item || target.PartNr || target.part_number) { items = [target]; } } } if (!items || items.length === 0) { toast.error("No relevant items detected. Try a closer photo."); } else { setExtractedItems(items); if (items.length === 1) { setEditingIndex(0); toast.success("Item identified!"); } else { toast.success(`Found ${items.length} items!`); } } } catch (error) { toast.error("Failed to process image with AI"); console.error(error); } finally { setUploading(false); } }; const confirmSingleItem = (index: number) => { const data = extractedItems[index]; const skipPhoto = data._skipPhoto === true; // User explicitly rejected the photo console.log('=== CONFIRM SINGLE ITEM (Hook) ==='); console.log('[Input] Item index:', index); console.log('[Input] Item data:', data); console.log('[Input] Skip photo flag:', skipPhoto); console.log('[Input] extractedImageBlob size:', extractedImageBlob?.size || 'null', 'bytes'); console.log('[Input] image_processing from data:', data.image_processing); const newItem = { name: String(data.Item || data.name || "New AI Item"), category: String(data.Category || data.category || "Uncategorized"), type: data.Type || data.type ? String(data.Type || data.type) : null, part_number: data.PartNr || data.part_number ? String(data.PartNr || data.part_number) : null, color: data.Color || data.color ? String(data.Color || data.color) : null, description: String(data.Description || data.description || ""), connector: data.Connector || data.connector ? String(data.Connector || data.connector) : null, size: data.Size || data.size ? String(data.Size || data.size) : null, ocr_text: data.OCR || data.ocr_text ? String(data.OCR || data.ocr_text) : null, specs: String(data.specs || ""), barcode: String(data.barcode || data.PartNr || data.part_number || `AI-${Date.now()}-${index}`), quantity: parseFloat(String(data.quantity || 1)), min_quantity: 1.0, box_label: data.box_label ? String(data.box_label) : null, labels_data: JSON.stringify(data), // Only pass image blob if user approved it ...(skipPhoto ? {} : { extractedImageBlob, imageProcessing: data.image_processing }) }; console.log('[Output] newItem being sent to onComplete:', newItem); console.log('[Output] extractedImageBlob attached:', !!newItem.extractedImageBlob); console.log('[Output] imageProcessing attached:', !!newItem.imageProcessing); console.log('=== END HOOK DEBUG ==='); onComplete(newItem); if (extractedItems.length > 1) { const remaining = [...extractedItems]; remaining.splice(index, 1); setExtractedItems(remaining); setEditingIndex(null); } else { setExtractedItems([]); setEditingIndex(null); } }; const confirmAllItems = async () => { const itemsToProcess = [...extractedItems]; setUploading(true); const toastId = toast.loading(`Adding ${itemsToProcess.length} items...`); try { for (let i = 0; i < itemsToProcess.length; i++) { const data = itemsToProcess[i]; const skipPhoto = data._skipPhoto === true; const newItem = { name: String(data.Item || data.name || "New AI Item"), category: String(data.Category || data.category || "Uncategorized"), type: data.Type || data.type ? String(data.Type || data.type) : null, part_number: data.PartNr || data.part_number ? String(data.PartNr || data.part_number) : null, color: data.Color || data.color ? String(data.Color || data.color) : null, description: String(data.Description || data.description || ""), connector: data.Connector || data.connector ? String(data.Connector || data.connector) : null, size: data.Size || data.size ? String(data.Size || data.size) : null, ocr_text: data.OCR || data.ocr_text ? String(data.OCR || data.ocr_text) : null, specs: String(data.specs || ""), barcode: String(data.barcode || data.PartNr || data.part_number || `AI-${Date.now()}-${i}`), quantity: parseFloat(String(data.quantity || 1)), min_quantity: 1.0, box_label: data.box_label ? String(data.box_label) : null, labels_data: JSON.stringify(data), // Only pass image blob if user approved it ...(skipPhoto ? {} : { extractedImageBlob, imageProcessing: data.image_processing }) }; await onComplete(newItem); } toast.success(`Successfully added ${itemsToProcess.length} items`, { id: toastId }); setExtractedItems([]); } catch (err) { toast.error("Error during batch add", { id: toastId }); } finally { setUploading(false); } }; const updateEditingItem = (fields: any) => { if (editingIndex === null) return; const newItems = [...extractedItems]; newItems[editingIndex] = { ...newItems[editingIndex], ...fields }; setExtractedItems(newItems); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = () => setImage(reader.result as string); reader.readAsDataURL(file); } }; useEffect(() => { return () => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); } }; }, []); return { image, setImage, uploading, extractedItems, setExtractedItems, editingIndex, setEditingIndex, mode, setMode, isLive, videoRef, canvasRef, fileInputRef, existingTypes, existingBoxes, extractedImageBlob, setExtractedImageBlob, startLiveCamera, stopLiveCamera, captureSnapshot, processImage, confirmSingleItem, confirmAllItems, updateEditingItem, handleFileChange }; }