- 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>
80 lines
4.7 KiB
JavaScript
80 lines
4.7 KiB
JavaScript
// Looks across every study at once: this week's own work, tags as themes, the original-language words you
|
||
// keep returning to, what slipped in recalls, and follow-ups still open. `projects` are full study projects.
|
||
import { formatChunkReference } from './bibleUtils.jsx';
|
||
import { NOTE_FIELDS, ownShare, ownWords } from './ownWork.js';
|
||
import { WEEK_MS } from './recall.js';
|
||
|
||
const lines = (text) => String(text ?? '').split('\n').map((l) => l.replace(/^\s*(?:[-*•–]|\d{1,2}[.)])\s+/, '').trim()).filter(Boolean);
|
||
|
||
// Every chunk with where it lives.
|
||
function everyChunk(projects) {
|
||
return (projects ?? []).flatMap((project) => (project?.chapters ?? []).flatMap((chapter, chapterIndex) => (chapter.chunks ?? []).map((chunk) => ({
|
||
project, chunk, projectId: project.id, projectTitle: project.title, chunkId: chunk.id,
|
||
reference: formatChunkReference(project, chapterIndex, chunk, '–'),
|
||
}))));
|
||
}
|
||
|
||
// Chunks edited in the last 7 days: how many, your own words in them, the share that is yours, and recalls done.
|
||
export function weeklySummary(projects, now = Date.now()) {
|
||
const all = everyChunk(projects);
|
||
const recent = all.filter(({ chunk }) => now - (chunk.editedAt ?? 0) < WEEK_MS && (chunk.editedAt ?? 0) > 0);
|
||
const ownWordsTotal = recent.reduce((n, { chunk }) => n + NOTE_FIELDS.reduce((m, f) => m + ownWords(chunk, f), 0), 0);
|
||
const shares = recent.map(({ chunk }) => ownShare(chunk)).filter((s) => s !== null);
|
||
return {
|
||
chunksTouched: recent.length,
|
||
ownWords: ownWordsTotal,
|
||
ownShare: shares.length ? Math.round(shares.reduce((a, b) => a + b, 0) / shares.length) : null,
|
||
recalls: all.reduce((n, { chunk }) => n + (chunk.recalls ?? []).filter((r) => now - (r.at ?? 0) < WEEK_MS).length, 0),
|
||
fiveMinutes: all.reduce((n, { chunk }) => n + (chunk.fiveMinutes ?? []).filter((f) => now - (f.at ?? 0) < WEEK_MS).length, 0),
|
||
teachBacks: recent.filter(({ chunk }) => (chunk.teachBack ?? '').trim()).length,
|
||
};
|
||
}
|
||
|
||
const snippet = (chunk) => {
|
||
const raw = (chunk.observation || chunk.interpretation || chunk.application || '').replace(/\s+/g, ' ').trim();
|
||
return raw.length > 110 ? `${raw.slice(0, 110)}…` : raw;
|
||
};
|
||
|
||
// Tags across studies: [{ tag, count, items: [{ projectId, projectTitle, chunkId, reference, snippet }] }], most used first.
|
||
export function themeIndex(projects) {
|
||
const map = new Map();
|
||
for (const entry of everyChunk(projects)) {
|
||
for (const tag of entry.chunk.tags ?? []) {
|
||
const key = tag.trim().toLowerCase();
|
||
if (!key) continue;
|
||
if (!map.has(key)) map.set(key, { tag: tag.trim(), items: [] });
|
||
map.get(key).items.push({ projectId: entry.projectId, projectTitle: entry.projectTitle, chunkId: entry.chunkId, reference: entry.reference, snippet: snippet(entry.chunk) });
|
||
}
|
||
}
|
||
return [...map.values()].map((t) => ({ ...t, count: t.items.length })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
||
}
|
||
|
||
// Original-language words studied in more than one place (or all, with minCount 1): most often first.
|
||
export function wordIndex(projects, { minCount = 2 } = {}) {
|
||
const map = new Map();
|
||
for (const entry of everyChunk(projects)) {
|
||
for (const w of entry.chunk.greekWords ?? []) {
|
||
const strong = (w.strongNumber ?? '').toUpperCase();
|
||
const key = strong || (w.lexeme ?? w.query ?? '').toLowerCase();
|
||
if (!key) continue;
|
||
if (!map.has(key)) map.set(key, { key, strong, lexeme: w.lexeme || w.query || key, gloss: w.englishGloss || w.shortDefinition || '', transliteration: w.transliteration ?? '', places: [] });
|
||
map.get(key).places.push({ projectId: entry.projectId, projectTitle: entry.projectTitle, chunkId: entry.chunkId, reference: entry.reference });
|
||
}
|
||
}
|
||
return [...map.values()].map((w) => ({ ...w, count: w.places.length })).filter((w) => w.count >= minCount).sort((a, b) => b.count - a.count || a.lexeme.localeCompare(b.lexeme));
|
||
}
|
||
|
||
// What you said slipped in your recalls, newest first: [{ at, reference, projectId, chunkId, items: [line] }].
|
||
export function slippedList(projects) {
|
||
return everyChunk(projects).flatMap((e) => (e.chunk.recalls ?? [])
|
||
.map((r) => ({ at: r.at ?? 0, reference: e.reference, projectId: e.projectId, projectTitle: e.projectTitle, chunkId: e.chunkId, items: lines(r.gaps) }))
|
||
.filter((r) => r.items.length)).sort((a, b) => b.at - a.at);
|
||
}
|
||
|
||
// Follow-ups across studies: open ones first (then done), newest chunk first.
|
||
export function followUpList(projects) {
|
||
return everyChunk(projects).flatMap((e) => (e.chunk.followUps ?? []).map((f) => ({
|
||
id: f.id, text: f.text, done: !!f.done, reference: e.reference, projectId: e.projectId, projectTitle: e.projectTitle, chunkId: e.chunkId, editedAt: e.chunk.editedAt ?? 0,
|
||
}))).sort((a, b) => Number(a.done) - Number(b.done) || b.editedAt - a.editedAt);
|
||
}
|