Download src/components/study-chat/RightWorkspacePanel.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 7.93 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/RightWorkspacePanel.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/study-chat/RightWorkspacePanel.tsx
-
curl -L -o RightWorkspacePanel.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/RightWorkspacePanel.tsx
7.93 kB
| "use client"; | |
| import type { ElementType } from "react"; | |
| import { | |
| CheckCircle2, | |
| ClipboardList, | |
| FileQuestion, | |
| FileText, | |
| GraduationCap, | |
| Layers3, | |
| Moon, | |
| Video, | |
| } from "lucide-react"; | |
| import type { StudentDashboard } from "@/lib/api/dashboard"; | |
| import type { ApiDocument, VideoRenderJob } from "@/lib/api/types"; | |
| import type { StudyIntent } from "@/lib/study-intents"; | |
| import type { ChatMessage } from "./types"; | |
| import { cardDisplayTitle, contextLabel, documentKindLabel, documentStatusLabel, formatDate } from "./utils"; | |
| import { PanelBlock, ToolCard } from "./WorkspacePanelPrimitives"; | |
| export function RecentOutputs({ | |
| messages, | |
| dashboard, | |
| videoJobs, | |
| }: { | |
| messages: ChatMessage[]; | |
| dashboard: StudentDashboard | null; | |
| videoJobs: VideoRenderJob[]; | |
| }) { | |
| const generatedOutputs = messages | |
| .filter((message) => message.card && message.card.kind !== "loading" && message.card.kind !== "error") | |
| .slice(-4) | |
| .reverse(); | |
| const dashboardOutputs = dashboard?.recent_results.slice(0, 3) ?? []; | |
| return ( | |
| <div className="space-y-2"> | |
| {generatedOutputs.length === 0 && dashboardOutputs.length === 0 && videoJobs.length === 0 ? ( | |
| <div className="rounded-2xl border border-dashed border-white/[0.08] px-4 py-5 text-sm leading-6 text-white/38"> | |
| Recent notes, quizzes, flashcards, video plans, and render jobs will appear here. | |
| </div> | |
| ) : null} | |
| {generatedOutputs.map((message) => ( | |
| <div key={message.id} className="rounded-2xl border border-white/[0.07] bg-white/[0.025] p-3"> | |
| <p className="text-sm font-semibold text-white/82">{message.card ? cardDisplayTitle(message.card) : message.text}</p> | |
| <p className="mt-1 line-clamp-2 text-xs leading-5 text-white/38">{message.text}</p> | |
| </div> | |
| ))} | |
| {dashboardOutputs.map((result) => ( | |
| <div key={result.id} className="rounded-2xl border border-white/[0.07] bg-white/[0.025] p-3"> | |
| <p className="text-sm font-semibold text-white/82">{result.label}</p> | |
| <p className="mt-1 text-xs text-white/38"> | |
| {result.source_title} - {formatDate(result.created_at)} | |
| </p> | |
| </div> | |
| ))} | |
| {videoJobs.slice(0, 3).map((job) => ( | |
| <div key={job.id} className="rounded-2xl border border-white/[0.07] bg-white/[0.025] p-3"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <p className="min-w-0 truncate text-sm font-semibold text-white/82">{job.title}</p> | |
| <span className="shrink-0 text-[10px] font-semibold uppercase text-white/38">{job.status}</span> | |
| </div> | |
| <div className="mt-2 h-1 rounded-full bg-white/[0.06]"> | |
| <div className="h-1 rounded-full bg-white/55" style={{ width: `${Math.max(4, Math.min(job.progress ?? 0, 100))}%` }} /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| export function RightWorkspacePanel({ | |
| documents, | |
| selectedDocuments, | |
| messages, | |
| dashboard, | |
| videoJobs, | |
| onAction, | |
| onUpload, | |
| }: { | |
| documents: ApiDocument[]; | |
| selectedDocuments: ApiDocument[]; | |
| messages: ChatMessage[]; | |
| dashboard: StudentDashboard | null; | |
| videoJobs: VideoRenderJob[]; | |
| onAction: (intent: StudyIntent, prompt: string) => void; | |
| onUpload: () => void; | |
| }) { | |
| const sourceContext = selectedDocuments.length ? contextLabel(selectedDocuments, documents) : "your selected source"; | |
| const readyCount = documents.filter((document) => document.status === "ready").length; | |
| const paperCount = documents.filter((document) => document.material_type === "question_paper" || /pyq|question paper|previous/i.test(document.title)).length; | |
| const tools: Array<{ label: string; description: string; intent: StudyIntent; prompt: string; icon: ElementType }> = [ | |
| { label: "Notes", description: "Exam-ready scoring notes", intent: "generate_notes", prompt: `Make exam-ready notes from ${sourceContext}.`, icon: FileText }, | |
| { label: "Quiz", description: "Active recall with traps", intent: "generate_quiz", prompt: `Create a quiz from ${sourceContext} with answers and explanations.`, icon: FileQuestion }, | |
| { label: "Flashcards", description: "Fast memory cards", intent: "generate_flashcards", prompt: `Make flashcards from ${sourceContext}.`, icon: Layers3 }, | |
| { label: "Study Video", description: "Scene plan plus builder handoff", intent: "create_video_plan", prompt: `Create a study video plan from ${sourceContext}.`, icon: Video }, | |
| { label: "PYQ Analysis", description: paperCount ? "Use uploaded papers" : "Material-only until papers exist", intent: "analyze_pyq", prompt: `Analyze PYQ patterns from ${sourceContext}. Be explicit about available evidence.`, icon: GraduationCap }, | |
| { label: "Study Plan", description: "Rank what scores first", intent: "continue_learning", prompt: `Build a study plan from ${sourceContext}.`, icon: ClipboardList }, | |
| { label: "Last Night Revision", description: "Tonight-only priorities", intent: "create_revision_plan", prompt: `Make a last-night revision plan from ${sourceContext}.`, icon: Moon }, | |
| { label: "Exam Mode", description: "Board-style answers", intent: "generate_exam_answer", prompt: `Create exam-mode answers from ${sourceContext}.`, icon: CheckCircle2 }, | |
| ]; | |
| return ( | |
| <aside className="hidden min-h-0 w-[288px] shrink-0 flex-col border-l border-white/[0.075] bg-[#050505]/88 p-3 xl:flex"> | |
| <div className="min-h-0 flex-1 overflow-y-auto pr-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> | |
| <PanelBlock title="Sources" actionLabel="Add" onAction={onUpload}> | |
| {selectedDocuments.length > 0 ? ( | |
| <div className="space-y-2"> | |
| {selectedDocuments.map((document) => ( | |
| <div key={document.id} className="rounded-2xl border border-white/[0.07] bg-white/[0.025] p-3"> | |
| <p className="line-clamp-2 text-sm font-semibold text-white/82">{document.title}</p> | |
| <p className="mt-1 text-xs text-white/38"> | |
| {documentKindLabel(document)} - {documentStatusLabel(document.status)} | |
| </p> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="rounded-2xl border border-dashed border-white/[0.08] px-4 py-5 text-sm leading-6 text-white/38"> | |
| Select sources to ground DocDoe's answers. | |
| </div> | |
| )} | |
| </PanelBlock> | |
| <PanelBlock title="Study Tools"> | |
| <div className="grid gap-2"> | |
| {tools.map((tool) => ( | |
| <ToolCard | |
| key={tool.label} | |
| icon={tool.icon} | |
| label={tool.label} | |
| description={tool.description} | |
| onClick={() => onAction(tool.intent, tool.prompt)} | |
| /> | |
| ))} | |
| </div> | |
| </PanelBlock> | |
| <PanelBlock title="Recent Outputs"> | |
| <RecentOutputs messages={messages} dashboard={dashboard} videoJobs={videoJobs} /> | |
| </PanelBlock> | |
| <PanelBlock title="Mission Snapshot"> | |
| <div className="grid gap-2"> | |
| {[ | |
| ["Ready sources", `${readyCount}/${documents.length || 0}`], | |
| ["Paper evidence", paperCount ? `${paperCount} paper${paperCount === 1 ? "" : "s"}` : "Not uploaded"], | |
| ["Video jobs", String(videoJobs.length)], | |
| ["Focus", dashboard?.study_focus.subject || selectedDocuments[0]?.subject || "Choose source"], | |
| ].map(([label, value]) => ( | |
| <div key={label} className="flex items-center justify-between rounded-2xl border border-white/[0.07] bg-white/[0.025] px-3 py-2.5"> | |
| <span className="text-xs text-white/38">{label}</span> | |
| <span className="max-w-[9rem] truncate text-sm font-semibold text-white/78">{value}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </PanelBlock> | |
| </div> | |
| </aside> | |
| ); | |
| } | |