File size: 7,928 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | "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>
);
}
|