- Home: "Your five minutes": a rotating Observe/Interpret/Apply prompt on the passage whose matching note is thinnest in your own words, a 5:00 clock and a box that appends to that note. Pasting is blocked; sessions show in Insights. - Study: a Reading list tab (R) for the study: books and articles to consult, with a note and a tick; merges across devices. - Starting a study from a reading-plan day now uses the notes template you last picked when starting a study by hand (logic moved to utils/planStudy.js). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
108 lines
6.0 KiB
React
108 lines
6.0 KiB
React
// Insights: your studies seen together — this week's own work, themes (tags), words you keep studying,
|
||
// what slipped in recalls, and follow-ups still open.
|
||
import { useMemo } from 'react';
|
||
import { useApp } from '../context/AppContext.js';
|
||
import PageShell from '../components/study/PageShell.jsx';
|
||
import { loadProjectById } from '../utils/projectStorage.js';
|
||
import { followUpList, slippedList, themeIndex, weeklySummary, wordIndex } from '../utils/insights.js';
|
||
|
||
const card = 'rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5';
|
||
const open = 'text-xs text-[var(--r-accent)] hover:underline';
|
||
|
||
function Stat({ label, value }) {
|
||
return (
|
||
<div className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-4 py-3">
|
||
<p className="text-2xl font-medium tabular-nums">{value}</p>
|
||
<p className="text-xs text-[var(--r-sub)]">{label}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function InsightsPage() {
|
||
const { projectIndex, openStudyChunk, goHome } = useApp();
|
||
const data = useMemo(() => {
|
||
const projects = projectIndex.map((e) => loadProjectById(e.id)).filter(Boolean);
|
||
return { week: weeklySummary(projects), themes: themeIndex(projects), words: wordIndex(projects), slipped: slippedList(projects), followUps: followUpList(projects) };
|
||
}, [projectIndex]);
|
||
const { week, themes, words, slipped, followUps } = data;
|
||
const link = (item) => <button type="button" onClick={() => openStudyChunk(item.projectId, item.chunkId)} className={open}>{item.reference} · {item.projectTitle}</button>;
|
||
|
||
return (
|
||
<PageShell title="Insights" active="studies" onBack={goHome} maxWidth="max-w-4xl">
|
||
<main className="mx-auto max-w-4xl space-y-5 px-4 pb-10">
|
||
<section aria-label="This week" className={card}>
|
||
<h2 className="text-sm font-medium">This week</h2>
|
||
<p className="mb-3 mt-0.5 text-xs text-[var(--r-sub)]">Counted over the chunks you edited in the last 7 days. Pasted text isn’t counted as yours.</p>
|
||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||
<Stat label="Chunks worked on" value={week.chunksTouched} />
|
||
<Stat label="Words of your own" value={week.ownWords} />
|
||
<Stat label="Your own words" value={week.ownShare === null ? '–' : `${week.ownShare}%`} />
|
||
<Stat label="Recalls done" value={week.recalls} />
|
||
</div>
|
||
{(week.teachBacks > 0 || week.fiveMinutes > 0) && <p className="mt-2 text-xs text-[var(--r-sub)]">{[week.fiveMinutes > 0 && `${week.fiveMinutes} five-minute session${week.fiveMinutes === 1 ? '' : 's'}`, week.teachBacks > 0 && `${week.teachBacks} teach-it-back summar${week.teachBacks === 1 ? 'y' : 'ies'}`].filter(Boolean).join(' · ')} this week.</p>}
|
||
</section>
|
||
|
||
<section aria-label="Open follow-ups" className={card}>
|
||
<h2 className="text-sm font-medium">Follow-ups</h2>
|
||
{followUps.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">None yet. Add them under “More” in the Study stage.</p> : (
|
||
<ul className="mt-2 space-y-1.5 text-sm">
|
||
{followUps.map((f) => (
|
||
<li key={f.id} className="flex flex-wrap items-baseline gap-x-3">
|
||
<span className={f.done ? 'text-[var(--r-sub)] line-through' : ''}>{f.text}</span>
|
||
{link(f)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
|
||
<section aria-label="What slipped" className={card}>
|
||
<h2 className="text-sm font-medium">What slipped in recalls</h2>
|
||
{slipped.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">Nothing yet. After a weekly recall, note what you missed.</p> : (
|
||
<ul className="mt-2 space-y-3 text-sm">
|
||
{slipped.map((r) => (
|
||
<li key={`${r.chunkId}-${r.at}`}>
|
||
<p className="text-xs text-[var(--r-sub)]">{new Date(r.at).toLocaleDateString()} · {link(r)}</p>
|
||
<ul className="ml-4 list-disc">{r.items.map((i) => <li key={i}>{i}</li>)}</ul>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
|
||
<section aria-label="Themes" className={card}>
|
||
<h2 className="text-sm font-medium">Themes</h2>
|
||
{themes.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">Tag a chunk (under “More”) and it appears here with every other chunk on the same theme.</p> : (
|
||
<div className="mt-2 space-y-3">
|
||
{themes.map((t) => (
|
||
<details key={t.tag} className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2">
|
||
<summary className="cursor-pointer text-sm"><span className="font-medium">{t.tag}</span> <span className="text-xs text-[var(--r-sub)]">· {t.count} chunk{t.count === 1 ? '' : 's'}</span></summary>
|
||
<ul className="mt-2 space-y-2">
|
||
{t.items.map((i) => (
|
||
<li key={`${i.chunkId}`} className="text-sm">{link(i)}{i.snippet && <p className="font-serif text-[13px] text-[var(--r-sub)]">{i.snippet}</p>}</li>
|
||
))}
|
||
</ul>
|
||
</details>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section aria-label="Words you keep studying" className={card}>
|
||
<h2 className="text-sm font-medium">Words you keep studying</h2>
|
||
{words.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">Words you study in two or more places show up here.</p> : (
|
||
<ul className="mt-2 space-y-2 text-sm">
|
||
{words.map((w) => (
|
||
<li key={w.key}>
|
||
<span className="font-serif text-base">{w.lexeme}</span> <span className="text-xs text-[var(--r-sub)]">{[w.transliteration, w.strong, w.gloss].filter(Boolean).join(' · ')} · {w.count} places</span>
|
||
<span className="mt-0.5 flex flex-wrap gap-x-3">{w.places.map((p) => <span key={p.chunkId}>{link(p)}</span>)}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
</main>
|
||
</PageShell>
|
||
);
|
||
}
|