import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useNavigate, useOutletContext } from "react-router-dom"; import { supabase } from "@/integrations/supabase/client"; import { useWorkspace, DocumentRow, FlashcardRow, NoteRow, PodcastRow, QuizQuestionRow, QuizRow } from "@/store/workspace"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { useToast } from "@/hooks/use-toast"; import MarkdownView from "@/components/MarkdownView"; import { FileText, Layers, ListChecks, Headphones, MessagesSquare, Loader2, Trash2, ChevronLeft, Sparkles, RefreshCw, Menu } from "lucide-react"; import { useAuth } from "@/hooks/useAuth"; import { streamNotes, generateDerivatives } from "@/lib/pipeline"; import { generatePodcast } from "@/lib/podcast"; import FlashcardsDeck from "@/components/FlashcardsDeck"; import QuizPlayer from "@/components/QuizPlayer"; import ChatPanel from "@/components/ChatPanel"; export default function DocumentWorkspace() { const { docId } = useParams(); const navigate = useNavigate(); const { user } = useAuth(); const { toast } = useToast(); const outlet = useOutletContext<{ openMobileNav?: () => void }>(); const { documents, upsertDocument, removeDocument, notes, flashcards, quiz, podcast, setNote, setFlashcards, setQuiz, setPodcast, } = useWorkspace(); const doc = useMemo( () => documents.find((d) => d.id === docId), [documents, docId] ); const [loading, setLoading] = useState(true); const [streaming, setStreaming] = useState(false); const autoStartedRef = useRef(null); useEffect(() => { if (!docId || !user) return; let mounted = true; setLoading(true); (async () => { const { data: docRow } = await supabase .from("documents") .select("id,title,source_type,status,error_code,created_at") .eq("id", docId) .maybeSingle(); if (docRow && mounted) upsertDocument(docRow as DocumentRow); const [n, f, q, p] = await Promise.all([ supabase.from("notes").select("id,document_id,markdown").eq("document_id", docId).maybeSingle(), supabase.from("flashcards").select("id,document_id,front,back,order_index").eq("document_id", docId).order("order_index"), supabase.from("quizzes").select("id,document_id,title").eq("document_id", docId).maybeSingle(), supabase.from("podcasts").select("id,document_id,script,audio_url,status").eq("document_id", docId).maybeSingle(), ]); if (!mounted) return; setNote(docId, (n.data as NoteRow) ?? null); setFlashcards(docId, (f.data as FlashcardRow[]) ?? []); if (q.data) { const qq = await supabase .from("quiz_questions") .select("id,quiz_id,question,type,choices,correct,explanation,order_index") .eq("quiz_id", q.data.id) .order("order_index"); setQuiz(docId, { ...(q.data as any), questions: (qq.data ?? []) as QuizQuestionRow[] } as QuizRow); } else { setQuiz(docId, null); } setPodcast(docId, (p.data as PodcastRow) ?? null); setLoading(false); })(); // Realtime: document status updates const channel = supabase .channel(`doc-${docId}`) .on( "postgres_changes", { event: "UPDATE", schema: "public", table: "documents", filter: `id=eq.${docId}` }, (payload) => upsertDocument(payload.new as DocumentRow), ) .on( "postgres_changes", { event: "*", schema: "public", table: "podcasts", filter: `document_id=eq.${docId}` }, (payload) => setPodcast(docId, (payload.new as PodcastRow) ?? null), ) .subscribe(); return () => { mounted = false; supabase.removeChannel(channel); }; }, [docId, user, upsertDocument, setNote, setFlashcards, setQuiz, setPodcast]); const noteForDoc = docId ? notes[docId] ?? null : null; const docStatus = docId ? documents.find((d) => d.id === docId)?.status : undefined; const generate = async () => { if (!docId || streaming) return; setStreaming(true); setNote(docId, { id: "draft", document_id: docId, markdown: "" } as NoteRow); try { const final = await streamNotes({ documentId: docId, onDelta: (chunk) => { const cur = (useWorkspace.getState().notes[docId]?.markdown ?? "") + chunk; setNote(docId, { id: "draft", document_id: docId, markdown: cur } as NoteRow); }, }); const { data: fresh } = await supabase .from("notes") .select("id,document_id,markdown") .eq("document_id", docId) .maybeSingle(); setNote(docId, (fresh as NoteRow) ?? { id: "draft", document_id: docId, markdown: final }); } catch (e: any) { toast({ title: "Notes generation failed", description: e.message, variant: "destructive" }); } finally { setStreaming(false); } }; useEffect(() => { if (!docId) return; if (docStatus === "ready" && !noteForDoc?.markdown && autoStartedRef.current !== docId && !streaming) { autoStartedRef.current = docId; generate(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [docId, docStatus, noteForDoc?.markdown]); const [derivLoading, setDerivLoading] = useState(false); const [podcastLoading, setPodcastLoading] = useState(false); const runDerivatives = async () => { if (!docId || derivLoading) return; setDerivLoading(true); try { await generateDerivatives(docId); const [{ data: f }, { data: qq }] = await Promise.all([ supabase.from("flashcards").select("id,document_id,front,back,order_index").eq("document_id", docId).order("order_index"), supabase.from("quizzes").select("id,document_id,title").eq("document_id", docId).maybeSingle(), ]); setFlashcards(docId, (f as FlashcardRow[]) ?? []); if (qq) { const { data: questions } = await supabase .from("quiz_questions") .select("id,quiz_id,question,type,choices,correct,explanation,order_index") .eq("quiz_id", qq.id) .order("order_index"); setQuiz(docId, { ...(qq as any), questions: (questions ?? []) as QuizQuestionRow[] } as QuizRow); } else { setQuiz(docId, null); } toast({ title: "Flashcards & quiz ready" }); } catch (e: any) { toast({ title: "Generation failed", description: e.message, variant: "destructive" }); } finally { setDerivLoading(false); } }; const refreshPodcast = async () => { if (!docId) return; const { data } = await supabase .from("podcasts") .select("id,document_id,script,audio_url,status") .eq("document_id", docId) .maybeSingle(); setPodcast(docId, (data as PodcastRow) ?? null); }; const runPodcast = async () => { if (!docId || podcastLoading) return; setPodcastLoading(true); setPodcast(docId, { id: pod?.id ?? "draft", document_id: docId, script: pod?.script ?? null, audio_url: null, status: "generating", }); try { await generatePodcast(docId); await refreshPodcast(); toast({ title: "Podcast ready", description: "Your audio recap is ready to play." }); } catch (e: any) { await refreshPodcast(); toast({ title: "Podcast generation failed", description: e.message, variant: "destructive" }); } finally { setPodcastLoading(false); } }; const handleDelete = async () => { if (!docId) return; if (!confirm("Delete this document and all its assets?")) return; const { error } = await supabase.from("documents").delete().eq("id", docId); if (error) { toast({ title: "Delete failed", description: error.message, variant: "destructive" }); return; } removeDocument(docId); navigate("/app"); }; if (!doc) { return (
{loading ? : (

Document not found.

)}
); } const note = notes[doc.id] ?? null; const cards = flashcards[doc.id] ?? []; const qz = quiz[doc.id] ?? null; const pod = podcast[doc.id] ?? null; const isProcessing = doc.status === "pending" || doc.status === "processing"; return (
{/* Header */}
{outlet?.openMobileNav && ( )}
{doc.source_type} {doc.status === "ready" && Ready} {isProcessing && Processing…} {doc.status === "failed" && {doc.error_code ?? "Failed"}}

{doc.title}

{/* Main Content Area */} {isProcessing ? (
{/* Background scanline effect */}
) : (
Notes Flashcards Quiz Podcast Chat
{note?.markdown ? (
{note.markdown} {streaming && (
Generating…
)}
) : doc.status === "ready" ? (

Ready to generate

Click below to stream AI-generated study notes.

) : isProcessing ? ( ) : doc.status === "failed" ? ( ) : ( )}
{cards.length === 0 ? ( ) : (

{cards.length} flashcards

)}
{!qz || qz.questions.length === 0 ? ( ) : (

{qz.title} — {qz.questions.length} questions

)}
{!note?.markdown ? ( ) : pod?.audio_url ? (

Audio summary ready

Listen now or regenerate it from your latest notes.

) : pod?.status === "generating" || podcastLoading ? (
Generating podcast audio…

This can take a minute depending on the script length.

) : pod?.status === "failed" ? (

Podcast generation failed

Try again to rebuild the two-host audio version.

) : (

Generate podcast

Create a two-host audio recap from your generated notes.

)}
)}
); } function ExpertProcessing({ status, sourceType }: { status: string; sourceType: string }) { const [step, setStep] = useState(0); const steps = [ "Initializing Expert Extraction Engine...", "Analyzing Document Hierarchy...", "Mapping Frameworks & Key Concepts...", "Ensuring Lossless Capture of Sequential Data...", "Finalizing Exhaustive Markdown Structure...", ]; useEffect(() => { const interval = setInterval(() => { setStep((s) => (s < steps.length - 1 ? s + 1 : s)); }, 3000); return () => clearInterval(interval); }, []); return (

Expert Agent at Work

{steps[step]}

Source Type

{sourceType}

Extraction Mode

Lossless Exhaustive

Advanced Academic Data Recovery in Progress
); } function Placeholder({ title, desc }: { title: string; desc: string }) { return (

{title}

{desc}

); } function DerivativesEmpty({ kind, noteReady, loading, onGenerate, }: { kind: "flashcards" | "quiz"; noteReady: boolean; loading: boolean; onGenerate: () => void }) { if (!noteReady) { return ; } return (

Generate {kind}

We'll use your notes to create {kind === "flashcards" ? "study flashcards" : "a multi-format quiz"}.

); }