proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
"use client";

import { useState, useCallback } from "react";
import {
  Box,
  Button,
  Typography,
  Alert,
  CircularProgress,
  Paper,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  Chip,
  Stack,
  Divider,
} from "@mui/material";
import { useDropzone } from "react-dropzone";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import CancelIcon from "@mui/icons-material/Cancel";
import InfoIcon from "@mui/icons-material/Info";
import { proofModeWorker, VerificationResult } from "@/lib/proofmode-worker";

export default function VerifyTab() {
  const [isVerifying, setIsVerifying] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [result, setResult] = useState<VerificationResult | null>(null);
  const [fileName, setFileName] = useState<string | null>(null);

  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    if (acceptedFiles.length === 0) return;

    const file = acceptedFiles[0];
    setFileName(file.name);
    setError(null);
    setResult(null);
    setIsVerifying(true);

    try {
      const verificationResult = await proofModeWorker.checkFile(file);
      setResult(verificationResult);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Verification failed");
    } finally {
      setIsVerifying(false);
    }
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      "image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"],
      "video/*": [".mp4", ".mov", ".avi"],
      "application/zip": [".zip"],
    },
    maxFiles: 1,
  });

  const renderVerificationCheck = (label: string, passed?: boolean) => {
    if (passed === undefined) return null;

    return (
      <ListItem>
        <ListItemIcon>
          {passed ? (
            <CheckCircleIcon color="success" />
          ) : (
            <CancelIcon color="error" />
          )}
        </ListItemIcon>
        <ListItemText
          primary={label}
          secondary={passed ? "Verified" : "Not verified"}
        />
      </ListItem>
    );
  };

  return (
    <Box>
      <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, videos, and ProofMode bundles (.zip)
        </Typography>
      </Paper>

      {isVerifying && (
        <Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
          <CircularProgress />
        </Box>
      )}

      {error && (
        <Alert severity="error" sx={{ mt: 2 }}>
          {error}
        </Alert>
      )}

      {result && (
        <Box sx={{ mt: 4 }}>
          <Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
            <Typography variant="h6">Verification Result</Typography>
            <Chip
              label={result.valid ? "Valid" : "Invalid"}
              color={result.valid ? "success" : "error"}
              size="small"
            />
          </Stack>

          {fileName && (
            <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
              File: {fileName}
            </Typography>
          )}

          <Paper variant="outlined" sx={{ p: 2 }}>
            <Typography variant="subtitle2" sx={{ mb: 1 }}>
              Verification Checks
            </Typography>
            <List dense>
              {renderVerificationCheck("C2PA Manifest", result.checks.c2pa)}
              {renderVerificationCheck("PGP Signature", result.checks.pgp)}
              {renderVerificationCheck("OpenTimestamps", result.checks.ots)}
              {renderVerificationCheck("EXIF Metadata", result.checks.exif)}
            </List>
          </Paper>

          {result.errors && result.errors.length > 0 && (
            <Alert severity="warning" sx={{ mt: 2 }}>
              <Typography variant="subtitle2" gutterBottom>
                Warnings:
              </Typography>
              <ul>
                {result.errors.map((error, index) => (
                  <li key={index}>{error}</li>
                ))}
              </ul>
            </Alert>
          )}

          {result.details && (
            <Paper variant="outlined" sx={{ mt: 2, p: 2 }}>
              <Typography variant="subtitle2" gutterBottom>
                <InfoIcon
                  sx={{ fontSize: 16, verticalAlign: "text-bottom", mr: 0.5 }}
                />
                Detailed Information
              </Typography>
              <Box
                component="pre"
                sx={{
                  mt: 1,
                  p: 1,
                  backgroundColor: "rgba(139, 195, 74, 0.08)",
                  borderRadius: 1,
                  overflow: "auto",
                  fontSize: "0.875rem",
                }}
              >
                {JSON.stringify(result.details, null, 2)}
              </Box>
            </Paper>
          )}
        </Box>
      )}
    </Box>
  );
}