'use client'; import React, { useState, useRef, useEffect } from 'react'; import { toast } from 'react-hot-toast'; import { Camera, Check, RefreshCw, X, Image as ImageIcon, Sparkles, Hash, Layout, Layers, Package, ChevronDown } from 'lucide-react'; import { inventoryApi } from '@/lib/api'; interface AIOnboardingProps { onCancel: () => void; onComplete: (itemData: any) => void; categories: any[]; inventory: any[]; } export default function AIOnboarding({ onCancel, onComplete, categories, inventory }: AIOnboardingProps) { 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 videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); 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(); 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; // HYPER-ROBUST: 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 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) }; onComplete(newItem); if (extractedItems.length > 1) { const remaining = [...extractedItems]; remaining.splice(index, 1); setExtractedItems(remaining); setEditingIndex(null); } else { setExtractedItems([]); setEditingIndex(null); } }; const confirmAllItems = async () => { // Clone items and process them sequentially 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 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) }; // Wait for parent to process each one await onComplete(newItem); } toast.success(`Successfully added ${itemsToProcess.length} items`, { id: toastId }); setExtractedItems([]); onCancel(); // Close the modal after bulk completion } 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); }; // Extract unique item types for suggestions const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[]; const existingBoxes = Array.from(new Set(inventory.map(i => i.box_label).filter(Boolean))).sort() as string[]; const fileInputRef = useRef(null); 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(() => { // Cleanup on unmount return () => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); } }; }, []); return (

AI Discovery

Powered by Gemini 2.0 Flash

{!image && !isLive ? (

{mode === 'box' ? 'Deep Box Analysis' : 'Multi-Item Extraction'}

{mode === 'box' ? 'Scan container labels to identify storage locations' : 'Identify multiple technical items from a single photo or label'}

) : isLive ? ( // LIVE VIEWFINDER MODE