CLIO v1.0 — Scanner Inteligente com IA (MVP)

This commit is contained in:
Jarvis Deploy
2026-02-10 23:05:41 +00:00
commit 8e903d9222
41 changed files with 3190 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
'use client';
import { useState, useRef, useCallback, useEffect } from 'react';
import { api } from '@/lib/api';
export default function Scanner({ onComplete }: { onComplete: () => void }) {
const [mode, setMode] = useState<'idle' | 'camera' | 'processing' | 'result'>('idle');
const [result, setResult] = useState<any>(null);
const [error, setError] = useState('');
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const stopCamera = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(t => t.stop());
streamRef.current = null;
}
}, []);
useEffect(() => {
return () => stopCamera();
}, [stopCamera]);
const startCamera = async () => {
setError('');
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } }
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
setMode('camera');
} catch (err) {
setError('Não foi possível acessar a câmera. Verifique as permissões.');
}
};
const capturePhoto = () => {
if (!videoRef.current || !canvasRef.current) return;
const video = videoRef.current;
const canvas = canvasRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(video, 0, 0);
const base64 = canvas.toDataURL('image/jpeg', 0.85);
stopCamera();
processImage(base64);
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const base64 = ev.target?.result as string;
processImage(base64);
};
reader.readAsDataURL(file);
};
const processImage = async (base64: string) => {
setMode('processing');
setError('');
try {
const res = await api.scanDocument(base64);
setResult(res);
setMode('result');
} catch (err: any) {
setError(err.message);
setMode('idle');
}
};
const categoryColors: Record<string, string> = {
contrato: 'bg-blue-500/20 text-blue-400',
nf: 'bg-green-500/20 text-green-400',
receita: 'bg-pink-500/20 text-pink-400',
rg: 'bg-yellow-500/20 text-yellow-400',
cnh: 'bg-orange-500/20 text-orange-400',
certidao: 'bg-purple-500/20 text-purple-400',
boleto: 'bg-red-500/20 text-red-400',
outro: 'bg-gray-500/20 text-gray-400',
};
const categoryLabels: Record<string, string> = {
contrato: '📄 Contrato',
nf: '🧾 Nota Fiscal',
receita: '💊 Receita',
rg: '🪪 RG',
cnh: '🚗 CNH',
certidao: '📋 Certidão',
boleto: '💰 Boleto',
outro: '📎 Outro',
};
if (mode === 'camera') {
return (
<div className="space-y-4">
<div className="relative rounded-2xl overflow-hidden bg-black">
<video ref={videoRef} className="w-full" autoPlay playsInline muted />
{/* Overlay guia */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-8 border-2 border-primary/40 rounded-xl" />
<div className="absolute top-10 left-10 w-8 h-8 border-t-3 border-l-3 border-primary rounded-tl-lg" />
<div className="absolute top-10 right-10 w-8 h-8 border-t-3 border-r-3 border-primary rounded-tr-lg" />
<div className="absolute bottom-10 left-10 w-8 h-8 border-b-3 border-l-3 border-primary rounded-bl-lg" />
<div className="absolute bottom-10 right-10 w-8 h-8 border-b-3 border-r-3 border-primary rounded-br-lg" />
</div>
</div>
<div className="flex gap-3">
<button onClick={() => { stopCamera(); setMode('idle'); }} className="flex-1 py-3 rounded-xl bg-dark-700 text-gray-300">
Cancelar
</button>
<button onClick={capturePhoto} className="flex-1 btn-primary scan-pulse">
📸 Capturar
</button>
</div>
<canvas ref={canvasRef} className="hidden" />
</div>
);
}
if (mode === 'processing') {
return (
<div className="glass-card p-12 text-center">
<div className="text-5xl mb-4 animate-bounce">🔍</div>
<h3 className="text-lg font-semibold text-primary mb-2">Analisando documento...</h3>
<p className="text-gray-400 text-sm">IA extraindo texto, categorizando e buscando dados relevantes</p>
<div className="mt-6 flex justify-center gap-1">
{[0,1,2].map(i => (
<div key={i} className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{animationDelay: `${i*0.15}s`}} />
))}
</div>
</div>
);
}
if (mode === 'result' && result) {
return (
<div className="space-y-4">
<div className="glass-card p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-lg">{result.title}</h3>
<span className={`category-badge ${categoryColors[result.category] || categoryColors.outro}`}>
{categoryLabels[result.category] || '📎 Outro'}
</span>
</div>
{/* Summary */}
{result.summary && (
<div className="mb-4">
<h4 className="text-sm font-medium text-accent mb-2">📌 Resumo</h4>
<div className="text-sm text-gray-300 whitespace-pre-line bg-dark-800 rounded-xl p-4">
{result.summary}
</div>
</div>
)}
{/* Extracted Data */}
{result.extracted_data && Object.keys(result.extracted_data).length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-medium text-accent mb-2">📊 Dados Extraídos</h4>
<div className="bg-dark-800 rounded-xl p-4 space-y-2">
{Object.entries(result.extracted_data).map(([key, val]) => (
<div key={key} className="flex justify-between text-sm">
<span className="text-gray-400 capitalize">{key.replace(/_/g, ' ')}</span>
<span className="text-gray-200 text-right max-w-[60%]">{String(val)}</span>
</div>
))}
</div>
</div>
)}
{/* Risk Alerts */}
{result.risk_alerts && result.risk_alerts.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-medium text-red-400 mb-2"> Alertas de Risco</h4>
<div className="space-y-2">
{result.risk_alerts.map((alert: string, i: number) => (
<div key={i} className="bg-red-500/10 border border-red-500/20 rounded-xl p-3 text-sm text-red-300">
{alert}
</div>
))}
</div>
</div>
)}
{/* Tags */}
{result.tags && result.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{result.tags.map((tag: string, i: number) => (
<span key={i} className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full">
#{tag}
</span>
))}
</div>
)}
</div>
<div className="flex gap-3">
<button onClick={() => { setResult(null); setMode('idle'); }} className="flex-1 py-3 rounded-xl bg-dark-700 text-gray-300">
Novo Scan
</button>
<button onClick={onComplete} className="flex-1 btn-accent">
Ver Histórico
</button>
</div>
</div>
);
}
// Idle state
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-6xl mb-4">📜</div>
<h2 className="text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent mb-2">
Scanner Inteligente
</h2>
<p className="text-gray-400">Aponte para o documento e deixe a IA fazer o resto</p>
</div>
{error && (
<div className="bg-red-400/10 text-red-400 rounded-xl p-3 text-sm text-center">{error}</div>
)}
<div className="grid grid-cols-2 gap-4">
<button onClick={startCamera} className="glass-card p-8 text-center hover:border-primary/30 transition-all active:scale-95">
<div className="text-4xl mb-3">📷</div>
<div className="font-medium">Câmera</div>
<div className="text-xs text-gray-500 mt-1">Capturar ao vivo</div>
</button>
<button
onClick={() => fileInputRef.current?.click()}
className="glass-card p-8 text-center hover:border-primary/30 transition-all active:scale-95"
>
<div className="text-4xl mb-3">📁</div>
<div className="font-medium">Galeria</div>
<div className="text-xs text-gray-500 mt-1">Escolher imagem</div>
</button>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
/>
<div className="glass-card p-4">
<h3 className="text-sm font-medium text-gray-300 mb-3">Tipos suportados</h3>
<div className="grid grid-cols-4 gap-2 text-center text-xs">
{[
['📄', 'Contrato'], ['🧾', 'NF'], ['💊', 'Receita'], ['🪪', 'RG/CNH'],
['📋', 'Certidão'], ['💰', 'Boleto'], ['📎', 'Outros'], ['🔍', 'OCR'],
].map(([icon, label]) => (
<div key={label} className="py-2">
<div className="text-lg">{icon}</div>
<div className="text-gray-500 mt-1">{label}</div>
</div>
))}
</div>
</div>
</div>
);
}