File size: 15,617 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 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | "use client";
import {
BookOpen,
ClipboardList,
Download,
FileQuestion,
Layers3,
LoaderCircle,
Moon,
RefreshCw,
ShieldCheck,
Sparkles,
Upload,
Video,
} from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
inferStudyIntentFromPrompt,
type StudyIntent,
} from "@/lib/study-intents";
import type { StudyCard } from "./types";
import { RichStudyCard } from "./StudyCardPrimitives";
import { StagedThinkingLine } from "./StagedThinking";
import {
ExamAnswerCard,
ExplanationCard,
FlashcardDeckCard,
NotesCard,
QuizCard,
RevisionPlanCard,
StudyPathCard,
VideoPlanCard,
} from "./StudyResultCards";
import { AnswerCorrectionCard } from "./AnswerCorrectionCard";
import { DeepChapterCard, VisualLessonCard } from "./DeepChapterCards";
import { InlineVideoRender } from "./InlineVideoRender";
import { PyqAnalysisCard } from "./PyqAnalysisCard";
import { StructuredStudyResponseCard } from "./StructuredStudyResponseCard";
import { PlainAssistantMessage } from "./PlainAssistantMessage";
export { PlainAssistantMessage };
// ββ Casual / welcome card ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function WelcomeCard({ card, onAction }: { card: Extract<StudyCard, { kind: "welcome" }>; onAction: (intent: StudyIntent, prompt: string) => void }) {
const suggestions: Array<{ label: string; intent: StudyIntent; prompt: string; icon: typeof BookOpen }> = [
{ label: "Teach a topic", intent: "teach_topic", prompt: "Teach me ", icon: BookOpen },
{ label: "One-night plan", intent: "create_revision_plan", prompt: "I need to learn ", icon: Moon },
{ label: "Make notes", intent: "generate_notes", prompt: "Make notes on ", icon: ClipboardList },
{ label: "Quiz me", intent: "generate_quiz", prompt: "Make a quiz on ", icon: FileQuestion },
{ label: "Study video", intent: "create_video_plan", prompt: "Create a study video plan for ", icon: Video },
];
return (
<div className="animate-msg-assistant max-w-[min(48rem,100%)] rounded-[1.25rem] border border-white/[0.085] bg-white/[0.035] px-4 py-4 shadow-[0_14px_50px_rgba(0,0,0,0.2)]">
<div className="flex items-center gap-2 mb-3">
<span className="grid size-7 shrink-0 place-items-center rounded-xl bg-white text-black">
<Sparkles className="size-3.5" />
</span>
<span className="text-[11px] font-bold uppercase tracking-[0.14em] text-white/40">Study Chat</span>
</div>
<div className="text-sm leading-7 text-white/76 prose-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
}}
>
{card.message}
</ReactMarkdown>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{suggestions.map((s) => {
const Icon = s.icon;
return (
<button
key={s.label}
type="button"
onClick={() => onAction(s.intent, s.prompt)}
className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.1] bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white/66 transition hover:border-white/[0.2] hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22"
>
<Icon className="size-3.5" />
{s.label}
</button>
);
})}
</div>
</div>
);
}
// ββ Needs-source CTA card ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function NeedsSourceCard({
card,
onUpload,
}: {
card: Extract<StudyCard, { kind: "needs_source" }>;
onUpload?: () => void;
}) {
return (
<div className="animate-msg-assistant max-w-[min(42rem,100%)] rounded-[1.25rem] border border-white/[0.085] bg-white/[0.025] px-4 py-4 shadow-[0_14px_50px_rgba(0,0,0,0.2)]">
<p className="text-sm leading-7 text-white/76">{card.message}</p>
{onUpload ? (
<button
type="button"
onClick={onUpload}
className="mt-4 inline-flex items-center gap-2 rounded-xl border border-white/[0.12] bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white/72 transition hover:bg-white/[0.09] hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22"
>
<Upload className="size-4" />
Upload source
</button>
) : null}
</div>
);
}
// ββ Clarify options card βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const CLARIFY_OPTIONS: Array<{ label: string; intent: StudyIntent; prompt: string; icon: typeof BookOpen }> = [
{ label: "Explain topic", intent: "teach_topic", prompt: "Explain ", icon: BookOpen },
{ label: "One-night plan", intent: "create_revision_plan", prompt: "I need to learn ", icon: Moon },
{ label: "Make notes", intent: "generate_notes", prompt: "Make notes on ", icon: ClipboardList },
{ label: "Quiz me", intent: "generate_quiz", prompt: "Quiz me on ", icon: FileQuestion },
{ label: "Flashcards", intent: "generate_flashcards", prompt: "Make flashcards for ", icon: Layers3 },
{ label: "Study plan", intent: "continue_learning", prompt: "Build my study plan", icon: Moon },
];
function ClarifyCard({
onAction,
}: {
card: Extract<StudyCard, { kind: "clarify" }>;
onAction: (intent: StudyIntent, prompt: string) => void;
}) {
return (
<div className="animate-msg-assistant max-w-[min(42rem,100%)] rounded-[1.25rem] border border-white/[0.085] bg-white/[0.025] px-4 py-4 shadow-[0_14px_50px_rgba(0,0,0,0.2)]">
<p className="text-sm leading-7 text-white/76">
Specify the topic, exam, time left, or goal. I can work from a typed question, pasted text, or an uploaded source.
</p>
<div className="mt-4 flex flex-wrap gap-2">
{CLARIFY_OPTIONS.map((opt) => {
const Icon = opt.icon;
return (
<button
key={opt.label}
type="button"
onClick={() => onAction(opt.intent, opt.prompt)}
className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.1] bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white/66 transition hover:border-white/[0.2] hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22"
>
<Icon className="size-3.5" />
{opt.label}
</button>
);
})}
</div>
</div>
);
}
// ββ Real PYQ papers card βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function PyqPapersCard({ card }: { card: Extract<StudyCard, { kind: "pyq_papers" }> }) {
const data = card.data;
const papers = (data.papers ?? []).filter((p) => p?.url);
const years = (data.years_covered ?? []).join(", ");
return (
<div className="animate-msg-assistant w-full rounded-[1.25rem] border border-white/[0.085] bg-white/[0.025] p-4 shadow-[0_14px_50px_rgba(0,0,0,0.2)] sm:p-5">
<div className="mb-3 flex items-center gap-2">
<span className="grid size-7 shrink-0 place-items-center rounded-xl bg-white text-black">
<FileQuestion className="size-3.5" />
</span>
<div>
<p className="text-[11px] font-bold uppercase tracking-[0.14em] text-white/40">Real previous papers</p>
<p className="text-sm font-semibold text-white">{data.board} {data.level !== "SSLC" ? data.level : ""} β actual board papers</p>
</div>
</div>
<p className="mb-3 flex items-center gap-1.5 text-xs leading-5 text-emerald-300/80">
<ShieldCheck className="size-3.5 shrink-0" />
Verified Kerala board papers with answer keys β not AI-generated.{years ? ` Years: ${years}.` : ""}
</p>
<div className="space-y-2">
{papers.map((p) => (
<a
key={p.url}
href={p.url}
target="_blank"
rel="noreferrer noopener"
className="group flex items-center gap-3 rounded-2xl border border-white/[0.08] bg-white/[0.025] p-3 transition hover:border-white/[0.2] hover:bg-white/[0.05] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/22"
>
<span className="grid size-9 shrink-0 place-items-center rounded-xl border border-white/[0.08] bg-white/[0.04] text-white/60 group-hover:text-white">
<Download className="size-4" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-semibold text-white/86">{p.title}</span>
<span className="block truncate text-xs text-white/38">
{p.source === "official" ? "Official Kerala portal" : p.source === "archive" ? "Public paper archive" : p.source}
{p.medium ? ` Β· ${p.medium}` : ""}
</span>
</span>
</a>
))}
</div>
<p className="mt-3 text-xs leading-5 text-white/38">{data.trust_note}</p>
</div>
);
}
export function StudyCardRenderer({
card,
onAction,
onUpload,
onStartRender,
}: {
card: StudyCard;
onAction: (intent: StudyIntent, prompt: string) => void;
onUpload?: () => void;
onStartRender?: (topic: string, sourceId: string | null, minutes: number) => void;
}) {
if (card.kind === "streaming") {
return (
<div className="animate-msg-assistant w-full">
<PlainAssistantMessage text={card.text} streaming />
{card.evidenceLabel ? (
<p className="mt-2 text-[11px] text-white/38">Basis: {card.evidenceLabel}</p>
) : null}
</div>
);
}
if (card.kind === "loading") {
return (
<RichStudyCard icon={LoaderCircle} title="DocDoe is preparing your answer" eyebrow="Reading and structuring">
<div className="flex items-start gap-3 text-sm text-white/60">
<span className="inline-flex items-center gap-1 pt-0.5" aria-label="DocDoe is preparing your answer">
<span className="typing-dot" />
<span className="typing-dot" />
<span className="typing-dot" />
</span>
<StagedThinkingLine intent={card.intent} />
</div>
</RichStudyCard>
);
}
if (card.kind === "error") {
const variant = card.variant ?? "generic";
const copy: Record<string, { title: string; eyebrow: string; hint: string | null }> = {
quota: {
title: "You've hit today's limit",
eyebrow: "Plan limit",
hint: "Your limit refreshes later. Upgrade your plan for more.",
},
daily_limit: {
title: "Today's AI allowance is used",
eyebrow: "Daily limit",
hint: "Your saved classes and notes are still available.",
},
rate_limit: {
title: "Going a bit fast",
eyebrow: "Slow down",
hint: "Wait a few seconds, then send again.",
},
auth: {
title: "Session expired",
eyebrow: "Sign in needed",
hint: "Sign in again to pick up where you left off.",
},
offline: {
title: "Request did not complete",
eyebrow: "Study tools busy",
hint: "This usually clears in a moment. Try again.",
},
generic: {
title: "Request did not complete",
eyebrow: "Something went wrong",
hint: "Try again, or rephrase your request.",
},
};
const { title, eyebrow, hint } = copy[variant] ?? copy.generic;
return (
<RichStudyCard icon={RefreshCw} title={title} eyebrow={eyebrow}>
<p className="text-sm leading-6 text-white/68">{card.message}</p>
{hint ? <p className="mt-2 text-xs leading-5 text-white/42">{hint}</p> : null}
{variant === "auth" ? (
<a
href="/login"
className="mt-4 inline-flex items-center gap-2 rounded-xl bg-white px-4 py-2 text-sm font-semibold text-black transition hover:bg-white/88"
>
Sign in
</a>
) : variant === "daily_limit" ? (
<a
href="/classes"
className="mt-4 inline-flex items-center gap-2 rounded-xl bg-white px-4 py-2 text-sm font-semibold text-black transition hover:bg-white/88"
>
Continue saved classes
</a>
) : variant === "quota" ? (
<a
href="/pricing"
className="mt-4 inline-flex items-center gap-2 rounded-xl border border-white/14 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white/80 transition hover:bg-white/[0.08] hover:text-white"
>
See plans
</a>
) : (
<button
type="button"
onClick={() => onAction(inferStudyIntentFromPrompt(card.prompt), card.prompt)}
className="mt-4 inline-flex items-center gap-2 rounded-xl bg-white px-4 py-2 text-sm font-semibold text-black transition hover:bg-white/88"
>
Try again
<RefreshCw className="size-4" />
</button>
)}
</RichStudyCard>
);
}
// ββ New response-mode card kinds ββββββββββββββββββββββββββββββββββββββββββ
if (card.kind === "welcome") return <WelcomeCard card={card} onAction={onAction} />;
if (card.kind === "needs_source") return <NeedsSourceCard card={card} onUpload={onUpload} />;
if (card.kind === "clarify") return <ClarifyCard card={card} onAction={onAction} />;
if (card.kind === "pyq_papers") return <PyqPapersCard card={card} />;
if (card.kind === "notes") return <NotesCard card={card} onAction={onAction} />;
if (card.kind === "quiz") return <QuizCard card={card} onAction={onAction} />;
if (card.kind === "flashcards") return <FlashcardDeckCard card={card} onAction={onAction} />;
if (card.kind === "exam_answer") return <ExamAnswerCard card={card} onAction={onAction} />;
if (card.kind === "revision_plan") return <RevisionPlanCard card={card} onAction={onAction} />;
if (card.kind === "study_path") return <StudyPathCard card={card} onAction={onAction} />;
if (card.kind === "video_plan") return <VideoPlanCard card={card} onAction={onAction} onStartRender={onStartRender} />;
if (card.kind === "video_render") return <InlineVideoRender topic={card.topic} sourceId={card.sourceId} minutes={card.minutes} format={card.format} />;
if (card.kind === "answer_correction") return <AnswerCorrectionCard data={card.data} topic={card.topic} meta={card.meta} />;
if (card.kind === "deep_chapter") return <DeepChapterCard data={card.data} topic={card.topic} />;
if (card.kind === "structured_study_response") return <StructuredStudyResponseCard data={card.data} onAction={onAction} />;
if (card.kind === "visual_lesson") return <VisualLessonCard data={card.data} topic={card.topic} />;
if (card.kind === "pyq_analysis") return <PyqAnalysisCard data={card.data} topic={card.topic} meta={card.meta} onAction={onAction} />;
return <ExplanationCard card={card} onAction={onAction} />;
}
|