refactor: extract useAIExtraction hook from AIOnboarding.tsx
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { Camera, Check, RefreshCw, X, Image as ImageIcon, Sparkles, Hash, Layout, Layers, Package, ChevronDown } from 'lucide-react';
|
import { Camera, Check, RefreshCw, X, Image as ImageIcon, Sparkles, Hash, Layout, Layers, Package, ChevronDown } from 'lucide-react';
|
||||||
import { inventoryApi } from '@/lib/api';
|
import { useAIExtraction } from '@/hooks/useAIExtraction';
|
||||||
|
|
||||||
interface AIOnboardingProps {
|
interface AIOnboardingProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -13,221 +13,37 @@ interface AIOnboardingProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AIOnboarding({ onCancel, onComplete, categories, inventory }: AIOnboardingProps) {
|
export default function AIOnboarding({ onCancel, onComplete, categories, inventory }: AIOnboardingProps) {
|
||||||
const [image, setImage] = useState<string | null>(null);
|
const {
|
||||||
const [uploading, setUploading] = useState(false);
|
image,
|
||||||
const [extractedItems, setExtractedItems] = useState<any[]>([]);
|
setImage,
|
||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
uploading,
|
||||||
const [mode, setMode] = useState<'item' | 'box'>('item');
|
extractedItems,
|
||||||
const [isLive, setIsLive] = useState(false);
|
setExtractedItems,
|
||||||
|
editingIndex,
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
setEditingIndex,
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
mode,
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
setMode,
|
||||||
|
isLive,
|
||||||
const startLiveCamera = async () => {
|
videoRef,
|
||||||
try {
|
canvasRef,
|
||||||
setIsLive(true);
|
fileInputRef,
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
existingTypes,
|
||||||
video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } },
|
existingBoxes,
|
||||||
audio: false
|
startLiveCamera,
|
||||||
});
|
stopLiveCamera,
|
||||||
if (videoRef.current) {
|
captureSnapshot,
|
||||||
videoRef.current.srcObject = stream;
|
processImage,
|
||||||
streamRef.current = stream;
|
confirmSingleItem,
|
||||||
}
|
confirmAllItems: hookConfirmAllItems,
|
||||||
} catch (err) {
|
updateEditingItem,
|
||||||
console.error("Camera access error:", err);
|
handleFileChange
|
||||||
toast.error("Could not access camera for live scan.");
|
} = useAIExtraction(inventory, onComplete);
|
||||||
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 () => {
|
const confirmAllItems = async () => {
|
||||||
// Clone items and process them sequentially
|
await hookConfirmAllItems();
|
||||||
const itemsToProcess = [...extractedItems];
|
onCancel(); // Close modal after bulk completion
|
||||||
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<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
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 (
|
return (
|
||||||
<div data-testid="ai-extraction-overlay" className="fixed inset-0 z-50 bg-background flex flex-col p-6 animate-in fade-in slide-in-from-bottom-5 duration-300">
|
<div data-testid="ai-extraction-overlay" className="fixed inset-0 z-50 bg-background flex flex-col p-6 animate-in fade-in slide-in-from-bottom-5 duration-300">
|
||||||
<div className="flex justify-between items-center mb-6 shrink-0">
|
<div className="flex justify-between items-center mb-6 shrink-0">
|
||||||
|
|||||||
248
frontend/hooks/useAIExtraction.ts
Normal file
248
frontend/hooks/useAIExtraction.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
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<string | null>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [extractedItems, setExtractedItems] = useState<any[]>([]);
|
||||||
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
|
const [mode, setMode] = useState<'item' | 'box'>('item');
|
||||||
|
const [isLive, setIsLive] = useState(false);
|
||||||
|
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(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();
|
||||||
|
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 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 () => {
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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,
|
||||||
|
startLiveCamera,
|
||||||
|
stopLiveCamera,
|
||||||
|
captureSnapshot,
|
||||||
|
processImage,
|
||||||
|
confirmSingleItem,
|
||||||
|
confirmAllItems,
|
||||||
|
updateEditingItem,
|
||||||
|
handleFileChange
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user