Download src/components/first-hour/FirstHourShell.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/first-hour/FirstHourShell.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/first-hour/FirstHourShell.tsx
-
curl -L -o FirstHourShell.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/first-hour/FirstHourShell.tsx
11.6 kB
| "use client"; | |
| import { useCallback, useEffect, useMemo, useState } from "react"; | |
| import Link from "next/link"; | |
| import { | |
| BookOpenCheck, | |
| Clapperboard, | |
| FileText, | |
| Layers3, | |
| LoaderCircle, | |
| RotateCw, | |
| UploadCloud, | |
| } from "lucide-react"; | |
| import { listDocuments } from "@/lib/api/documents"; | |
| import type { ApiDocument } from "@/lib/api/types"; | |
| import { buildDemoSeedDocuments } from "@/lib/demo-seed-documents"; | |
| import { getPublicDemoMode } from "@/lib/env"; | |
| import { buildStudyIntentUrl, type StudyIntent } from "@/lib/study-intents"; | |
| import { cn } from "@/lib/utils"; | |
| function sourceStatusLabel(status: string | null | undefined) { | |
| if (status === "ready" || status === "complete") return "Ready"; | |
| if (status === "processing" || status === "uploaded" || status === "running") return "Reading"; | |
| if (status === "failed") return "Needs clearer file"; | |
| return "Saved"; | |
| } | |
| function studyIntentHref(doc: ApiDocument, intent: StudyIntent, prompt: string) { | |
| return buildStudyIntentUrl({ | |
| intent, | |
| sourceId: doc.id, | |
| prompt, | |
| run: true, | |
| }); | |
| } | |
| export function FirstHourShell() { | |
| const [documents, setDocuments] = useState<ApiDocument[]>([]); | |
| const [selectedId, setSelectedId] = useState<string | null>(null); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const loadSources = useCallback(async () => { | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| let docs = await listDocuments(); | |
| if (getPublicDemoMode() !== "live" && docs.length === 0) { | |
| docs = buildDemoSeedDocuments(); | |
| } | |
| setDocuments(docs); | |
| setSelectedId((current) => current ?? docs.find((doc) => doc.status === "ready")?.id ?? docs[0]?.id ?? null); | |
| } catch { | |
| if (getPublicDemoMode() !== "live") { | |
| const docs = buildDemoSeedDocuments(); | |
| setDocuments(docs); | |
| setSelectedId(docs[0]?.id ?? null); | |
| } else { | |
| setError("DocDoe could not load your sources. Upload still works, and you can retry this panel after the connection recovers."); | |
| } | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }, []); | |
| useEffect(() => { | |
| const timeoutId = window.setTimeout(() => { | |
| void loadSources(); | |
| }, 0); | |
| return () => window.clearTimeout(timeoutId); | |
| }, [loadSources]); | |
| const readyDocuments = useMemo( | |
| () => documents.filter((doc) => doc.status === "ready"), | |
| [documents], | |
| ); | |
| const selectedDocument = documents.find((doc) => doc.id === selectedId) ?? readyDocuments[0] ?? documents[0] ?? null; | |
| const selectedIsReady = selectedDocument?.status === "ready"; | |
| const kitActions = selectedDocument | |
| ? [ | |
| { | |
| label: "Teach source", | |
| icon: BookOpenCheck, | |
| href: studyIntentHref(selectedDocument, "explain_source", `Explain ${selectedDocument.title} in simple exam language`), | |
| disabled: !selectedIsReady, | |
| }, | |
| { | |
| label: "Scoring notes", | |
| icon: FileText, | |
| href: studyIntentHref(selectedDocument, "generate_notes", `Make exam-ready notes from ${selectedDocument.title}`), | |
| disabled: !selectedIsReady, | |
| }, | |
| { | |
| label: "Quiz", | |
| icon: Layers3, | |
| href: studyIntentHref(selectedDocument, "generate_quiz", `Make a quiz from ${selectedDocument.title}`), | |
| disabled: !selectedIsReady, | |
| }, | |
| { | |
| label: "Flashcards", | |
| icon: Layers3, | |
| href: studyIntentHref(selectedDocument, "generate_flashcards", `Make flashcards from ${selectedDocument.title}`), | |
| disabled: !selectedIsReady, | |
| }, | |
| { | |
| label: "Study video plan", | |
| icon: Clapperboard, | |
| href: `/video-generator?source_id=${encodeURIComponent(selectedDocument.id)}`, | |
| disabled: !selectedIsReady, | |
| }, | |
| ] | |
| : []; | |
| return ( | |
| <main className="min-h-screen bg-[#030303] px-4 py-8 text-white sm:px-6 sm:py-12 lg:px-8"> | |
| <div className="mx-auto w-full max-w-6xl"> | |
| <div className="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between"> | |
| <div className="max-w-3xl"> | |
| <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/42">Study Generator</p> | |
| <h1 className="mt-3 text-4xl font-semibold leading-tight tracking-normal text-white sm:text-5xl"> | |
| Upload your material. Generate a complete study kit. | |
| </h1> | |
| <p className="mt-4 max-w-2xl text-base leading-7 text-white/58"> | |
| Video, quiz, flashcards, notes, and questions from one upload. Pick a ready source below or add fresh material first. | |
| </p> | |
| </div> | |
| <div className="flex flex-col gap-2 sm:flex-row"> | |
| <Link | |
| href="/upload" | |
| className="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition hover:bg-white/90" | |
| > | |
| <UploadCloud className="size-4" /> | |
| Upload material | |
| </Link> | |
| <Link | |
| href="/sources" | |
| className="inline-flex min-h-11 items-center justify-center rounded-lg border border-white/[0.12] px-4 text-sm font-semibold text-white/70 transition hover:border-white/24 hover:text-white" | |
| > | |
| Open sources | |
| </Link> | |
| </div> | |
| </div> | |
| {error ? ( | |
| <div className="mt-8 flex flex-col gap-3 rounded-lg border border-white/[0.12] bg-white/[0.04] p-4 text-sm text-white/70 sm:flex-row sm:items-center sm:justify-between"> | |
| <p>{error}</p> | |
| <button | |
| type="button" | |
| onClick={() => void loadSources()} | |
| className="inline-flex min-h-10 items-center justify-center gap-2 rounded-lg border border-white/[0.12] px-4 text-sm font-semibold text-white transition hover:border-white/25" | |
| > | |
| <RotateCw className="size-4" /> | |
| Retry | |
| </button> | |
| </div> | |
| ) : null} | |
| <div className="mt-10 grid gap-6 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]"> | |
| <section className="rounded-lg border border-white/[0.1] bg-white/[0.03] p-5"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <div> | |
| <h2 className="text-lg font-semibold text-white">Choose source</h2> | |
| <p className="mt-1 text-sm text-white/48">The selected source is passed into every study-kit action.</p> | |
| </div> | |
| {isLoading ? <LoaderCircle className="size-5 animate-spin text-white/45" /> : null} | |
| </div> | |
| {isLoading ? ( | |
| <div className="mt-5 space-y-3"> | |
| {Array.from({ length: 3 }).map((_, index) => ( | |
| <div key={index} className="h-20 animate-pulse rounded-lg border border-white/[0.08] bg-white/[0.03]" /> | |
| ))} | |
| </div> | |
| ) : documents.length > 0 ? ( | |
| <div className="mt-5 space-y-3"> | |
| {documents.slice(0, 8).map((doc) => { | |
| const selected = doc.id === selectedDocument?.id; | |
| return ( | |
| <button | |
| key={doc.id} | |
| type="button" | |
| onClick={() => setSelectedId(doc.id)} | |
| className={cn( | |
| "w-full rounded-lg border p-4 text-left transition", | |
| selected | |
| ? "border-white/28 bg-white/[0.075] text-white" | |
| : "border-white/[0.08] bg-black/20 text-white/68 hover:border-white/18 hover:text-white", | |
| )} | |
| > | |
| <div className="flex items-start justify-between gap-3"> | |
| <div className="min-w-0"> | |
| <p className="line-clamp-2 text-sm font-semibold">{doc.title || "Untitled source"}</p> | |
| <p className="mt-1 text-xs text-white/42"> | |
| {[doc.subject, doc.chapter, doc.material_type].filter(Boolean).join(" · ") || "Study material"} | |
| </p> | |
| </div> | |
| <span className="shrink-0 rounded-full border border-white/[0.1] px-2 py-0.5 text-[10px] font-semibold text-white/48"> | |
| {sourceStatusLabel(doc.status)} | |
| </span> | |
| </div> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| ) : ( | |
| <div className="mt-5 rounded-lg border border-dashed border-white/[0.14] p-6 text-center"> | |
| <p className="text-base font-semibold text-white">No source uploaded yet</p> | |
| <p className="mx-auto mt-2 max-w-sm text-sm leading-6 text-white/50"> | |
| Upload a selectable-text PDF or TXT export of your notes, chapter, syllabus, or question paper to unlock the study kit. | |
| </p> | |
| <Link href="/upload" className="mt-5 inline-flex rounded-lg bg-white px-4 py-2 text-sm font-semibold text-black"> | |
| Upload first material | |
| </Link> | |
| </div> | |
| )} | |
| </section> | |
| <section className="rounded-lg border border-white/[0.1] bg-white/[0.03] p-5"> | |
| <h2 className="text-lg font-semibold text-white">Generate study kit</h2> | |
| <p className="mt-1 text-sm text-white/48"> | |
| {selectedDocument | |
| ? `Using: ${selectedDocument.title || "selected source"}` | |
| : "Upload or choose a source first."} | |
| </p> | |
| {selectedDocument && !selectedIsReady ? ( | |
| <div className="mt-5 rounded-lg border border-white/[0.1] bg-white/[0.035] p-4 text-sm leading-6 text-white/62"> | |
| This source is still being read or needs a clearer upload. The original source stays visible; retry these actions once it is ready. | |
| </div> | |
| ) : null} | |
| <div className="mt-5 grid gap-3 sm:grid-cols-2"> | |
| {kitActions.map((action) => { | |
| const Icon = action.icon; | |
| const className = cn( | |
| "inline-flex min-h-14 items-center justify-center gap-2 rounded-lg border px-4 text-sm font-semibold transition", | |
| action.disabled | |
| ? "pointer-events-none border-white/[0.07] bg-white/[0.02] text-white/30" | |
| : "border-white/[0.12] bg-white/[0.045] text-white/78 hover:border-white/25 hover:bg-white/[0.075] hover:text-white", | |
| ); | |
| return action.disabled ? ( | |
| <span key={action.label} className={className} aria-disabled="true"> | |
| <Icon className="size-4" /> | |
| {action.label} | |
| </span> | |
| ) : ( | |
| <Link key={action.label} href={action.href} className={className}> | |
| <Icon className="size-4" /> | |
| {action.label} | |
| </Link> | |
| ); | |
| })} | |
| </div> | |
| {!selectedDocument ? ( | |
| <div className="mt-5 rounded-lg border border-white/[0.1] bg-white/[0.035] p-4 text-sm leading-6 text-white/58"> | |
| Study Generator is ready as soon as you add material. No fake generated kit is shown before a real source exists. | |
| </div> | |
| ) : null} | |
| </section> | |
| </div> | |
| </div> | |
| </main> | |
| ); | |
| } | |