// 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); }