akoulapure's picture
Initial Deployment: Best ViT Model
6cc8ae1 verified
Raw
History Blame Contribute Delete
17.4 kB
import { useState, useRef, useCallback } from 'react';
import { predictFromFile, predictFromURL, predictFromBase64 } from '../services/api';
import { usePredictionHistory } from '../hooks/usePredictionHistory';
function PredictPage() {
const [activeTab, setActiveTab] = useState('upload');
const [imagePreview, setImagePreview] = useState(null);
const [selectedFile, setSelectedFile] = useState(null);
const [imageUrl, setImageUrl] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [dragOver, setDragOver] = useState(false);
const [cameraActive, setCameraActive] = useState(false);
const fileInputRef = useRef(null);
const videoRef = useRef(null);
const canvasRef = useRef(null);
const streamRef = useRef(null);
const { history, addPrediction } = usePredictionHistory();
// File upload handlers
const handleFileSelect = (e) => {
const file = e.target.files?.[0];
if (file) processFile(file);
};
const processFile = (file) => {
if (!file.type.startsWith('image/')) {
setError('Please select an image file');
return;
}
setSelectedFile(file);
setError(null);
const reader = new FileReader();
reader.onload = (e) => setImagePreview(e.target.result);
reader.readAsDataURL(file);
};
const handleDrop = (e) => {
e.preventDefault();
setDragOver(false);
const file = e.dataTransfer.files?.[0];
if (file) processFile(file);
};
// Camera handlers
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 640 }, height: { ideal: 480 } }
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.play();
}
setCameraActive(true);
setError(null);
} catch (err) {
setError('Could not access camera. Please check permissions.');
}
};
const stopCamera = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(t => t.stop());
}
setCameraActive(false);
};
const capturePhoto = () => {
if (!videoRef.current || !canvasRef.current) return;
const canvas = canvasRef.current;
const video = videoRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
setImagePreview(dataUrl);
stopCamera();
};
// Clear
const clearImage = () => {
setImagePreview(null);
setSelectedFile(null);
setResult(null);
setError(null);
if (fileInputRef.current) fileInputRef.current.value = '';
};
// Predict
const handlePredict = useCallback(async () => {
setLoading(true);
setError(null);
setResult(null);
try {
let response;
if (activeTab === 'upload' && selectedFile) {
response = await predictFromFile(selectedFile);
} else if (activeTab === 'url' && imageUrl) {
response = await predictFromURL(imageUrl);
} else if (activeTab === 'camera' && imagePreview) {
response = await predictFromBase64(imagePreview);
} else {
setError('Please provide an image first');
setLoading(false);
return;
}
setResult(response);
addPrediction(response, imagePreview);
} catch (err) {
setError(err.message || 'Prediction failed');
} finally {
setLoading(false);
}
}, [activeTab, selectedFile, imageUrl, imagePreview, addPrediction]);
const getConfidenceClass = (conf) => {
if (conf >= 0.75) return 'high';
if (conf >= 0.5) return 'medium';
return 'low';
};
const canPredict = (activeTab === 'upload' && selectedFile) ||
(activeTab === 'url' && imageUrl.trim()) ||
(activeTab === 'camera' && imagePreview);
return (
<div className="page">
<h2 className="section-title">Predict Breed</h2>
<p className="section-subtitle">Upload an image, use your camera, or paste a URL to identify the breed.</p>
<div className="predict-layout">
{/* Left: Input panel */}
<div className="glass-card">
<div className="input-tabs">
<button className={`input-tab ${activeTab === 'upload' ? 'active' : ''}`}
onClick={() => { setActiveTab('upload'); stopCamera(); }}>
📁 Upload
</button>
<button className={`input-tab ${activeTab === 'camera' ? 'active' : ''}`}
onClick={() => setActiveTab('camera')}>
📷 Camera
</button>
<button className={`input-tab ${activeTab === 'url' ? 'active' : ''}`}
onClick={() => { setActiveTab('url'); stopCamera(); }}>
🔗 URL
</button>
</div>
{/* Upload tab */}
{activeTab === 'upload' && (
<>
<div className={`upload-zone ${dragOver ? 'drag-over' : ''}`}
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}>
<div className="icon">📤</div>
<p><strong>Click to upload</strong> or drag & drop</p>
<p className="hint">JPG, PNG, WebP — max 10MB</p>
</div>
<input ref={fileInputRef} type="file" accept="image/*" hidden onChange={handleFileSelect} />
</>
)}
{/* Camera tab */}
{activeTab === 'camera' && (
<>
{!cameraActive && !imagePreview && (
<div className="upload-zone" onClick={startCamera}>
<div className="icon">📷</div>
<p><strong>Click to open camera</strong></p>
<p className="hint">Allow camera access when prompted</p>
</div>
)}
{cameraActive && (
<div className="camera-container">
<video ref={videoRef} playsInline muted />
<div className="camera-controls">
<button className="camera-capture-btn" onClick={capturePhoto} title="Capture Photo" />
<button className="btn-secondary" onClick={stopCamera}>Cancel</button>
</div>
</div>
)}
<canvas ref={canvasRef} hidden />
</>
)}
{/* URL tab */}
{activeTab === 'url' && (
<>
<label style={{ fontSize: '0.9rem', color: 'var(--color-text-secondary)' }}>Image URL</label>
<div className="url-input-group">
<input
type="url"
placeholder="https://example.com/cow-image.jpg"
value={imageUrl}
onChange={(e) => { setImageUrl(e.target.value); setImagePreview(e.target.value); }}
/>
</div>
</>
)}
{/* Image preview */}
{imagePreview && (
<div className="image-preview">
<img src={imagePreview} alt="Preview" />
<button className="remove-btn" onClick={clearImage}></button>
</div>
)}
{/* Error */}
{error && (
<div className="prediction-warning" style={{ borderColor: 'rgba(239,68,68,0.3)', background: 'rgba(239,68,68,0.08)' }}>
⚠️ {error}
</div>
)}
{/* Predict button */}
<button className="btn-primary predict-btn" onClick={handlePredict}
disabled={!canPredict || loading}>
{loading ? '⏳ Analyzing...' : '🔍 Classify Breed'}
</button>
{/* Tips */}
<div className="tips-box">
<h4>📸 Tips for Better Results</h4>
<ul style={{ listStyle: 'none', padding: 0 }}>
<li style={{ fontSize: '0.8rem', color: 'var(--color-text-secondary)', marginBottom: '4px' }}>• Use a clear side-view photo of the animal</li>
<li style={{ fontSize: '0.8rem', color: 'var(--color-text-secondary)', marginBottom: '4px' }}>• Ensure good lighting and minimal background clutter</li>
<li style={{ fontSize: '0.8rem', color: 'var(--color-text-secondary)', marginBottom: '4px' }}>• Show the full body including head and horns</li>
<li style={{ fontSize: '0.8rem', color: 'var(--color-text-secondary)' }}>• Avoid photos with multiple animals</li>
</ul>
</div>
</div>
{/* Right: Results panel */}
<div>
{loading && (
<div className="glass-card loading-spinner">
<div className="spinner" />
<p style={{ color: 'var(--color-text-secondary)' }}>Analyzing image...</p>
</div>
)}
{result && !loading && (
<div className="glass-card result-card">
<div className="result-header">
<h3 className="result-breed-name">{result.predicted_breed}</h3>
<span className={`result-badge ${getConfidenceClass(result.confidence)}`}>
{(result.confidence * 100).toFixed(1)}%
</span>
</div>
{/* Confidence bar */}
<div className="confidence-bar-container">
<div className="confidence-bar-label">
<span>Confidence</span>
<span>{(result.confidence * 100).toFixed(1)}%</span>
</div>
<div className="confidence-bar">
<div className="confidence-bar-fill" style={{ width: `${result.confidence * 100}%` }} />
</div>
</div>
{/* Top K predictions */}
{result.top_k && result.top_k.length > 1 && (
<div className="topk-list">
<h4 style={{ fontSize: '0.9rem', marginBottom: '0.5rem', color: 'var(--color-text-secondary)' }}>
Top Predictions
</h4>
{result.top_k.map((item, idx) => (
<div className="topk-item" key={idx}>
<span className={`topk-rank ${idx === 0 ? 'first' : ''}`}>{idx + 1}</span>
<span className="topk-name">{item.breed}</span>
<span className="topk-conf">{(item.confidence * 100).toFixed(1)}%</span>
</div>
))}
</div>
)}
{/* Breed info */}
{result.breed_info && (
<>
<h4 style={{ fontSize: '0.9rem', marginBottom: '0.75rem', color: 'var(--color-text-secondary)' }}>
Breed Details
</h4>
<div className="breed-info-grid">
<div className="breed-info-item">
<div className="breed-info-label">Region</div>
<div className="breed-info-value">{result.breed_info.region}</div>
</div>
<div className="breed-info-item">
<div className="breed-info-label">Type</div>
<div className="breed-info-value">{result.breed_info.animal_type}</div>
</div>
<div className="breed-info-item">
<div className="breed-info-label">Milk Yield</div>
<div className="breed-info-value">{result.breed_info.avg_milk_liters_per_day} L/day</div>
</div>
<div className="breed-info-item">
<div className="breed-info-label">Lifespan</div>
<div className="breed-info-value">{result.breed_info.lifespan_years} years</div>
</div>
<div className="breed-info-item">
<div className="breed-info-label">Primary Use</div>
<div className="breed-info-value">{result.breed_info.primary_use}</div>
</div>
<div className="breed-info-item">
<div className="breed-info-label">Inference</div>
<div className="breed-info-value">{result.inference_time_ms} ms</div>
</div>
</div>
{result.breed_info.description && (
<p style={{ marginTop: '1rem', fontSize: '0.85rem', color: 'var(--color-text-secondary)', lineHeight: '1.6' }}>
{result.breed_info.description}
</p>
)}
</>
)}
{/* Warning */}
{result.warning && (
<div className="prediction-warning">
⚠️ {result.warning}
</div>
)}
</div>
)}
{/* History */}
{!result && !loading && history.length > 0 && (
<div className="glass-card">
<h4 style={{ marginBottom: '1rem', color: 'var(--color-text-secondary)' }}>Recent Predictions</h4>
{history.slice(0, 5).map((entry) => (
<div key={entry.id} className="topk-item">
<span className="topk-name">{entry.predictedBreed}</span>
<span className="topk-conf">
{(entry.confidence * 100).toFixed(1)}% · {new Date(entry.timestamp).toLocaleDateString()}
</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}
export default PredictPage;