Highlight translation differences, first-draft snapshot, verse notes, paste check

- Compare translations can highlight the words that differ from the first
  translation (toggle remembered).
- A copy of your notes is kept the first time commentary is shown; "First draft,
  before commentary" under More shows what changed since. Saving it doesn't
  count as editing the chunk.
- Notes on single verses: a pencil next to each verse in Study, and a note line
  under each verse in Compare. They merge per verse across devices.
- Pasting 15+ words into a note asks "Is this from AI?"; "I wrote it" counts the
  words as yours again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 12:56:57 -04:00
1 parent 3a28ffa51a
commit 648c3206df
14 files changed
+232 -27

No files matched your search

+27 -1
View File
@@ -1,5 +1,6 @@
// Comparing translations of one chunk: which chapter(s) and verses to fetch, and the verses lined up by number.
import { getChunkVerseEntries } from './bibleUtils.jsx';
import { diffVerses } from './textDiff.js';
// -> [{ bookAbbrev, book, chapter, from, to }]: one entry per chapter the chunk covers (two when it spills over).
export function translationSpans(project, chapterIndex, chunk) {
@@ -23,8 +24,33 @@ export function alignVerses(spans, byTranslation) {
for (const [id, chapters] of Object.entries(byTranslation)) {
texts[id] = chapters?.[`${span.book}|${span.chapter}`]?.find((v) => v.number === n)?.text ?? null;
}
rows.push({ key: `${span.book}${span.chapter}:${n}`, label: spans.length > 1 ? `${span.chapter}:${n}` : String(n), texts });
rows.push({ key: `${span.book}${span.chapter}:${n}`, noteKey: `${span.chapter}:${n}`, label: spans.length > 1 ? `${span.chapter}:${n}` : String(n), texts });
}
}
return rows;
}
// For one verse in several translations: which words in each text the others lack, measured against the first
// translation (and the first against the second). -> { BSB: [{ start, end }], NET: [...] }
export function differencesFor(texts, ids) {
const out = Object.fromEntries(ids.map((id) => [id, []]));
const present = ids.filter((id) => texts[id]);
if (present.length < 2) return out;
const [base, ...rest] = present;
out[base] = diffVerses(texts[base], texts[rest[0]]).a;
for (const id of rest) out[id] = diffVerses(texts[base], texts[id]).b;
return out;
}
// Cuts a text at the ranges -> [{ text, mark }] so a view can wrap the marked parts.
export function markRanges(text, ranges) {
const parts = [];
let at = 0;
for (const r of [...(ranges ?? [])].sort((a, b) => a.start - b.start)) {
if (r.start > at) parts.push({ text: text.slice(at, r.start), mark: false });
parts.push({ text: text.slice(r.start, r.end), mark: true });
at = r.end;
}
if (at < text.length) parts.push({ text: text.slice(at), mark: false });
return parts;
}