Files
study-bible-project/src/utils/insights.js
T
nmemmertandClaude Sonnet 5.5 eccc5e509d Studying: daily five minutes, reading list, plan-day studies use your template
- 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>
2026-10-14 13:51:49 -04:00

80 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}