diff --git a/src/components/RecallHistory.jsx b/src/components/RecallHistory.jsx
new file mode 100644
index 0000000..0dfb618
--- /dev/null
+++ b/src/components/RecallHistory.jsx
@@ -0,0 +1,47 @@
+// Home: how your weekly recalls have gone — the last eight weeks at a glance, then every recall you've written.
+import { useMemo, useState } from 'react';
+import { useApp } from '../context/AppContext.js';
+import { loadProjectById } from '../utils/projectStorage.js';
+import { recallHistory, recallSummary } from '../utils/recall.js';
+
+const LABEL = { solid: 'Had it', partly: 'Partly', forgot: 'Slipped' };
+const DOT = { solid: 'bg-[var(--r-accent)] border-[var(--r-accent)]', partly: 'bg-[var(--r-hl)] border-[var(--r-accent)]', forgot: 'border-[var(--r-sub)]' };
+
+export default function RecallHistory() {
+ const { projectIndex, openStudyChunk } = useApp();
+ const [open, setOpen] = useState(false);
+ const history = useMemo(() => recallHistory(projectIndex.map((e) => loadProjectById(e.id)).filter(Boolean)), [projectIndex]);
+ if (history.length === 0) return null;
+ const { counts, total, recent } = recallSummary(history);
+
+ return (
+
+
+ {open && (
+
+ )}
+
+ );
+}
diff --git a/src/components/WeeklyRecall.jsx b/src/components/WeeklyRecall.jsx
new file mode 100644
index 0000000..7a30379
--- /dev/null
+++ b/src/components/WeeklyRecall.jsx
@@ -0,0 +1,77 @@
+// Home card: once a week, write what you remember of a passage you studied, then compare it with your notes.
+import { useMemo, useState } from 'react';
+import { useApp } from '../context/AppContext.js';
+import { loadProjectById } from '../utils/projectStorage.js';
+import { pickRecall } from '../utils/recall.js';
+
+const RATE = [
+ { id: 'solid', label: 'I had it' },
+ { id: 'partly', label: 'Partly' },
+ { id: 'forgot', label: 'Most of it slipped' },
+];
+const box = 'mb-4 rounded-xl border border-[var(--r-line)] bg-[var(--r-bar)] px-4 py-3 text-sm';
+const noteLabels = [['observation', 'Observe'], ['interpretation', 'Interpret'], ['application', 'Apply']];
+
+export default function WeeklyRecall() {
+ const { projectIndex, saveChunkRecall, openStudyChunk } = useApp();
+ const [step, setStep] = useState('card'); // card -> write -> compare -> done
+ const [text, setText] = useState('');
+ const pick = useMemo(
+ () => pickRecall(projectIndex.map((e) => loadProjectById(e.id)).filter(Boolean)),
+ [projectIndex],
+ );
+ if (step === 'done') return
Recall saved. See you next week.
;
+ if (!pick) return null;
+
+ const finish = (rating) => {
+ saveChunkRecall(pick.projectId, pick.chunk.id, { at: Date.now(), text: text.trim(), rating });
+ setStep('done');
+ };
+
+ if (step === 'card') {
+ return (
+
+ Weekly recall: {pick.reference} · {pick.projectTitle}
+
+
+ );
+ }
+ return (
+
+ {pick.reference} · {pick.projectTitle}
+ {step === 'write' ? (
+ <>
+ From memory, no peeking: what did this passage say, what did it mean, and what did it ask of you?
+
+ );
+}
diff --git a/src/components/study/NotesPane.jsx b/src/components/study/NotesPane.jsx
index 1feaade..c272a4d 100644
--- a/src/components/study/NotesPane.jsx
+++ b/src/components/study/NotesPane.jsx
@@ -2,6 +2,7 @@
// area for background notes and tags so the page stays quiet.
import { useEffect, useState } from 'react';
import DictateButton, { appendSpoken } from '../DictateButton.jsx';
+import { ownShare, ownWords, pastedWords, withPaste, clearPastes } from '../../utils/ownWork.js';
import { templateFromChunk, templatePrompt } from '../../utils/studyTemplates.js';
const SECTIONS = [
@@ -36,11 +37,23 @@ export default function NotesPane({ chunk, updateChunk, allTags, templateId, sav
};
const suggestions = allTags.filter((t) => !tags.some((x) => x.toLowerCase() === t.toLowerCase()));
+ const share = ownShare(chunk);
+ const keepPaste = (field) => (e) => {
+ const next = withPaste(chunk, field, e.clipboardData?.getData('text'));
+ if (next) updateChunk(chunk.id, { aiPastes: next });
+ };
+
return (
+ {share !== null && (
+
+ {share === 100 ? 'All of these notes are your own words.' : `${share}% of these notes are your own words.`}
+
+ )}
{SECTIONS.map(({ field, label, hint, placeholder }) => {
const isOpen = open === field;
const n = words(chunk[field]);
+ const pasted = pastedWords(chunk, field);
return (
{isOpen && (
)}
diff --git a/src/hooks/useProjectLibrary.js b/src/hooks/useProjectLibrary.js
index 62dd81e..223f8fb 100644
--- a/src/hooks/useProjectLibrary.js
+++ b/src/hooks/useProjectLibrary.js
@@ -221,6 +221,32 @@ export function useProjectLibrary({
});
};
+ // Saves a weekly recall (what you remembered, and how it went) on one chunk of a stored study.
+ const saveChunkRecall = (id, chunkId, recall) => {
+ const loaded = loadProjectById(id);
+ if (!loaded) return;
+ const updated = {
+ ...loaded,
+ lastEdited: Date.now(),
+ chapters: loaded.chapters.map((ch) => ({
+ ...ch,
+ chunks: ch.chunks.map((c) => (c.id === chunkId ? { ...c, recalls: [...(c.recalls ?? []), recall] } : c)),
+ })),
+ };
+ saveProjectToStorage(updated);
+ unsyncedStudies.add(id);
+ setProjectIndex(loadProjectIndex());
+ if (project?.id === id) setProject(updated);
+ saveRemoteProject(updated, { baseEdited: getSyncBase(id) }).then((result) => {
+ if (result.ok) {
+ const serverEdited = result.data?.lastEdited ?? updated.lastEdited;
+ setSyncBase(id, serverEdited);
+ unsyncedStudies.remove(id);
+ try { saveProjectToStorage({ ...updated, lastEdited: serverEdited }); setProjectIndex(loadProjectIndex()); } catch { /* already saved */ }
+ } else if (result.status === 409) toast('That study changed on another device. Open it to choose which version to keep.', { tone: 'error' });
+ });
+ };
+
// Deleting is immediate here, with an Undo for a few seconds; only after that is the server copy
// removed (and straight away if the tab is closed first, so a deleted project never comes back).
const finishDelete = (id) => {
@@ -287,6 +313,7 @@ export function useProjectLibrary({
openStudyChunk,
renameProjectInStorage,
setProjectSeries,
+ saveChunkRecall,
deleteProject,
pullLatestFromServer,
};
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index 3112ecd..cd0ce36 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -2,6 +2,8 @@ import { useEffect, useRef, useState } from 'react';
import { useApp } from '../context/AppContext.js';
import { filterStudies, groupBySeries, isTagQuery, seriesList, tagCounts } from '../utils/studyFilter.js';
import BookCover from '../components/BookCover.jsx';
+import WeeklyRecall from '../components/WeeklyRecall.jsx';
+import RecallHistory from '../components/RecallHistory.jsx';
import IqDailyCard from '../components/iq/IqDailyCard.jsx';
import IqNarratorOptions, { useLazyNarrators } from '../components/iq/IqNarratorOptions.jsx';
import PageShell from '../components/study/PageShell.jsx';
@@ -135,6 +137,8 @@ export default function HomePage() {
>
+
+
{autoRestoredCount !== null && (
Synced {autoRestoredCount} project{autoRestoredCount > 1 ? 's' : ''} from another device.
diff --git a/src/pages/ScriptPage.jsx b/src/pages/ScriptPage.jsx
index 5f0e863..abcfc7a 100644
--- a/src/pages/ScriptPage.jsx
+++ b/src/pages/ScriptPage.jsx
@@ -11,6 +11,7 @@ import { useToast } from '../components/Toasts.jsx';
import StudioShell from '../components/study/StudioShell.jsx';
import { formatChunkReference } from '../utils/bibleUtils.jsx';
import { buildClaudePrompt, buildPodcastPrompt } from '../utils/exportBuilders.js';
+import { chunksNeedingWork } from '../utils/ownWork.js';
import { buildScriptDraft, spokenMinutes, wordCount } from '../utils/scriptDraft.js';
const has = (s) => (s ?? '').trim().length > 0;
@@ -23,6 +24,7 @@ export default function ScriptPage() {
const iq = useIqAvailable();
const [gathering, setGathering] = useState(false);
const [presenting, setPresenting] = useState(false);
+ const [heldAction, setHeldAction] = useState(null); // an AI hand-off waiting on the own-work check
const { toast } = useToast();
const chunk = allChunks.find((c) => c.id === project?.selectedChunkId) ?? allChunks[0] ?? null;
@@ -54,6 +56,11 @@ export default function ScriptPage() {
}
};
+ // AI hand-offs only go straight through once every chunk has your own notes; otherwise ask first.
+ const needingWork = chunksNeedingWork(allChunks);
+ const guarded = (action) => () => { if (needingWork.length === 0) action(); else setHeldAction(() => action); };
+ const goStudy = (id) => { select(id); setHeldAction(null); setCurrentPage('study'); };
+
// Copies a prompt, then opens claude.ai so it can be pasted straight in.
const copyPrompt = (prompt) => {
navigator.clipboard.writeText(prompt).then(() => {
@@ -130,12 +137,12 @@ export default function ScriptPage() {
{ importDocx(e.target.files?.[0]); e.target.value = ''; }} />
-
+ {heldAction && (
+
+
Is this your study, or Claude's?
+
These chunks don't have much of your own thinking yet. Write it first, then let Claude polish it.
+
+ {needingWork.map(({ chunk: c, missing }) => (
+ -
+ {formatChunkReference(project, project.chapters.findIndex((ch) => ch.chunks.some((x) => x.id === c.id)), c, '–')}: {missing.map((m) => `${m.label} ${m.have}/${m.need}`).join(', ')}
+ goStudy(c.id)} className="text-[var(--r-accent)] hover:underline">Write it →
+
+ ))}
+
+
+ goStudy(needingWork[0].chunk.id)} className="rounded-full bg-[var(--r-text)] px-3 py-1 text-xs font-medium text-[var(--r-bg)]">Go write my notes
+ { const run = heldAction; setHeldAction(null); run(); }} className="rounded-full border border-[var(--r-text)] px-3 py-1 text-xs hover:bg-black/5">Send to Claude anyway
+ setHeldAction(null)} className="text-xs text-[var(--r-sub)] hover:underline">Cancel
+
+
+ )}