import React, { useEffect, useState } from "react";
import {
ArrowSquareOut,
BookOpen,
CheckCircle,
NotePencil,
PencilSimple,
Plus,
Trash,
} from "@phosphor-icons/react";
import { api, downloadText } from "./api.js";
import {
Empty,
IconButton,
LoadingButton,
Modal,
Spinner,
handleTabListKeyDown,
} from "./ui.jsx";
import { Markdown } from "./Markdown.jsx";
import { messageSources } from "./messageSources.js";
function MarkdownField({
label,
value,
onChange,
limit = 100000,
materialId,
sources = [],
}) {
const [preview, setPreview] = useState(false);
return (
{label}
setPreview(false)}
>
Write
setPreview(true)}
>
Preview
{preview ? (
) : (
);
}
export function LearningEditor({ editor, onClose, onSave, material }) {
const [draft, setDraft] = useState({}),
[busy, setBusy] = useState(false),
[error, setError] = useState("");
useEffect(() => {
setDraft(editor?.data || {});
setError("");
}, [editor]);
if (!editor) return null;
const card = editor.type === "card",
highlight = editor.type === "highlight";
const set = (key, value) => setDraft((v) => ({ ...v, [key]: value }));
return (
{
if (!open && !busy) onClose();
}}
title={
card
? draft.id
? "Edit review card"
: "Create review card"
: highlight
? "Passage note"
: draft.id
? "Edit note"
: "Save a note"
}
className="learning-editor"
>
);
}
export function NotebookDialog({
open,
onOpenChange,
material,
notes,
onEdit,
onCreate,
onDelete,
onOrigin,
onCard,
onCapture,
}) {
const [capturing, setCapturing] = useState("");
const [captureError, setCaptureError] = useState("");
return (
{notes.length} {notes.length === 1 ? "note" : "notes"}
New note
{captureError && {captureError}
}
{notes.length ? (
{notes.map((note) => (
{note.title}
onEdit(note)}>
onDelete(note)}
>
{note.quote && {note.quote} }
!c.comparison) },
material?.sources,
)}
comparisonCitations={note.citations}
onComparisonPage={(page) =>
onOrigin({
material_id: note.citations.find(
(c) => c.comparison && c.page === page,
)?.materialId,
page,
})
}
onPage={(page) => onOrigin({ ...note, page, chat_id: null })}
/>
{onCapture && {
setCapturing(note.id); setCaptureError("");
try { await onCapture(note); }
catch (error) { setCaptureError(error.message || "Unable to save this note."); }
finally { setCapturing(""); }
}}>{capturing === note.id ? "Saving…" : "Save to Notes"} }
onOrigin(note)}>
{note.chat_id
? "Original conversation"
: note.page
? `Page ${note.page}`
: "Open material"}
onCard({
question: note.title,
answer: note.body,
quote: note.quote,
page: note.page,
note_id: note.id,
})
}
>
Create review card
))}
) : (
Save a passage or an assistant answer here.
)}
{!!notes.length && (
downloadText(
"notes.md",
notes
.map(
(n) =>
`# ${n.title}\n\n${n.quote ? "> " + n.quote.replace(/\n/g, "\n> ") + "\n\n" : ""}${n.body}`,
)
.join("\n\n---\n\n"),
)
}
>
Export notes
)}
);
}
function interval(card, grade) {
return grade === "again"
? "10m"
: `${Math.round(grade === "hard" ? Math.max(1, card.interval_days * 1.2) : grade === "good" ? (card.interval_days ? Math.max(3, card.interval_days * 2) : 1) : card.interval_days ? Math.max(4, card.interval_days * 2.5) : 4)}d`;
}
export function ReviewDialog({
open,
onOpenChange,
onEdit,
onDelete,
onSource,
revision = 0,
}) {
const [cards, setCards] = useState([]),
[loading, setLoading] = useState(false),
[revealed, setRevealed] = useState(false),
[busy, setBusy] = useState(false),
[error, setError] = useState(""),
[tab, setTab] = useState("today"),
[completed, setCompleted] = useState(0);
useEffect(() => {
if (!open) return;
let live = true;
setLoading(true);
setError("");
api("/review")
.then((data) => {
if (live) setCards(data.cards);
})
.catch((e) => {
if (live) setError(e.message);
})
.finally(() => {
if (live) setLoading(false);
});
return () => {
live = false;
};
}, [open, revision]);
useEffect(() => {
if (open) {
setRevealed(false);
setCompleted(0);
setTab("today");
}
}, [open]);
const due = cards.filter((c) => c.due_at <= new Date().toISOString()),
card = due[0];
useEffect(() => setRevealed(false), [card?.id]);
return (
setTab("today")}
>
Today {due.length}
setTab("all")}
>
All cards {cards.length}
{error && (
{error}
)}
{loading ? (
) : tab === "all" ? (
{cards.map((c) => (
{c.question}
{c.material_title} ·{" "}
{c.due_at <= new Date().toISOString()
? "Due now"
: new Date(c.due_at).toLocaleDateString()}
onEdit(c)}>
onDelete(c)}>
))}
{!cards.length && (
Create one from an exercise, highlight or note.
)}
) : card ? (
<>
{card.material_title}
{completed} reviewed · {due.length} remaining
onSource(card)}>
Open source
onEdit(card)}>
{!revealed ? (
setRevealed(true)}
>
Show answer
) : (
{["again", "hard", "good", "easy"].map((grade) => (
{
if (busy) return;
setBusy(true);
setError("");
try {
const updated = await api(`/cards/${card.id}/review`, {
method: "POST",
body: { grade },
});
setCards((list) =>
list.map((c) =>
c.id === card.id ? { ...c, ...updated } : c,
),
);
setCompleted((n) => n + 1);
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
}}
>
{grade[0].toUpperCase() + grade.slice(1)}
{interval(card, grade)}
))}
)}
>
) : (
{completed
? `${completed} cards reviewed.`
: cards.length
? "Your next cards will appear when they are due."
: "Create a card from an exercise, highlight or note."}
)}
);
}