import { useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/hooks/useAuth"; import { supabase } from "@/integrations/supabase/client"; import { useNavigate } from "react-router-dom"; import { Loader2, Upload, FileText, Youtube, Sparkles } from "lucide-react"; import { useDropzone } from "react-dropzone"; import { triggerIngest } from "@/lib/pipeline"; import { extractFileText } from "@/lib/extract"; const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50MB const AUDIO_EXTS = ["mp3", "wav", "m4a", "ogg", "flac", "webm"]; const VIDEO_EXTS = ["mp4", "mov", "mkv"]; const IMAGE_EXTS = ["png", "jpg", "jpeg", "webp", "bmp"]; export default function UploadDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) { const { user } = useAuth(); const { toast } = useToast(); const navigate = useNavigate(); const [submitting, setSubmitting] = useState(false); const [extractionProgress, setExtractionProgress] = useState(0); // Text mode const [textTitle, setTextTitle] = useState(""); const [textContent, setTextContent] = useState(""); // YouTube mode const [ytUrl, setYtUrl] = useState(""); // File mode const [file, setFile] = useState(null); const { getRootProps, getInputProps, isDragActive } = useDropzone({ multiple: false, onDrop: (files) => setFile(files[0] ?? null), }); const reset = () => { setTextTitle(""); setTextContent(""); setYtUrl(""); setFile(null); setExtractionProgress(0); }; const createTextDoc = async () => { if (!user || !textContent.trim()) return; setSubmitting(true); try { // EXPERT OVERRIDE const EXPERT_PREFIX = `[EXPERT ADVISORY: ACT AS AN EXPERT ACADEMIC DATA EXTRACTION AGENT. STYLE: PROFESSIONAL, CLEAN MARKDOWN. NO EMOJIS ALLOWED. DIRECTIVE: LOSSLESS EXTRACTION. ZERO OMISSION. TEMPLATE: Overview -> Key Points -> Tables for Frameworks -> Sub-headings for Detailed Terms.]\n\n`; const finalContent = EXPERT_PREFIX + textContent; const { data, error } = await supabase .from("documents") .insert({ user_id: user.id, title: textTitle.trim() || "Untitled note", source_type: "text", raw_text: finalContent, status: "ready", }) .select("id") .single(); if (error) throw error; toast({ title: "Document created", description: "Expert agent has saved your notes with lossless directives." }); reset(); onOpenChange(false); navigate(`/app/doc/${data!.id}`); } catch (e: any) { toast({ title: "Failed", description: e.message, variant: "destructive" }); } finally { setSubmitting(false); } }; const createYoutubeDoc = async () => { if (!user || !ytUrl.trim()) return; setSubmitting(true); try { const { data, error } = await supabase .from("documents") .insert({ user_id: user.id, title: ytUrl, source_type: "youtube", source_url: ytUrl, status: "pending", }) .select("id") .single(); if (error) throw error; toast({ title: "Source Queued", description: "Expert Agent is fetching the transcript..." }); reset(); onOpenChange(false); navigate(`/app/doc/${data!.id}`); triggerIngest(data!.id).catch((e) => toast({ title: "Extraction failed", description: e.message, variant: "destructive" }), ); } catch (e: any) { toast({ title: "Failed", description: e.message, variant: "destructive" }); } finally { setSubmitting(false); } }; const createFileDoc = async () => { if (!user || !file) return; if (file.size > MAX_FILE_BYTES) { toast({ title: "File too large", description: "Expert Agent requires files under 50MB.", variant: "destructive" }); return; } setSubmitting(true); try { const ext = file.name.split(".").pop()?.toLowerCase() ?? ""; const isPdf = ext === "pdf"; const isDocx = ext === "docx" || ext === "doc"; const isImage = IMAGE_EXTS.includes(ext); const isAudio = AUDIO_EXTS.includes(ext); const isVideo = VIDEO_EXTS.includes(ext); // PDF/DOCX/Image: extract text in the browser, no file upload needed if (isPdf || isDocx || isImage) { toast({ title: "Expert Extraction Initialized", description: "Running parallel OCR and hierarchy mapping..." }); const { text: extractedText, sourceType } = await extractFileText(file, (p) => setExtractionProgress(p)); if (!extractedText || extractedText.trim().length < 5) { throw new Error("Expert Agent could not extract readable text from this file."); } // EXPERT OVERRIDE: Prepend instructions to ensure lossless extraction even if backend is old const EXPERT_PREFIX = `[EXPERT ADVISORY: ACT AS AN EXPERT ACADEMIC DATA EXTRACTION AGENT. STYLE: PROFESSIONAL, CLEAN MARKDOWN. NO EMOJIS ALLOWED. DIRECTIVE: LOSSLESS EXTRACTION. ZERO OMISSION. TEMPLATE: Overview -> Key Points -> Tables for Frameworks -> Sub-headings for Detailed Terms.]\n\n`; const finalRawText = EXPERT_PREFIX + extractedText; const { data, error } = await supabase .from("documents") .insert({ user_id: user.id, title: file.name, source_type: sourceType, raw_text: finalRawText, status: "pending", }) .select("id") .single(); if (error) throw error; toast({ title: "Extraction Complete", description: "Expert Agent is finalizing the document..." }); reset(); onOpenChange(false); navigate(`/app/doc/${data!.id}`); triggerIngest(data!.id).catch((e) => toast({ title: "Ingest failed", description: e.message, variant: "destructive" }), ); return; } // Audio/Video: upload to storage; ingest function transcribes via Groq Whisper if (!isAudio && !isVideo) { throw new Error("Unsupported file type. Use PDF, DOCX, Image, audio or video."); } const sourceType = isAudio ? "audio" : "video"; const path = `${user.id}/${crypto.randomUUID()}-${file.name}`; const { error: upErr } = await supabase.storage.from("uploads").upload(path, file); if (upErr) throw upErr; const { data, error } = await supabase .from("documents") .insert({ user_id: user.id, title: file.name, source_type: sourceType, source_url: path, status: "pending", }) .select("id") .single(); if (error) throw error; toast({ title: "Source Uploaded", description: "Expert Agent is initiating transcription..." }); reset(); onOpenChange(false); navigate(`/app/doc/${data!.id}`); triggerIngest(data!.id).catch((e) => toast({ title: "Ingest failed", description: e.message, variant: "destructive" }), ); } catch (e: any) { toast({ title: "Failed", description: e.message, variant: "destructive" }); } finally { setSubmitting(false); } }; return ( Add Academic Source File YouTube Text
{file ? (

{file.name}

) : (

{isDragActive ? "Drop here…" : "Drag your academic document here"}

PDF, DOCX, Images, MP3, MP4 Supported

)}
{submitting && extractionProgress > 0 && (
Extraction Progress {extractionProgress}%
)}
setYtUrl(e.target.value)} placeholder="https://youtube.com/watch?v=..." className="bg-slate-900 border-slate-800 text-white placeholder:text-slate-600" />
setTextTitle(e.target.value)} placeholder="Untitled Lecture Notes" className="bg-slate-900 border-slate-800 text-white placeholder:text-slate-600" />