DocDoeAI / src /components /first-hour /FirstHourShell.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
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>
);
}