Add own-work tracking, Claude hand-off check, and weekly recall

- Long pastes into Observe/Interpret/Apply are tagged as pasted; notes show
  Mine / N pasted and an own-words share. Pastes follow text through merges.
- The "-> Claude" buttons on the Script stage ask first when a chunk lacks
  enough of the user's own (non-pasted) words.
- Weekly recall card on Home: write a passage from memory, compare with notes,
  self-rate; recalls are saved on the chunk and synced.
- Recall history on Home with an eight-week strip and the full list.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 12:25:49 -04:00
1 parent edc5a65d2b
commit 0c8a632cff
11 files changed
+429 -5

No files matched your search

+4
View File
@@ -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() {
>
<main className="mx-auto grid max-w-6xl gap-6 px-4 py-6 lg:grid-cols-[minmax(0,1fr)_21rem]">
<section className="min-w-0">
<WeeklyRecall />
<RecallHistory />
<IqDailyCard />
{autoRestoredCount !== null && (
<div className={notice}>Synced {autoRestoredCount} project{autoRestoredCount > 1 ? 's' : ''} from another device.</div>
+29 -3
View File
@@ -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() {
<input type="file" accept=".docx" className="hidden" onChange={(e) => { importDocx(e.target.files?.[0]); e.target.value = ''; }} />
</label>
<span className="mx-1 h-4 w-px bg-[var(--r-line)]" />
<button type="button" onClick={() => copyPrompt(buildClaudePrompt(project))} className={btn}
<button type="button" onClick={guarded(() => copyPrompt(buildClaudePrompt(project)))} className={btn}
title="Copies your study notes as a Claude prompt, then opens claude.ai">Study guide → Claude ↗</button>
<button type="button" onClick={() => copyPrompt(buildPodcastPrompt(project, authUser?.podcastName))} className={btn}
<button type="button" onClick={guarded(() => copyPrompt(buildPodcastPrompt(project, authUser?.podcastName)))} className={btn}
title="Copies a podcast script prompt for Claude — set your show name in Account Settings first">Podcast → Claude ↗</button>
{iq && (
<button type="button" disabled={gathering} onClick={copyPodcastWithContext} className={btn}
<button type="button" disabled={gathering} onClick={guarded(copyPodcastWithContext)} className={btn}
title={`Adds IQ Bible commentary, cross-references and word glosses for up to ${chunkRefs(project).refs.length} passages (each uses a few API requests)`}>
{gathering ? 'Gathering…' : 'Podcast + background → Claude ↗'}
</button>
@@ -145,6 +152,25 @@ export default function ScriptPage() {
<DictateButton label="Dictate script" onText={(said) => set({ finalScript: appendSpoken(script, said) })} />
{notice && <span role="status" className="text-xs text-[var(--r-accent)]">{notice}</span>}
</div>
{heldAction && (
<div role="alert" className="rounded-xl border border-[var(--r-accent)] bg-[var(--r-hl)] p-3 text-sm">
<p className="font-medium">Is this your study, or Claude's?</p>
<p className="mt-0.5 text-xs text-[var(--r-sub)]">These chunks don't have much of your own thinking yet. Write it first, then let Claude polish it.</p>
<ul className="mt-2 space-y-1 text-xs">
{needingWork.map(({ chunk: c, missing }) => (
<li key={c.id} className="flex items-center justify-between gap-3">
<span>{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(', ')}</span>
<button type="button" onClick={() => goStudy(c.id)} className="text-[var(--r-accent)] hover:underline">Write it →</button>
</li>
))}
</ul>
<div className="mt-2.5 flex gap-2">
<button type="button" onClick={() => 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</button>
<button type="button" onClick={() => { 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</button>
<button type="button" onClick={() => setHeldAction(null)} className="text-xs text-[var(--r-sub)] hover:underline">Cancel</button>
</div>
</div>
)}
<textarea aria-label="Final script" value={script} onChange={(e) => set({ finalScript: e.target.value })}
placeholder="Write or paste the final script here. “Build outline from notes” starts you from your study notes."
className="min-h-[24rem] flex-1 resize-y rounded-2xl border border-[var(--r-line)] bg-[var(--r-bg)] px-5 py-4 font-serif text-[16px] leading-8 text-[var(--r-text)] placeholder:font-sans placeholder:text-sm placeholder:text-[var(--r-sub)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]" />