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:
1 parent
3a28ffa51a
commit
648c3206df
14 files changed
+232
-27
No files matched your search
@@ -5,8 +5,9 @@ import { useApp } from '../../context/AppContext.js';
|
||||
import { loadProjectById } from '../../utils/projectStorage.js';
|
||||
import { formatChunkReference, getChunkVerseEntries } from '../../utils/bibleUtils.jsx';
|
||||
import { READER_TRANSLATIONS, fetchChapter, translationName } from '../../utils/bibleText.js';
|
||||
import { alignVerses, translationSpans } from '../../utils/compareTranslations.js';
|
||||
import { ownShare, pastedWords, withPaste } from '../../utils/ownWork.js';
|
||||
import { alignVerses, differencesFor, markRanges, translationSpans } from '../../utils/compareTranslations.js';
|
||||
import { ownShare, pastedWords } from '../../utils/ownWork.js';
|
||||
import { usePasteCheck } from './PastePrompt.jsx';
|
||||
|
||||
const FIELDS = [['observation', 'Observe'], ['interpretation', 'Interpret'], ['application', 'Apply']];
|
||||
|
||||
@@ -48,6 +49,7 @@ const OIA = [
|
||||
// They are the same notes as on the Study page, and long pastes are tagged the same way.
|
||||
function OiaBoxes({ chunk, updateChunk }) {
|
||||
const share = ownShare(chunk);
|
||||
const pasteCheck = usePasteCheck(chunk, updateChunk);
|
||||
return (
|
||||
<section aria-label="Your notes" className="border-t border-[var(--r-line)] px-4 pb-6 pt-4">
|
||||
<p className="mb-3 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Your notes{share !== null ? ` · ${share}% your own words` : ''}</p>
|
||||
@@ -58,9 +60,10 @@ function OiaBoxes({ chunk, updateChunk }) {
|
||||
<label key={field} className="flex flex-col text-xs text-[var(--r-sub)]">
|
||||
<span className="mb-1 flex items-baseline justify-between"><span><span className="text-sm font-medium text-[var(--r-text)]">{label}</span> {hint}</span>{pasted > 0 && <span className="rounded-full bg-[var(--r-hl)] px-2 py-0.5 text-[var(--r-text)]">{pasted} pasted</span>}</span>
|
||||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={9}
|
||||
onPaste={(e) => { const next = withPaste(chunk, field, e.clipboardData?.getData('text')); if (next) updateChunk(chunk.id, { aiPastes: next }); }}
|
||||
onPaste={pasteCheck.onPaste(field)}
|
||||
onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })}
|
||||
className="min-h-[10rem] w-full resize-y rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2.5 font-serif text-[15px] leading-7 text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]" />
|
||||
{pasteCheck.prompt(field)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
@@ -71,7 +74,9 @@ function OiaBoxes({ chunk, updateChunk }) {
|
||||
|
||||
// One chunk's verses in several translations, a row per verse. Free translations load straight away; licensed ones
|
||||
// come through the server and may be unavailable (no key set), which shows as a note instead of text.
|
||||
function TranslationsView({ project, chunkEntry }) {
|
||||
function TranslationsView({ project, chunkEntry, updateChunk }) {
|
||||
const [highlight, setHighlight] = useState(() => { try { return localStorage.getItem('compare-highlight') !== 'off'; } catch { return true; } });
|
||||
const setHl = (on) => { setHighlight(on); try { localStorage.setItem('compare-highlight', on ? 'on' : 'off'); } catch { /* storage unavailable */ } };
|
||||
const spans = useMemo(() => (chunkEntry ? translationSpans(project, chunkEntry.chapterIndex, chunkEntry.chunk) : []), [project, chunkEntry]);
|
||||
const spanKey = spans.map((s) => `${s.book}${s.chapter}:${s.from}-${s.to}`).join(',');
|
||||
const [picked, setPicked] = useState(() => {
|
||||
@@ -116,6 +121,9 @@ function TranslationsView({ project, chunkEntry }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{shown.length > 1 && (
|
||||
<label className="flex items-center gap-2 text-xs text-[var(--r-sub)]"><input type="checkbox" checked={highlight} onChange={(e) => setHl(e.target.checked)} className="accent-[var(--r-accent)]" /> Highlight the words that differ from {shown[0]}</label>
|
||||
)}
|
||||
{picked.map((id) => loaded[id]?.status === 'error' && (
|
||||
<p key={id} role="status" className="text-xs text-[var(--r-sub)]">{id} isn’t available right now: {loaded[id].error}</p>
|
||||
))}
|
||||
@@ -126,17 +134,27 @@ function TranslationsView({ project, chunkEntry }) {
|
||||
<div className="hidden gap-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)] md:grid" style={cols}>
|
||||
{shown.map((id) => <span key={id}>{translationName(id)}</span>)}
|
||||
</div>
|
||||
{rows.map((row) => (
|
||||
<div key={row.key} className="grid gap-x-4 gap-y-1 border-t border-[var(--r-line)] pt-3 md:[grid-template-columns:var(--cols)]" style={{ '--cols': cols.gridTemplateColumns }}>
|
||||
{shown.map((id) => (
|
||||
<p key={id} className="font-serif text-[14px] leading-relaxed">
|
||||
<sup className="mr-1 font-sans text-[var(--r-accent)]">{row.label}</sup>
|
||||
<span className="mr-1 font-sans text-[10px] uppercase tracking-wide text-[var(--r-sub)] md:hidden">{id}</span>
|
||||
{row.texts[id] ?? <span className="text-[var(--r-sub)]">—</span>}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{rows.map((row) => {
|
||||
const diffs = highlight && shown.length > 1 ? differencesFor(row.texts, shown) : {};
|
||||
const note = chunkEntry.chunk.verseNotes?.[row.noteKey] ?? '';
|
||||
return (
|
||||
<div key={row.key} className="border-t border-[var(--r-line)] pt-3">
|
||||
<div className="grid gap-x-4 gap-y-1 md:[grid-template-columns:var(--cols)]" style={{ '--cols': cols.gridTemplateColumns }}>
|
||||
{shown.map((id) => (
|
||||
<p key={id} className="font-serif text-[14px] leading-relaxed">
|
||||
<sup className="mr-1 font-sans text-[var(--r-accent)]">{row.label}</sup>
|
||||
<span className="mr-1 font-sans text-[10px] uppercase tracking-wide text-[var(--r-sub)] md:hidden">{id}</span>
|
||||
{row.texts[id] == null ? <span className="text-[var(--r-sub)]">—</span>
|
||||
: markRanges(row.texts[id], diffs[id]).map((part, i) => (part.mark ? <mark key={i} className="rounded bg-[var(--r-hl)] px-0.5 text-inherit">{part.text}</mark> : <span key={i}>{part.text}</span>))}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<input aria-label={`Note on verse ${row.label}`} value={note} placeholder={`Note on verse ${row.label}`}
|
||||
onChange={(e) => updateChunk(chunkEntry.chunk.id, { verseNotes: { ...(chunkEntry.chunk.verseNotes ?? {}), [row.noteKey]: e.target.value } })}
|
||||
className="mt-1.5 w-full rounded-lg border border-transparent bg-transparent px-2 py-1 text-xs text-[var(--r-text)] placeholder:text-[var(--r-sub)] placeholder:opacity-60 hover:border-[var(--r-line)] focus:border-[var(--r-line)] focus:bg-[var(--r-bg)] focus:outline-none" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="space-y-1 border-t border-[var(--r-line)] pt-3 text-[11px] leading-snug text-[var(--r-sub)]">
|
||||
{shown.map((id) => {
|
||||
const t = READER_TRANSLATIONS.find((x) => x.id === id);
|
||||
@@ -187,7 +205,7 @@ export default function CompareSheet({ currentChunkId, onClose }) {
|
||||
) : (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<h3 className="px-4 pt-4 font-medium">{mine?.label}</h3>
|
||||
<TranslationsView project={project} chunkEntry={mine} />
|
||||
<TranslationsView project={project} chunkEntry={mine} updateChunk={updateChunk} />
|
||||
{mine && <OiaBoxes chunk={mine.chunk} updateChunk={updateChunk} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user