changes: Request cancelation and handle reset
This commit is contained in:
78
app/page.tsx
78
app/page.tsx
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState, useCallback, Suspense } from 'react';
|
import { useEffect, useRef, useState, useCallback, Suspense } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Camera, History, VideoOff, Settings, Video } from 'lucide-react';
|
import { Camera, History, VideoOff, Settings, Video, X } from 'lucide-react';
|
||||||
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { type Shoe } from '@/lib/shoe-database';
|
import { type Shoe } from '@/lib/shoe-database';
|
||||||
@@ -31,6 +31,7 @@ function HomePageContent() {
|
|||||||
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
const [notFoundMessage, setNotFoundMessage] = useState(false);
|
const [notFoundMessage, setNotFoundMessage] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
// Effect to clean up the stream when component unmounts or stream changes
|
// Effect to clean up the stream when component unmounts or stream changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -164,6 +165,22 @@ function HomePageContent() {
|
|||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
if (!videoRef.current || isScanning) return;
|
if (!videoRef.current || isScanning) return;
|
||||||
|
|
||||||
|
// 1. Cancelar petición anterior si existe
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
console.log('🚫 Cancelando petición anterior...');
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Crear nuevo AbortController para esta petición
|
||||||
|
const controller = new AbortController();
|
||||||
|
abortControllerRef.current = controller;
|
||||||
|
|
||||||
|
// 3. Configurar timeout de 30 segundos
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
console.log('⏱️ Timeout alcanzado - cancelando petición...');
|
||||||
|
controller.abort();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
setNotFoundMessage(false); // Limpiar mensaje anterior
|
setNotFoundMessage(false); // Limpiar mensaje anterior
|
||||||
@@ -181,8 +198,12 @@ function HomePageContent() {
|
|||||||
|
|
||||||
console.log('🔍 Llamando al servidor Python para identificar SKU...');
|
console.log('🔍 Llamando al servidor Python para identificar SKU...');
|
||||||
|
|
||||||
// Call SKU identification service
|
// Call SKU identification service with abort signal
|
||||||
const sku = await skuIdentificationService.identifySKU(imageData);
|
const sku = await skuIdentificationService.identifySKU(imageData, controller.signal);
|
||||||
|
|
||||||
|
// Limpiar timeout si la petición completó antes
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
console.log('📦 SKU result:', sku);
|
console.log('📦 SKU result:', sku);
|
||||||
|
|
||||||
if (sku) {
|
if (sku) {
|
||||||
@@ -217,6 +238,15 @@ function HomePageContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Limpiar timeout en caso de error
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// No mostrar error si fue cancelación
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
console.log('🚫 Escaneo cancelado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.error('❌ Error en identificación:', error);
|
console.error('❌ Error en identificación:', error);
|
||||||
setDetectedSKU(null);
|
setDetectedSKU(null);
|
||||||
setNotFoundMessage(true);
|
setNotFoundMessage(true);
|
||||||
@@ -227,6 +257,16 @@ function HomePageContent() {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
} finally {
|
} finally {
|
||||||
setIsScanning(false);
|
setIsScanning(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelScan = () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
console.log('🚫 Usuario canceló el escaneo');
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
setIsScanning(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -507,22 +547,46 @@ function HomePageContent() {
|
|||||||
{/* Main Capture Button - Larger */}
|
{/* Main Capture Button - Larger */}
|
||||||
<button
|
<button
|
||||||
onClick={handleScan}
|
onClick={handleScan}
|
||||||
|
disabled={isScanning}
|
||||||
className='group relative'
|
className='group relative'
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-red-500/40 to-pink-500/40 rounded-full flex items-center justify-center border-2 border-white/40 hover:from-red-500/60 hover:to-pink-500/60 transition-all duration-300 transform hover:scale-110 shadow-2xl">
|
<div className={`w-16 h-16 bg-gradient-to-br rounded-full flex items-center justify-center border-2 border-white/40 transition-all duration-300 shadow-2xl ${
|
||||||
|
isScanning
|
||||||
|
? 'from-blue-500/40 to-purple-500/40 animate-pulse cursor-not-allowed'
|
||||||
|
: 'from-red-500/40 to-pink-500/40 hover:from-red-500/60 hover:to-pink-500/60 transform hover:scale-110'
|
||||||
|
}`}>
|
||||||
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
||||||
<Camera size={28} className="text-white drop-shadow-lg" />
|
<Camera size={28} className="text-white drop-shadow-lg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-black/80 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
<div className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-black/80 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||||
Detectar Zapato
|
{isScanning ? 'Analizando...' : 'Detectar Zapato'}
|
||||||
</div>
|
</div>
|
||||||
{/* Pulsing Ring */}
|
{/* Pulsing Ring - solo cuando NO está escaneando */}
|
||||||
<div className="absolute inset-0 rounded-full border-2 border-red-400/50 animate-ping"></div>
|
{!isScanning && (
|
||||||
|
<div className="absolute inset-0 rounded-full border-2 border-red-400/50 animate-ping"></div>
|
||||||
|
)}
|
||||||
{/* Glow Effect */}
|
{/* Glow Effect */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-red-500/30 to-pink-500/30 rounded-full blur opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform scale-150"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-red-500/30 to-pink-500/30 rounded-full blur opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform scale-150"></div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Cancel Button - solo visible cuando está escaneando */}
|
||||||
|
{isScanning && (
|
||||||
|
<button
|
||||||
|
onClick={handleCancelScan}
|
||||||
|
className='group relative animate-in fade-in slide-in-from-bottom-2 duration-300'
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-gradient-to-br from-orange-500/40 to-red-500/40 rounded-full flex items-center justify-center border border-white/40 hover:from-orange-500/60 hover:to-red-500/60 transition-all duration-300 transform hover:scale-110">
|
||||||
|
<X size={20} className="text-white drop-shadow-sm" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute -top-10 left-1/2 transform -translate-x-1/2 bg-black/80 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||||
|
Cancelar
|
||||||
|
</div>
|
||||||
|
{/* Glow Effect */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-orange-500/20 to-red-500/20 rounded-full blur opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform scale-150"></div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* History Button */}
|
{/* History Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setHistoryOpen(true)}
|
onClick={() => setHistoryOpen(true)}
|
||||||
|
|||||||
@@ -64,9 +64,10 @@ export class SKUIdentificationService {
|
|||||||
/**
|
/**
|
||||||
* Identify product SKU from shoe image
|
* Identify product SKU from shoe image
|
||||||
* @param imageData - Image data captured from video
|
* @param imageData - Image data captured from video
|
||||||
|
* @param signal - Optional AbortSignal for cancellation
|
||||||
* @returns Promise<string | null> - SKU if found, null otherwise
|
* @returns Promise<string | null> - SKU if found, null otherwise
|
||||||
*/
|
*/
|
||||||
async identifySKU(imageData: ImageData): Promise<string | null> {
|
async identifySKU(imageData: ImageData, signal?: AbortSignal): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
console.log('\n🔍 ========== IDENTIFICACIÓN DE SKU ==========');
|
console.log('\n🔍 ========== IDENTIFICACIÓN DE SKU ==========');
|
||||||
console.log('📊 Dimensiones imagen:', imageData.width, 'x', imageData.height);
|
console.log('📊 Dimensiones imagen:', imageData.width, 'x', imageData.height);
|
||||||
@@ -101,6 +102,7 @@ export class SKUIdentificationService {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
mode: 'cors', // Need CORS headers from server
|
mode: 'cors', // Need CORS headers from server
|
||||||
|
signal, // Pass AbortSignal for cancellation support
|
||||||
headers: {
|
headers: {
|
||||||
// Don't set Content-Type, let browser set it with boundary for multipart/form-data
|
// Don't set Content-Type, let browser set it with boundary for multipart/form-data
|
||||||
'ngrok-skip-browser-warning': 'true', // Skip ngrok browser warning
|
'ngrok-skip-browser-warning': 'true', // Skip ngrok browser warning
|
||||||
@@ -154,6 +156,13 @@ export class SKUIdentificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle AbortError separately (request was cancelled)
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
console.log('🚫 PETICIÓN CANCELADA por el usuario o timeout');
|
||||||
|
console.log('=========================================\n');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
console.error('❌ ERROR EN IDENTIFICACIÓN:', error);
|
console.error('❌ ERROR EN IDENTIFICACIÓN:', error);
|
||||||
console.error(' Detalles:', {
|
console.error(' Detalles:', {
|
||||||
message: error instanceof Error ? error.message : 'Unknown error',
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
|||||||
Reference in New Issue
Block a user