Add Insights, Observe-first, compare, handout layouts, richer search, backup nudge

- Commentary and study aids wait for 15 words of your own Observe notes.
- Insights page: this week's own work, themes across studies, words you keep
  studying, what slipped in recalls, open follow-ups.
- Recall can record what slipped; session notes (teach it back, who came,
  group notes, follow-ups); "Pray about this" from Apply.
- Compare any two passages side by side.
- Handout layouts: standard, questions only, large print, note-taking.
- Search understands "in <book>", "last week/month/year" and a year.
- Home shows the last backup age and nudges after two weeks.
- Phone: the stage bar scrolls instead of clipping Script and Export.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 12:42:11 -04:00
1 parent 2ddc4e1ff6
commit d8e522dd06
24 files changed
+540 -28

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
import { test, expect } from 'vitest';
import { weeklySummary, themeIndex, wordIndex, slippedList, followUpList } from './insights.js';
import { WEEK_MS } from './recall.js';
const NOW = 100 * WEEK_MS;
const text = (n) => Array.from({ length: n }, (_, i) => `w${i}`).join(' ');
const chunk = (id, extra = {}) => ({ id, startVerse: 1, endVerse: 5, observation: text(20), editedAt: NOW - 1000, ...extra });
const study = (chunks, id = 'p1', title = 'Romans') => ({ id, title, translation: 'BSB', chapters: [{ book: 'Romans', bookAbbrev: 'Rom', chapter: 8, chunks }] });
test('the weekly summary counts only chunks edited this week', () => {
const pasted = text(10);
const projects = [study([
chunk('a', { teachBack: 'x', recalls: [{ at: NOW - 1000, rating: 'solid' }] }),
chunk('b', { observation: `${text(10)} ${pasted}`, aiPastes: { observation: [pasted] }, editedAt: NOW - 500 }),
chunk('old', { editedAt: NOW - 3 * WEEK_MS, recalls: [{ at: NOW - 3 * WEEK_MS, rating: 'solid' }] }),
])];
const s = weeklySummary(projects, NOW);
expect(s).toMatchObject({ chunksTouched: 2, ownWords: 30, recalls: 1, teachBacks: 1 });
expect(s.ownShare).toBe(75); // 100% and 50%, averaged
expect(weeklySummary([], NOW).ownShare).toBeNull();
});
test('tags group notes from different studies into themes, most used first', () => {
const p1 = study([chunk('a', { tags: ['Grace'] }), chunk('b', { tags: ['hope'] })]);
const p2 = study([chunk('c', { tags: ['grace '] })], 'p2', 'John');
const themes = themeIndex([p1, p2]);
expect(themes.map((t) => [t.tag, t.count])).toEqual([['Grace', 2], ['hope', 1]]);
expect(themes[0].items.map((i) => i.projectTitle)).toEqual(['Romans', 'John']);
});
test('words that come up in more than one place are listed', () => {
const w = (n, g) => ({ id: n, strongNumber: n, lexeme: g, englishGloss: g });
const projects = [study([chunk('a', { greekWords: [w('G26', 'agape'), w('G4102', 'pistis')] }), chunk('b', { greekWords: [w('g26', 'agape')] })])];
const list = wordIndex(projects);
expect(list).toHaveLength(1);
expect(list[0]).toMatchObject({ strong: 'G26', count: 2 });
expect(wordIndex(projects, { minCount: 1 })).toHaveLength(2);
});
test('slipped lines and follow-ups are collected, open follow-ups first', () => {
const projects = [study([chunk('a', {
recalls: [{ at: 5, gaps: '- the serpent\nwho spoke', rating: 'partly' }, { at: 9, gaps: '', rating: 'solid' }],
followUps: [{ id: '1', text: 'done thing', done: true }, { id: '2', text: 'call Sam', done: false }],
})])];
expect(slippedList(projects)).toHaveLength(1);
expect(slippedList(projects)[0].items).toEqual(['the serpent', 'who spoke']);
expect(followUpList(projects).map((f) => f.text)).toEqual(['call Sam', 'done thing']);
});