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

+26 -2
View File
@@ -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 (
<section aria-label="Study notes" className="space-y-2">
{share !== null && (
<p className="px-1 text-xs text-[var(--r-sub)]" title="Long pastes into a note box count as pasted, not your own">
{share === 100 ? 'All of these notes are your own words.' : `${share}% of these notes are your own words.`}
</p>
)}
{SECTIONS.map(({ field, label, hint, placeholder }) => {
const isOpen = open === field;
const n = words(chunk[field]);
const pasted = pastedWords(chunk, field);
return (
<div key={field} className={`overflow-hidden rounded-2xl border ${isOpen ? 'border-[var(--r-accent)]' : 'border-[var(--r-line)]'}`}>
<button type="button" onClick={() => setOpen(isOpen ? null : field)} aria-expanded={isOpen}
@@ -50,12 +63,23 @@ export default function NotesPane({ chunk, updateChunk, allTags, templateId, sav
<span className="text-sm font-medium">{label}</span>
<span className="text-xs text-[var(--r-sub)]">{hint}</span>
</span>
<span className="text-xs text-[var(--r-sub)]">{n ? `${n} word${n === 1 ? '' : 's'}` : ''}</span>
<span className="flex items-center gap-2 text-xs text-[var(--r-sub)]">
{n > 0 && (pasted > 0
? <span className="rounded-full bg-[var(--r-hl)] px-2 py-0.5 text-[var(--r-text)]">{ownWords(chunk, field) === 0 ? 'Pasted' : `${pasted} pasted`}</span>
: <span className="rounded-full border border-[var(--r-line)] px-2 py-0.5">Mine</span>)}
{n ? `${n} word${n === 1 ? '' : 's'}` : ''}
</span>
</button>
{isOpen && (
<div className="px-4 pb-4">
<textarea aria-label={label} value={chunk[field] ?? ''} rows={7} placeholder={templatePrompt(templateId, field) ?? placeholder}
onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
onPaste={keepPaste(field)} onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
{pasted > 0 && (
<p className="mt-1.5 text-xs text-[var(--r-sub)]">
{pasted} word{pasted === 1 ? '' : 's'} here were pasted, so they don't count as your own work.{' '}
<button type="button" onClick={() => updateChunk(chunk.id, { aiPastes: clearPastes(chunk, field) })} className="text-[var(--r-accent)] hover:underline">That’s mine</button>
</p>
)}
<div className="mt-1.5"><DictateButton label={`Dictate ${label.toLowerCase()}`} onText={(said) => updateChunk(chunk.id, { [field]: appendSpoken(chunk[field], said) })} /></div>
</div>
)}