Download src/components/dashboard/VideoTutorBrainModal.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/dashboard/VideoTutorBrainModal.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/dashboard/VideoTutorBrainModal.tsx
-
curl -L -o VideoTutorBrainModal.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/dashboard/VideoTutorBrainModal.tsx
15.9 kB
| "use client"; | |
| import { useCallback, useState } from "react"; | |
| import { | |
| Sparkles, | |
| Settings2, | |
| Clock, | |
| Zap, | |
| Tv2, | |
| LoaderCircle, | |
| CheckCircle2, | |
| Target, | |
| } from "lucide-react"; | |
| import type { LucideIcon } from "lucide-react"; | |
| import { Modal, PrimaryButton, SecondaryButton } from "@/components/dashboard/dashboardPrimitives"; | |
| import { useJobPolling } from "@/hooks/useJobPolling"; | |
| import { getVideoPlanExtended } from "@/lib/api/intelligence"; | |
| import { createVideoRenderJob, createVideoScenePlan, getVideoRenderJob } from "@/lib/api/video"; | |
| import { normalizeApiError } from "@/lib/api/client"; | |
| import type { | |
| StudentLevel, | |
| VideoFormat, | |
| VideoPlanV2Response, | |
| VideoPlanDurationType, | |
| VideoPlanStyle, | |
| VideoRenderJob, | |
| VoiceMode, | |
| VoiceProfile, | |
| } from "@/lib/api/types"; | |
| import { prepareScenePlanForRender } from "@/lib/video/durations"; | |
| import { cn } from "@/lib/utils"; | |
| const ALLOW_LOCAL_PREVIEW_AUDIO = process.env.NODE_ENV !== "production"; | |
| const durationOptions: { value: VideoPlanDurationType; label: string; time: string; icon: LucideIcon }[] = [ | |
| { value: "quick_concept", label: "5-minute video", time: "5 min", icon: Zap }, | |
| { value: "exam_focus", label: "10-minute video", time: "10 min", icon: Target }, | |
| { value: "deep_masterclass", label: "30-minute video", time: "30 min", icon: Tv2 }, | |
| ]; | |
| const styleOptions: { value: VideoPlanStyle; label: string; desc: string }[] = [ | |
| { value: "normal_teacher", label: "Normal Teacher", desc: "Traditional academic style" }, | |
| { value: "visual_tutor", label: "Visual Tutor", desc: "Heavy on diagrams & flowcharts" }, | |
| { value: "anime_tutor", label: "Anime Tutor", desc: "High energy, character-led" }, | |
| { value: "malayalam_english_tutor", label: "Mixed Mode", desc: "Malayalam + English key terms" }, | |
| { value: "exam_war_mode", label: "Exam War Mode", desc: "Strictly mark-focused, fast-paced" }, | |
| ]; | |
| const formatOptions: { value: VideoFormat; label: string }[] = [ | |
| { value: "16:9", label: "Wide Class Video" }, | |
| { value: "9:16", label: "Phone Revision" }, | |
| ]; | |
| const voiceOptions: { value: VoiceProfile; label: string; voiceMode: VoiceMode }[] = [ | |
| { value: "anime_female_tutor", label: "Anime Tutor", voiceMode: "english_soft" }, | |
| { value: "calm_teacher", label: "Calm Teacher", voiceMode: "english_soft" }, | |
| { value: "malayalam_english_teacher", label: "Malayalam + English Tutor", voiceMode: "ml_en_mix" }, | |
| { value: "exam_war_coach", label: "Exam War Coach", voiceMode: "english_soft" }, | |
| { value: "premium_human_teacher", label: "Premium Human Teacher", voiceMode: "english_soft" }, | |
| ]; | |
| export function VideoTutorBrainModal({ | |
| isOpen, | |
| onClose, | |
| documentId, | |
| studentLevel, | |
| onPlanGenerated | |
| }: { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| documentId: string; | |
| studentLevel: StudentLevel; | |
| onPlanGenerated: (plan: VideoPlanV2Response) => void; | |
| }) { | |
| const [step, setStep] = useState<"config" | "loading" | "success">("config"); | |
| const [duration, setDuration] = useState<VideoPlanDurationType>("exam_focus"); | |
| const [style, setStyle] = useState<VideoPlanStyle>("visual_tutor"); | |
| const [format, setFormat] = useState<VideoFormat>("16:9"); | |
| const [voiceProfile, setVoiceProfile] = useState<VoiceProfile>("calm_teacher"); | |
| const [generatedPlan, setGeneratedPlan] = useState<VideoPlanV2Response | null>(null); | |
| const [renderStep, setRenderStep] = useState<"idle" | "planning" | "voice" | "building" | "ready" | "failed">("idle"); | |
| const [renderJob, setRenderJob] = useState<VideoRenderJob | null>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const pollRenderJob = useCallback(async () => { | |
| if (!renderJob) throw new Error("No video render job is running."); | |
| return getVideoRenderJob(renderJob.id); | |
| }, [renderJob]); | |
| useJobPolling({ | |
| enabled: Boolean(renderJob && (renderJob.status === "queued" || renderJob.status === "running")), | |
| fetcher: pollRenderJob, | |
| shouldContinue: (job) => job.status === "queued" || job.status === "running", | |
| onData: (job) => { | |
| setRenderJob(job); | |
| if (job.status === "ready") { | |
| setRenderStep("ready"); | |
| } else if (job.status === "failed") { | |
| setRenderStep("failed"); | |
| setError("DocDoe could not finish this video. Try again from a shorter source."); | |
| } else if (job.current_step?.startsWith("generating_audio")) { | |
| setRenderStep("voice"); | |
| } else { | |
| setRenderStep("building"); | |
| } | |
| }, | |
| onError: () => setError("DocDoe could not refresh the video status. Open Videos to check it."), | |
| }); | |
| async function generatePlan() { | |
| setStep("loading"); | |
| setError(null); | |
| try { | |
| const data = await getVideoPlanExtended(documentId, duration, style); | |
| setGeneratedPlan(data); | |
| setStep("success"); | |
| onPlanGenerated(data); | |
| } catch (caughtError) { | |
| setError(normalizeApiError(caughtError)); | |
| setStep("config"); | |
| } | |
| } | |
| async function buildVideo() { | |
| setRenderStep("planning"); | |
| setError(null); | |
| try { | |
| const selectedVoice = voiceOptions.find((option) => option.value === voiceProfile) ?? voiceOptions[1]; | |
| const durationMinutes = | |
| duration === "quick_concept" ? 5 : | |
| duration === "exam_focus" ? 10 : | |
| duration === "deep_masterclass" ? 30 : | |
| 10; | |
| const scenePlan = prepareScenePlanForRender( | |
| await createVideoScenePlan({ | |
| document_id: documentId, | |
| title: generatedPlan?.title ?? "DocDoe Study Video", | |
| language: selectedVoice.voiceMode === "ml_en_mix" ? "Malayalam + English" : "English", | |
| duration_minutes: durationMinutes, | |
| visual_style: style === "anime_tutor" ? "anime_visual_explainer" : "clean_explainer", | |
| video_format: format, | |
| }), | |
| durationMinutes * 60, | |
| ); | |
| setRenderStep("voice"); | |
| const job = await createVideoRenderJob({ | |
| scene_plan: scenePlan, | |
| title: scenePlan.title, | |
| allow_mock_audio: ALLOW_LOCAL_PREVIEW_AUDIO, | |
| voice_mode: selectedVoice.voiceMode, | |
| voice: selectedVoice.value, | |
| language: scenePlan.language, | |
| }); | |
| setRenderJob(job); | |
| setRenderStep(job.status === "ready" ? "ready" : "building"); | |
| } catch (caughtError) { | |
| setError(normalizeApiError(caughtError)); | |
| setRenderStep("failed"); | |
| } | |
| } | |
| return ( | |
| <Modal isOpen={isOpen} onClose={onClose} title="Video Tutor Brain V2"> | |
| {step === "config" && ( | |
| <div className="space-y-6 py-2"> | |
| <div className="space-y-3"> | |
| <p className="flex items-center gap-2 text-[11px] font-bold text-violet-400 uppercase tracking-widest"> | |
| <Clock className="size-3" /> | |
| Select Duration | |
| </p> | |
| <div className="grid grid-cols-2 gap-2"> | |
| {durationOptions.map((opt) => ( | |
| <button | |
| key={opt.value} | |
| onClick={() => setDuration(opt.value)} | |
| className={cn( | |
| "p-3 rounded-2xl border text-left transition-all", | |
| duration === opt.value | |
| ? "border-violet-500 bg-violet-500/10 shadow-[0_0_20px_rgba(124,58,237,0.15)]" | |
| : "border-white/5 bg-white/[0.02] hover:border-white/10" | |
| )} | |
| > | |
| <opt.icon className={cn("size-4 mb-2", duration === opt.value ? "text-violet-400" : "text-white/20")} /> | |
| <p className="text-[12px] font-bold text-white">{opt.label}</p> | |
| <p className="text-[10px] font-medium text-white/30">{opt.time}</p> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <p className="flex items-center gap-2 text-[11px] font-bold text-violet-400 uppercase tracking-widest"> | |
| <Settings2 className="size-3" /> | |
| Teaching Style | |
| </p> | |
| <div className="grid gap-2"> | |
| {styleOptions.map((opt) => ( | |
| <button | |
| key={opt.value} | |
| onClick={() => setStyle(opt.value)} | |
| className={cn( | |
| "flex items-center justify-between p-3 rounded-xl border transition-all text-left", | |
| style === opt.value | |
| ? "border-violet-500 bg-violet-500/10" | |
| : "border-white/5 bg-white/[0.02] hover:bg-white/[0.04]" | |
| )} | |
| > | |
| <div className="min-w-0"> | |
| <p className="text-[12px] font-bold text-white">{opt.label}</p> | |
| <p className="text-[10px] font-medium text-white/30 truncate">{opt.desc}</p> | |
| </div> | |
| {style === opt.value && <CheckCircle2 className="size-4 text-violet-400 shrink-0" />} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="grid gap-3 sm:grid-cols-2"> | |
| <div className="space-y-2"> | |
| <p className="text-[11px] font-bold text-violet-400 uppercase tracking-widest">Format</p> | |
| <div className="grid gap-2"> | |
| {formatOptions.map((opt) => ( | |
| <button | |
| key={opt.value} | |
| onClick={() => setFormat(opt.value)} | |
| className={cn( | |
| "rounded-xl border px-3 py-2 text-left text-[12px] font-bold transition", | |
| format === opt.value ? "border-violet-500 bg-violet-500/10 text-white" : "border-white/5 bg-white/[0.02] text-white/50", | |
| )} | |
| > | |
| {opt.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <p className="text-[11px] font-bold text-violet-400 uppercase tracking-widest">Voice</p> | |
| <select | |
| value={voiceProfile} | |
| onChange={(event) => setVoiceProfile(event.target.value as VoiceProfile)} | |
| className="h-10 w-full rounded-xl border border-white/5 bg-black/40 px-3 text-[12px] font-bold text-white outline-none" | |
| > | |
| {voiceOptions.map((option) => ( | |
| <option key={option.value} value={option.value}>{option.label}</option> | |
| ))} | |
| </select> | |
| <p className="text-[10.5px] leading-4 text-white/32">Current level: {studentLevel}. Long videos can take more time to prepare.</p> | |
| </div> | |
| </div> | |
| {error ? ( | |
| <p className="rounded-2xl border border-rose-300/12 bg-rose-500/[0.06] px-3 py-2 text-[12px] leading-5 text-rose-50/70"> | |
| {error} | |
| </p> | |
| ) : null} | |
| <PrimaryButton onClick={generatePlan} className="w-full h-12 shadow-[0_0_30px_rgba(124,58,237,0.2)]"> | |
| <Sparkles className="size-4" /> | |
| Generate Extended Video Plan | |
| </PrimaryButton> | |
| </div> | |
| )} | |
| {step === "loading" && ( | |
| <div className="py-20 flex flex-col items-center justify-center gap-4 text-center px-6"> | |
| <div className="relative"> | |
| <div className="absolute inset-0 bg-violet-500/20 blur-3xl rounded-full animate-pulse" /> | |
| <LoaderCircle className="relative size-12 animate-spin text-violet-400" strokeWidth={1.5} /> | |
| </div> | |
| <div className="space-y-1"> | |
| <p className="text-[15px] font-bold text-white tracking-tight">DocDoe is building your study video plan.</p> | |
| <p className="text-[12px] text-white/40">Creating hook, analogy, and source-based teaching flow for your {duration.replace("_", " ")} video.</p> | |
| </div> | |
| </div> | |
| )} | |
| {step === "success" && generatedPlan && ( | |
| <div className="space-y-6 py-2"> | |
| <div className="flex items-center gap-4 p-4 rounded-[24px] bg-emerald-500/10 border border-emerald-500/20"> | |
| <div className="size-10 grid place-items-center rounded-xl bg-emerald-500/20 text-emerald-400"> | |
| <CheckCircle2 className="size-6" /> | |
| </div> | |
| <div className="min-w-0"> | |
| <p className="text-[11px] font-bold text-emerald-400 uppercase tracking-wider">Plan Ready</p> | |
| <h3 className="text-[15px] font-bold text-white truncate">{generatedPlan.title}</h3> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <div className="space-y-1.5"> | |
| <p className="text-[10px] font-bold text-white/30 uppercase tracking-widest">Master Script Hook</p> | |
| <div className="p-3 rounded-xl bg-white/5 border border-white/5 text-[13px] text-white/70 italic leading-relaxed"> | |
| "{generatedPlan.hook}" | |
| </div> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <p className="text-[10px] font-bold text-white/30 uppercase tracking-widest">The Analogy</p> | |
| <p className="text-[13px] text-white/80 font-medium px-1">{generatedPlan.analogy}</p> | |
| </div> | |
| <div className="space-y-2"> | |
| <p className="text-[10px] font-bold text-white/30 uppercase tracking-widest">Scene Breakdown ({generatedPlan.sections.length} scenes)</p> | |
| <div className="grid gap-2"> | |
| {generatedPlan.sections.map(s => ( | |
| <div key={s.scene_number} className="flex gap-3 p-2.5 rounded-xl bg-black/20 border border-white/5"> | |
| <span className="text-[11px] font-black text-violet-400/50 mt-0.5">#{s.scene_number}</span> | |
| <div className="min-w-0"> | |
| <p className="text-[12px] font-bold text-white/90">{s.label}</p> | |
| <p className="text-[11px] text-white/40 mt-1">{s.visual_suggestion}</p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| {renderStep !== "idle" ? ( | |
| <div className="rounded-2xl border border-white/5 bg-white/[0.03] p-3"> | |
| <p className="text-[11px] font-bold uppercase tracking-wider text-violet-200/70"> | |
| {renderStep === "planning" ? "Preparing" : | |
| renderStep === "voice" ? "Adding voice" : | |
| renderStep === "building" ? "Building" : | |
| renderStep === "ready" ? "Your video is ready" : | |
| "Could not build video"} | |
| </p> | |
| {renderJob ? ( | |
| <p className="mt-1 text-[12px] text-white/52">Open Videos to track progress and preview the final study video.</p> | |
| ) : error ? ( | |
| <p className="mt-1 text-[12px] text-rose-50/70">{error}</p> | |
| ) : ( | |
| <p className="mt-1 text-[12px] text-white/42">Long videos can take more time to prepare.</p> | |
| )} | |
| </div> | |
| ) : null} | |
| <div className="grid grid-cols-2 gap-3"> | |
| <SecondaryButton onClick={() => setStep("config")} className="h-11">Edit Config</SecondaryButton> | |
| <PrimaryButton onClick={buildVideo} disabled={renderStep === "planning" || renderStep === "voice" || renderStep === "building"} className="h-11"> | |
| {renderStep === "idle" || renderStep === "failed" ? "Build Video" : renderStep === "ready" ? "Build Again" : "Preparing"} | |
| </PrimaryButton> | |
| </div> | |
| {renderStep === "ready" ? ( | |
| <PrimaryButton onClick={onClose} className="w-full h-11">Close & Save</PrimaryButton> | |
| ) : null} | |
| </div> | |
| )} | |
| </Modal> | |
| ); | |
| } | |