"use client"; import { motion } from "framer-motion"; import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { AiDisclaimer } from "@/components/AiDisclaimer"; import { CameraIcon, CheckCircleIcon, FolderIcon, PlayIcon, PlusIcon, SparklesIcon } from "@/components/icons"; import { Button } from "@/components/ui"; import { WizardActionBar } from "@/components/WizardActionBar"; import { DOCUMENT_ACCEPT_ATTR, describeExtraction, documentsToText, } from "@/lib/documents"; import type { HomeworkMode, HomeworkRecord } from "@/lib/schemas"; import { ACCEPTED_TYPES, formatBytes, imageFilesFromDataTransfer } from "@/lib/image"; import { useI18n } from "@/lib/i18n"; import { stepVariants } from "@/lib/motion"; import { modelErrorCopy } from "@/lib/model-errors"; import { takePendingShare } from "@/lib/share-pending"; import { useBackClose } from "@/lib/use-back-close"; import type { useNotesIntake } from "@/lib/use-notes-intake"; import { useIsPhone } from "@/lib/use-phone"; import { useOnline } from "@/lib/use-online"; import { clearWizardDraft, loadWizardDraft, saveWizardDraft } from "@/lib/wizard-draft"; import { openSettings } from "@/lib/ui-events"; import { ModelErrorActions } from "@/components/ModelErrorActions"; import { cn, truncate } from "@/lib/utils"; import { HomeworkHistory } from "./HomeworkHistory"; const MODES: Array<{ id: HomeworkMode; icon: React.ReactNode; titleKey: string; subtitleKey: string }> = [ { id: "make", icon: , titleKey: "homework.mode.make.title", subtitleKey: "homework.mode.make.subtitle" }, { id: "guide", icon: , titleKey: "homework.mode.guide.title", subtitleKey: "homework.mode.guide.subtitle" }, { id: "check", icon: , titleKey: "homework.mode.check", subtitleKey: "homework.mode.checkDesc" }, ]; export function HomeworkInput({ mode, onModeChange, text, onTextChange, solution, onSolutionChange, intake, solutionIntake, onSubmit, error, history, onOpen, onDelete, }: { mode: HomeworkMode; onModeChange: (mode: HomeworkMode) => void; text: string; onTextChange: (text: string) => void; /** Mode "check": the student's own attempt. */ solution: string; onSolutionChange: (solution: string) => void; intake: ReturnType; /** Mode "check": photos/documents of the attempt itself. */ solutionIntake: ReturnType; /** `modeOverride` lets the phone mode cards start the run they tapped. */ onSubmit: (modeOverride?: HomeworkMode) => void; error: { message: string; kind?: string } | null; history: HomeworkRecord[]; onOpen: (record: HomeworkRecord) => void; onDelete: (id: string) => void; }) { const { t, plural } = useI18n(); const router = useRouter(); const fileInputRef = useRef(null); const cameraInputRef = useRef(null); const solutionFileRef = useRef(null); const solutionCameraRef = useRef(null); const [step, setStep] = useState(1); const [stepDir, setStepDir] = useState<1 | -1>(1); const [pasteOpen, setPasteOpen] = useState(false); /** Phones: the check card reveals the solution field instead of starting. */ const [checkOpen, setCheckOpen] = useState(false); const [dragging, setDragging] = useState(false); const isPhone = useIsPhone(); const reduceMotion = useReducedMotion(); const online = useOnline(); const [clipboardAvailable, setClipboardAvailable] = useState(false); /* System Back goes one wizard step back (phones), then leaves the page. */ useBackClose(isPhone && step > 1, () => { setStepDir(-1); setStep(1); }); useEffect(() => { setClipboardAvailable( typeof navigator !== "undefined" && typeof navigator.clipboard?.readText === "function", ); }, []); /* Restore a draft after an OS kill / reload. */ useEffect(() => { const draft = loadWizardDraft("homework"); if (draft?.text) onTextChange(draft.text); // eslint-disable-next-line react-hooks/exhaustive-deps -- restore once on mount }, []); /* Content shared in from another app (share target) lands here. */ useEffect(() => { const pending = takePendingShare(); if (!pending) return; if (pending.text) onTextChange(pending.text); if (pending.files.length > 0) void intake.addFiles(pending.files); // eslint-disable-next-line react-hooks/exhaustive-deps -- once on mount }, [intake.addFiles]); /* Persist the typed assignment while the user works (debounced). */ useEffect(() => { const timer = window.setTimeout(() => saveWizardDraft("homework", { text }), 400); return () => window.clearTimeout(timer); }, [text]); // Clipboard: screenshots become photos, long copied text becomes the assignment. useEffect(() => { const onPaste = (event: ClipboardEvent) => { const target = event.target as HTMLElement | null; const isTextField = target && /^(INPUT|TEXTAREA)$/.test(target.tagName); const files = imageFilesFromDataTransfer(event.clipboardData); if (files.length > 0) { event.preventDefault(); void intake.addFiles(files); return; } if (!isTextField) { const pasted = event.clipboardData?.getData("text/plain")?.trim(); if (pasted && pasted.length > 40) { event.preventDefault(); onTextChange(text ? `${text}\n${pasted}` : pasted); } } }; window.addEventListener("paste", onPaste); return () => window.removeEventListener("paste", onPaste); }, [intake, onTextChange, text]); const readyDocuments = intake.documents.filter((entry) => entry.status === "ready" && entry.extracted); const documentsBusy = intake.documents.some((entry) => entry.status === "extracting"); const documentImages = readyDocuments.flatMap((entry) => entry.extracted!.images); const documentText = documentsToText(readyDocuments.map((entry) => entry.extracted!)); const totalBytes = intake.images.reduce((sum, image) => sum + image.bytes, 0); /* Mode "check": attachments of the attempt itself. */ const solutionReadyDocuments = solutionIntake.documents.filter( (entry) => entry.status === "ready" && entry.extracted, ); const solutionBusy = solutionIntake.busy || solutionIntake.documents.some((entry) => entry.status === "extracting"); const solutionDocumentText = documentsToText(solutionReadyDocuments.map((entry) => entry.extracted!)); const solutionHasContent = solution.trim().length > 0 || solutionIntake.images.length > 0 || solutionDocumentText.trim().length > 0; const hasContent = intake.images.length > 0 || documentImages.length > 0 || text.trim().length > 0 || documentText.trim().length > 0; const canSubmit = hasContent && !intake.busy && !documentsBusy; const modelCopy = modelErrorCopy(error?.kind); const keyError = error?.kind === "needs_key" || error?.kind === "auth"; const errorCard = error ? (

{modelCopy?.title ?? (keyError ? t("upload.errAuth") : t("homework.error.title"))}

{modelCopy?.body ?? (error.message || t("homework.error.body"))}

{keyError ? ( ) : null}
) : null; /* Shared solution field (mode "check"): type it, paste it, or attach photos and documents of the work itself. Documents are read in the browser; the photos are transcribed by the server before the check runs. */ const solutionField = (