"use client"; import { useRef, useState } from "react"; import { motion } from "framer-motion"; import type { Level } from "@/lib/languages"; import type { FuriSegment } from "@/lib/types/dialogue"; import { LevelPicker } from "@/components/LevelPicker"; import { Furi } from "@/components/Furi"; import { TTSButton } from "@/components/TTSButton"; import { PhotoMarkers } from "./PhotoMarkers"; import { ImagePlus, Camera, RotateCcw, Wand2, AlertCircle } from "lucide-react"; type AnalysisObject = { label: string; translation: string; translationSegments?: FuriSegment[]; romanized?: string; box: [number, number, number, number]; score: number; }; type Analysis = { id: string; caption: string; objects: AnalysisObject[]; sentences: Array<{ target: string; targetSegments?: FuriSegment[]; gloss: string; romanized?: string }>; imageUrl?: string; }; const EASE = [0.16, 1, 0.3, 1] as const; export function CameraClient({ defaultLevel, lang }: { defaultLevel: Level; lang: string }) { const [previewUrl, setPreviewUrl] = useState(null); const [imageSize, setImageSize] = useState<{ w: number; h: number } | null>(null); const [analysis, setAnalysis] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [level, setLevel] = useState(defaultLevel); const inputRef = useRef(null); function pick(file: File) { setError(null); setAnalysis(null); if (previewUrl) URL.revokeObjectURL(previewUrl); const url = URL.createObjectURL(file); setPreviewUrl(url); const img = new Image(); img.onload = () => setImageSize({ w: img.naturalWidth, h: img.naturalHeight }); img.src = url; void upload(file); } async function upload(file: File) { setIsLoading(true); try { const form = new FormData(); form.append("image", file); form.append("level", level); const res = await fetch("/api/vision/analyze", { method: "POST", body: form }); if (!res.ok) { const j = await res.json().catch(() => ({})); setError(j.message || j.error || "Analysis failed."); return; } setAnalysis((await res.json()) as Analysis); } catch { setError("Network error. Please try again."); } finally { setIsLoading(false); } } function onInputChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (file) pick(file); } function clear() { if (previewUrl) URL.revokeObjectURL(previewUrl); setPreviewUrl(null); setAnalysis(null); setImageSize(null); setError(null); if (inputRef.current) inputRef.current.value = ""; } return (
Sentences match level
{!previewUrl && ( inputRef.current?.click()} className="group relative flex min-h-[360px] w-full flex-col items-center justify-center rounded-2xl border-3 border-black bg-white shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[10px_10px_0px_rgba(0,0,0,1)] active:translate-x-[2px] active:translate-y-[2px] active:shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all p-8 text-center overflow-hidden cursor-pointer" >

Drop anything to upload

Camera or gallery images automatically become interactive vocabulary cards.

)} {previewUrl && imageSize && (
Selected practice {analysis ? : null} {isLoading && (
Analyzing image...
)}
{analysis?.caption ? (
"{analysis.caption}"
) : null}
)} {error ? (

Analysis failed

{error}

) : null} {analysis ? ( {analysis.objects.length > 0 ? (
    {analysis.objects.map((o, i) => (
  • {o.romanized ?
    {o.romanized}
    : null}
    {o.label}
    {Math.round(o.score * 100)}%
  • ))}
) : (

No objects detected. Try a clearer photo.

)}
{analysis.sentences.length > 0 ? (
    {analysis.sentences.map((s, i) => (
  • {s.romanized ?
    {s.romanized}
    : null}
    {s.gloss}
  • ))}
) : (

No sentences generated. Retry the photo.

)}
) : null}
); } function ResultCard({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); }