Download src/components/library/NotesView.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/library/NotesView.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/library/NotesView.tsx
-
curl -L -o NotesView.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/library/NotesView.tsx
13 kB
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import Link from "next/link"; | |
| import { ChevronDown, Copy, Download, ExternalLink, FileText, MessageSquareText, RefreshCcw, Trash2, X } from "lucide-react"; | |
| import { deleteNote, listNotes, type SavedNote } from "@/lib/student-activity"; | |
| import { useStudentActivityVersion } from "@/hooks/useStudentActivity"; | |
| import { buildStudyIntentUrl } from "@/lib/study-intents"; | |
| // ============================================================================= | |
| // Real saved notes — every entry here was saved by the student (from a | |
| // StudyChat answer, a notes-generation handoff, or the tuition demo). No | |
| // placeholder rows: the empty state points at the actions that create notes. | |
| // Extracted from the standalone /notes route so /library can compose it as a | |
| // tab; the /notes route is now a thin wrapper around this view. | |
| // ============================================================================= | |
| const SOURCE_LABEL: Record<SavedNote["source"], string> = { | |
| "study-chat": "StudyChat", | |
| "learn-anything": "Learn Anything", | |
| "tuition-demo": "Tuition demo", | |
| }; | |
| export function NotesView() { | |
| const [notes, setNotes] = useState<SavedNote[]>([]); | |
| const [copiedId, setCopiedId] = useState<string | null>(null); | |
| const [subjectFilter, setSubjectFilter] = useState(""); | |
| const [topicFilter, setTopicFilter] = useState(""); | |
| const [sourceFilter, setSourceFilter] = useState(""); | |
| const [openNote, setOpenNote] = useState<SavedNote | null>(null); | |
| const activityVersion = useStudentActivityVersion(); | |
| useEffect(() => { | |
| // localStorage read on mount + whenever notes change (this tab or another). | |
| let cancelled = false; | |
| void Promise.resolve().then(() => { | |
| if (!cancelled) setNotes(listNotes()); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [activityVersion]); | |
| function handleCopy(note: SavedNote) { | |
| void navigator.clipboard?.writeText(`${note.title}\n\n${note.content}`).then(() => { | |
| setCopiedId(note.id); | |
| setTimeout(() => setCopiedId(null), 1500); | |
| }); | |
| } | |
| function handleDelete(id: string) { | |
| deleteNote(id); | |
| setNotes(listNotes()); | |
| if (openNote?.id === id) setOpenNote(null); | |
| } | |
| function handleExport(note: SavedNote) { | |
| const blob = new Blob([`${note.title}\n\n${note.content}`], { type: "text/plain;charset=utf-8" }); | |
| const url = URL.createObjectURL(blob); | |
| const anchor = document.createElement("a"); | |
| anchor.href = url; | |
| anchor.download = `${note.title.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "docdoe-note"}.txt`; | |
| anchor.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| const subjects = Array.from(new Set(notes.map((note) => note.subject).filter((value): value is string => Boolean(value)))).sort(); | |
| const topics = Array.from(new Set(notes.map((note) => note.topic).filter((value): value is string => Boolean(value)))).sort(); | |
| const filteredNotes = notes.filter( | |
| (note) => | |
| (!subjectFilter || note.subject === subjectFilter) && | |
| (!topicFilter || note.topic === topicFilter) && | |
| (!sourceFilter || note.source === sourceFilter), | |
| ); | |
| return ( | |
| <div className="text-white"> | |
| <header className="mb-8"> | |
| <h2 className="text-2xl font-semibold text-white sm:text-3xl">Your saved notes</h2> | |
| <p className="mt-2 max-w-2xl text-sm leading-6 text-white/52"> | |
| Answers you saved from StudyChat and notes generated from your lessons. Stored on this device. | |
| </p> | |
| </header> | |
| {openNote ? ( | |
| <article className="mb-6 rounded-2xl border border-white/[0.12] bg-[#0B0B0B] p-5 sm:p-6"> | |
| <div className="flex items-start justify-between gap-4"> | |
| <div> | |
| <p className="text-[10px] font-bold uppercase tracking-[0.14em] text-white/40">Open note</p> | |
| <h3 className="mt-2 text-xl font-semibold text-white">{openNote.title}</h3> | |
| <p className="mt-1 text-xs text-white/45"> | |
| {[openNote.subject, openNote.topic, SOURCE_LABEL[openNote.source]].filter(Boolean).join(" · ")} | |
| </p> | |
| </div> | |
| <button type="button" onClick={() => setOpenNote(null)} aria-label="Close open note" className="rounded-lg p-2 text-white/45 hover:text-white"> | |
| <X className="size-4" /> | |
| </button> | |
| </div> | |
| <p className="mt-5 whitespace-pre-line text-sm leading-7 text-white/75">{openNote.content}</p> | |
| </article> | |
| ) : null} | |
| {notes.length > 0 ? ( | |
| <div className="mb-6 grid gap-3 rounded-2xl border border-white/[0.08] bg-[#0B0B0B] p-4 sm:grid-cols-3"> | |
| <label className="text-[11px] font-semibold text-white/55"> | |
| Subject | |
| <select | |
| aria-label="Filter notes by subject" | |
| value={subjectFilter} | |
| onChange={(event) => setSubjectFilter(event.target.value)} | |
| className="mt-1.5 min-h-10 w-full rounded-lg border border-white/[0.1] bg-black px-3 text-sm text-white" | |
| > | |
| <option value="">All subjects</option> | |
| {subjects.map((subject) => <option key={subject} value={subject}>{subject}</option>)} | |
| </select> | |
| </label> | |
| <label className="text-[11px] font-semibold text-white/55"> | |
| Topic | |
| <select | |
| aria-label="Filter notes by topic" | |
| value={topicFilter} | |
| onChange={(event) => setTopicFilter(event.target.value)} | |
| className="mt-1.5 min-h-10 w-full rounded-lg border border-white/[0.1] bg-black px-3 text-sm text-white" | |
| > | |
| <option value="">All topics</option> | |
| {topics.map((topic) => <option key={topic} value={topic}>{topic}</option>)} | |
| </select> | |
| </label> | |
| <label className="text-[11px] font-semibold text-white/55"> | |
| Source | |
| <select | |
| aria-label="Filter notes by source" | |
| value={sourceFilter} | |
| onChange={(event) => setSourceFilter(event.target.value)} | |
| className="mt-1.5 min-h-10 w-full rounded-lg border border-white/[0.1] bg-black px-3 text-sm text-white" | |
| > | |
| <option value="">All sources</option> | |
| {Object.entries(SOURCE_LABEL).map(([value, label]) => <option key={value} value={value}>{label}</option>)} | |
| </select> | |
| </label> | |
| </div> | |
| ) : null} | |
| {notes.length === 0 ? ( | |
| <div className="rounded-2xl border border-dashed border-white/[0.12] bg-black/30 p-10 text-center"> | |
| <FileText className="mx-auto mb-4 size-8 text-white/40" /> | |
| <p className="text-base font-semibold text-white">No notes saved yet</p> | |
| <p className="mx-auto mt-2 max-w-sm text-sm leading-6 text-white/52"> | |
| Ask a doubt in Study Chat and press <span className="text-white/80">"Save as note"</span> on the | |
| answer, or use the notes generator from any lesson. | |
| </p> | |
| <Link | |
| href={buildStudyIntentUrl({ intent: "generate_notes", prompt: "Make exam notes on a topic I choose" })} | |
| className="mt-6 inline-flex rounded-xl bg-white px-6 py-2.5 text-sm font-semibold text-black" | |
| > | |
| Make your first note | |
| </Link> | |
| </div> | |
| ) : ( | |
| <div className="dd-stagger space-y-3"> | |
| {filteredNotes.map((note) => ( | |
| <details key={note.id} className="group rounded-2xl border border-white/[0.08] bg-[#0B0B0B]"> | |
| <summary className="flex cursor-pointer list-none items-center gap-3 px-5 py-4 [&::-webkit-details-marker]:hidden"> | |
| <FileText className="size-4 shrink-0 text-white/45" /> | |
| <span className="min-w-0 flex-1 truncate text-sm font-semibold text-white">{note.title}</span> | |
| <span className="hidden shrink-0 text-[11px] text-white/40 sm:inline"> | |
| {SOURCE_LABEL[note.source]} | |
| {note.provenance === "starter" ? " · starter/sample" : ""} | |
| {` · ${new Date(note.at).toLocaleDateString()}`} | |
| </span> | |
| <ChevronDown className="size-4 shrink-0 text-white/40 transition-transform group-open:rotate-180" /> | |
| </summary> | |
| <div className="border-t border-white/[0.06] px-5 py-4"> | |
| <p className="whitespace-pre-line text-sm leading-7 text-white/70">{note.content}</p> | |
| <div className="mt-4 flex flex-wrap items-center gap-2"> | |
| <button | |
| type="button" | |
| onClick={() => setOpenNote(note)} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg bg-white px-3 text-xs font-semibold text-black" | |
| > | |
| <ExternalLink className="size-3.5" /> Open | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => handleCopy(note)} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 text-xs font-semibold text-white/70 transition hover:text-white" | |
| > | |
| <Copy className="size-3.5" /> | |
| {copiedId === note.id ? "Copied ✓" : "Copy"} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => handleExport(note)} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 text-xs font-semibold text-white/70 transition hover:text-white" | |
| > | |
| <Download className="size-3.5" /> Export .txt | |
| </button> | |
| <Link | |
| href={buildStudyIntentUrl({ | |
| intent: "generate_notes", | |
| prompt: `Regenerate clearer exam-ready notes for:\n${note.title}\n${note.content.slice(0, 400)}`, | |
| run: true, | |
| })} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 text-xs font-semibold text-white/70 transition hover:text-white" | |
| > | |
| <RefreshCcw className="size-3.5" /> | |
| Regenerate | |
| </Link> | |
| <Link | |
| href={buildStudyIntentUrl({ | |
| intent: "teach_topic", | |
| prompt: `Use this saved note as context and help me study it:\n${note.title}\n${note.content.slice(0, 400)}`, | |
| run: false, | |
| })} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 text-xs font-semibold text-white/70 transition hover:text-white" | |
| > | |
| <MessageSquareText className="size-3.5" /> Send to StudyChat | |
| </Link> | |
| <Link | |
| href={buildStudyIntentUrl({ | |
| intent: "generate_quiz", | |
| prompt: `Make a 5-question practice quiz from this note:\n${note.title}\n${note.content.slice(0, 400)}`, | |
| run: true, | |
| })} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 text-xs font-semibold text-white/70 transition hover:text-white" | |
| > | |
| Turn into quiz | |
| </Link> | |
| <Link | |
| href={buildStudyIntentUrl({ | |
| intent: "generate_flashcards", | |
| prompt: `Turn this note into flashcards:\n${note.title}\n${note.content.slice(0, 400)}`, | |
| run: true, | |
| })} | |
| className="inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 text-xs font-semibold text-white/70 transition hover:text-white" | |
| > | |
| Turn into flashcards | |
| </Link> | |
| <button | |
| type="button" | |
| onClick={() => handleDelete(note.id)} | |
| className="ml-auto inline-flex min-h-9 items-center gap-1.5 rounded-lg px-3 text-xs font-semibold text-white/40 transition hover:text-white/70" | |
| > | |
| <Trash2 className="size-3.5" /> | |
| Delete | |
| </button> | |
| </div> | |
| </div> | |
| </details> | |
| ))} | |
| {filteredNotes.length === 0 ? ( | |
| <div className="rounded-2xl border border-dashed border-white/[0.12] p-8 text-center text-sm text-white/52"> | |
| No saved notes match these filters. | |
| </div> | |
| ) : null} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |