Download src/components/study-chat/StudyCards.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/StudyCards.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/study-chat/StudyCards.tsx
-
curl -L -o StudyCards.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/StudyCards.tsx
15.6 kB
| "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} />; | |
| } | |