'use client';
import { useState, useCallback, useRef } from 'react';
import {
Box,
Button,
Typography,
Alert,
CircularProgress,
Paper,
TextField,
FormControlLabel,
Checkbox,
Stack,
Divider,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Card,
CardMedia,
Chip,
} from '@mui/material';
import { useDropzone } from 'react-dropzone';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CameraAltIcon from '@mui/icons-material/CameraAlt';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DownloadIcon from '@mui/icons-material/Download';
import CloseIcon from '@mui/icons-material/Close';
import { proofModeWorker, ProofResult, LocationInfo, GenerateOptions } from '@/lib/proofmode-worker';
export default function GenerateTab() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [passphrase, setPassphrase] = useState('');
const [includeLocation, setIncludeLocation] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ProofResult | null>(null);
const [locationInfo, setLocationInfo] = useState<LocationInfo | null>(null);
const [cameraOpen, setCameraOpen] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length === 0) return;
const file = acceptedFiles[0];
setSelectedFile(file);
setError(null);
setResult(null);
// Create preview for images
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(file);
} else {
setPreview(null);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
'video/*': ['.mp4', '.mov', '.avi'],
},
maxFiles: 1,
});
const handleGetLocation = async () => {
try {
const location = await proofModeWorker.getCurrentLocation();
setLocationInfo(location);
setIncludeLocation(true);
} catch (err) {
setError('Failed to get location. Please enable location services.');
}
};
const handleGenerateProof = async () => {
if (!selectedFile) return;
console.log('Starting proof generation for:', selectedFile.name);
setIsGenerating(true);
setError(null);
setResult(null);
try {
const options: GenerateOptions = {
passphrase,
locationInfo: includeLocation && locationInfo ? locationInfo : undefined,
deviceInfo: {
manufacturer: 'Web Browser',
model: navigator.userAgent,
osVersion: navigator.platform,
// Additional browser info
screenResolution: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
};
console.log('Calling proofModeWorker.generateProof...');
const proofResult = await proofModeWorker.generateProof(selectedFile, options);
console.log('Proof result received:', proofResult);
setResult(proofResult);
} catch (err) {
console.error('Proof generation error:', err);
setError(err instanceof Error ? err.message : 'Failed to generate proof');
} finally {
setIsGenerating(false);
}
};
const handleOpenCamera = () => {
setCameraOpen(true);
};
const handleCloseCamera = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
setCameraOpen(false);
};
const handleStartCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
} catch (err) {
setError('Failed to access camera');
handleCloseCamera();
}
};
const handleCapturePhoto = () => {
if (!videoRef.current) return;
const canvas = document.createElement('canvas');
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.drawImage(videoRef.current, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const file = new File([blob], `capture_${Date.now()}.jpg`, { type: 'image/jpeg' });
setSelectedFile(file);
setPreview(canvas.toDataURL('image/jpeg'));
handleCloseCamera();
}
}, 'image/jpeg', 0.95);
};
const handleDownloadProof = () => {
if (!result) return;
// Create a blob with the proof data
const proofData = JSON.stringify(result, null, 2);
const blob = new Blob([proofData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Create download link
const a = document.createElement('a');
a.href = url;
a.download = `proof_${result.file_hash_sha256.substring(0, 8)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return (
<Box>
{!selectedFile ? (
<>
<Paper
{...getRootProps()}
sx={{
p: 4,
textAlign: 'center',
backgroundColor: isDragActive ? 'action.hover' : 'background.paper',
border: '2px dashed',
borderColor: isDragActive ? 'primary.main' : 'divider',
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
borderColor: 'primary.main',
backgroundColor: 'action.hover',
},
}}
>
<input {...getInputProps()} />
<CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" gutterBottom>
{isDragActive ? 'Drop the file here' : 'Drop a file here or click to select'}
</Typography>
<Typography variant="body2" color="text.secondary">
Supports images and videos
</Typography>
</Paper>
<Box sx={{ textAlign: 'center', mt: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Or
</Typography>
<Button
variant="outlined"
startIcon={<CameraAltIcon />}
onClick={handleOpenCamera}
>
Take Photo
</Button>
</Box>
</>
) : (
<Box>
{preview && (
<Card sx={{ mb: 3, maxWidth: 400, mx: 'auto' }}>
<CardMedia
component="img"
image={preview}
alt="Selected file"
sx={{ maxHeight: 300, objectFit: 'contain' }}
/>
</Card>
)}
<Paper variant="outlined" sx={{ p: 2, mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Selected File
</Typography>
<Typography variant="body2">{selectedFile.name}</Typography>
<Button
size="small"
onClick={() => {
setSelectedFile(null);
setPreview(null);
setResult(null);
}}
sx={{ mt: 1 }}
>
Change File
</Button>
</Paper>
<Stack spacing={3}>
<TextField
label="Passphrase (optional)"
type="password"
value={passphrase}
onChange={(e) => setPassphrase(e.target.value)}
fullWidth
helperText="Protect your proof with a passphrase"
/>
<Paper variant="outlined" sx={{ p: 2 }}>
<Stack direction="row" alignItems="center" spacing={2} sx={{ mb: 1 }}>
<LocationOnIcon color="action" />
<Typography variant="subtitle2">Location Information</Typography>
</Stack>
<FormControlLabel
control={
<Checkbox
checked={includeLocation}
onChange={(e) => setIncludeLocation(e.target.checked)}
/>
}
label="Include location in proof"
/>
{includeLocation && !locationInfo && (
<Button
size="small"
onClick={handleGetLocation}
sx={{ mt: 1 }}
>
Get Current Location
</Button>
)}
{locationInfo && (
<Box sx={{ mt: 1 }}>
<Typography variant="body2">
Lat: {locationInfo.latitude.toFixed(6)},
Lng: {locationInfo.longitude.toFixed(6)}
</Typography>
{locationInfo.accuracy && (
<Typography variant="caption" color="text.secondary">
Accuracy: ±{locationInfo.accuracy.toFixed(0)}m
</Typography>
)}
</Box>
)}
</Paper>
<Paper variant="outlined" sx={{ p: 2 }}>
<Stack direction="row" alignItems="center" spacing={2} sx={{ mb: 1 }}>
<DeviceHubIcon color="action" />
<Typography variant="subtitle2">Device Information</Typography>
</Stack>
<Typography variant="body2" color="text.secondary">
Browser and system information will be automatically included
</Typography>
</Paper>
<Button
variant="contained"
size="large"
onClick={handleGenerateProof}
disabled={isGenerating || !selectedFile}
fullWidth
>
{isGenerating ? <CircularProgress size={24} /> : 'Generate Proof'}
</Button>
</Stack>
</Box>
)}
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{result && (
<Alert
severity="success"
sx={{ mt: 3 }}
action={
<Button
color="inherit"
size="small"
startIcon={<DownloadIcon />}
onClick={handleDownloadProof}
>
Download
</Button>
}
>
<Typography variant="subtitle2" gutterBottom>
Proof Generated Successfully!
</Typography>
<Typography variant="body2">
Hash: <code>{result.file_hash_sha256}</code>
</Typography>
<Typography variant="body2">
Timestamp: {new Date(result.timestamps.proof_generated_at).toLocaleString()}
</Typography>
</Alert>
)}
{/* Camera Dialog */}
<Dialog
open={cameraOpen}
onClose={handleCloseCamera}
maxWidth="md"
fullWidth
>
<DialogTitle>
Take Photo
<IconButton
aria-label="close"
onClick={handleCloseCamera}
sx={{
position: 'absolute',
right: 8,
top: 8,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ position: 'relative', width: '100%', height: 400 }}>
<video
ref={videoRef}
autoPlay
playsInline
onLoadedMetadata={handleStartCamera}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
borderRadius: 4,
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseCamera}>Cancel</Button>
<Button
variant="contained"
onClick={handleCapturePhoto}
startIcon={<CameraAltIcon />}
>
Capture
</Button>
</DialogActions>
</Dialog>
</Box>
);
}