- StudyPage owns layout/tabs/collapsed sections, tags, DOCX script import, chunk navigation, keyboard shortcuts, and the selected-chunk values derived from the project. - useStudyResources: commentary + interlinear for the selected chunk's chapter. These now load only on the study page (they previously also fetched while on Setup). - useCrossRefs: typed refs, dataset suggestions, verse text. - useWordStudy: word entries, Strong's lookups, suggestion picker. - Fix: study-page messages (word suggestions, DOCX import, lookups) set a status that only SetupPage displayed, so they were never seen. They now show as a toast on the study page. - copyVerse moves into ReaderPage. - Tests: toast, arrow-key navigation, cross-refs, tags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
4.6 KiB
JavaScript
130 lines
4.6 KiB
JavaScript
// Cross-references for the selected chunk: typed entry, suggestions from the
|
|
// Open Cross Ref dataset, and loading a reference's verse text.
|
|
import { useEffect, useState } from 'react';
|
|
import { parseBibleChapter } from '../utils/bibleUtils.jsx';
|
|
import { parseCrossRefString } from '../utils/importParsers.js';
|
|
import { formatCrossRef } from '../utils/wordStudy.js';
|
|
|
|
const crossRefDatasetCache = {}; // "BOOK/chapter" -> Open Cross Ref chapter data
|
|
const verseChapterCache = {}; // "translation/BOOK/chapter" -> parsed verses
|
|
|
|
// notify(message, ms) shows a short status message on the study page.
|
|
export function useCrossRefs({ project, allChunks, selectedChunk, updateChunk, notify }) {
|
|
const [crossRefInput, setCrossRefInput] = useState('');
|
|
const [suggestingCrossRefs, setSuggestingCrossRefs] = useState(false);
|
|
const [crossRefSuggestions, setCrossRefSuggestions] = useState([]);
|
|
|
|
useEffect(() => {
|
|
setCrossRefSuggestions([]);
|
|
}, [selectedChunk?.id]);
|
|
|
|
useEffect(() => {
|
|
setCrossRefInput('');
|
|
}, [project?.selectedChunkId]);
|
|
|
|
const addCrossRef = (chunkId) => {
|
|
const ref = crossRefInput.trim();
|
|
if (!ref) return;
|
|
updateChunk(chunkId, {
|
|
crossReferences: [...(selectedChunk?.crossReferences ?? []), ref],
|
|
});
|
|
setCrossRefInput('');
|
|
};
|
|
|
|
const removeCrossRef = (chunkId, ref) => {
|
|
updateChunk(chunkId, {
|
|
crossReferences: (selectedChunk?.crossReferences ?? []).filter((r) => r !== ref),
|
|
});
|
|
};
|
|
|
|
|
|
const suggestCrossRefsForChunk = async (chunkId) => {
|
|
const chunk = allChunks.find((c) => c.id === chunkId);
|
|
const chapterIndex = project?.chapters.findIndex((ch) =>
|
|
ch.chunks.some((c) => c.id === chunkId)
|
|
) ?? -1;
|
|
const chapter = chapterIndex >= 0 ? project.chapters[chapterIndex] : null;
|
|
if (!chunk || !chapter) return;
|
|
|
|
setSuggestingCrossRefs(true);
|
|
setCrossRefSuggestions([]);
|
|
try {
|
|
const cacheKey = `${chapter.bookAbbrev}/${chapter.chapter}`;
|
|
let chapterData = crossRefDatasetCache[cacheKey];
|
|
if (!chapterData) {
|
|
const res = await fetch(`https://bible.helloao.org/api/d/open-cross-ref/${chapter.bookAbbrev}/${chapter.chapter}.json`);
|
|
if (!res.ok) throw new Error('No cross-reference data found for this chapter.');
|
|
chapterData = await res.json();
|
|
crossRefDatasetCache[cacheKey] = chapterData;
|
|
}
|
|
|
|
const verseEntries = (chapterData.chapter?.content ?? []).filter(
|
|
(item) => item.verse >= chunk.startVerse
|
|
&& item.verse <= chunk.endVerse,
|
|
);
|
|
|
|
const existing = new Set(chunk.crossReferences ?? []);
|
|
const seen = new Set();
|
|
const refs = [];
|
|
for (const v of verseEntries) {
|
|
for (const ref of v.references ?? []) {
|
|
const formatted = formatCrossRef(ref);
|
|
if (existing.has(formatted) || seen.has(formatted)) continue;
|
|
seen.add(formatted);
|
|
refs.push({ formatted, score: ref.score ?? 0 });
|
|
}
|
|
}
|
|
|
|
refs.sort((a, b) => b.score - a.score);
|
|
if (refs.length === 0) {
|
|
notify('No new cross-reference suggestions found for this passage.', 2500);
|
|
return;
|
|
}
|
|
setCrossRefSuggestions(refs.slice(0, 12).map((r) => r.formatted));
|
|
} catch (err) {
|
|
notify(`Cross-reference suggest failed: ${err.message}`, 3000);
|
|
} finally {
|
|
setSuggestingCrossRefs(false);
|
|
}
|
|
};
|
|
|
|
|
|
const loadVerseText = async (refString) => {
|
|
const parsed = parseCrossRefString(refString);
|
|
if (!parsed) throw new Error('Could not parse reference.');
|
|
const translation = project?.translation ?? 'BSB';
|
|
const cacheKey = `${translation}/${parsed.bookAbbrev}/${parsed.chapter}`;
|
|
let verses = verseChapterCache[cacheKey];
|
|
if (!verses) {
|
|
const res = await fetch(`https://bible.helloao.org/api/${translation}/${parsed.bookAbbrev}/${parsed.chapter}.json`);
|
|
if (!res.ok) throw new Error('Could not load verse text.');
|
|
const data = await res.json();
|
|
verses = parseBibleChapter(data);
|
|
verseChapterCache[cacheKey] = verses;
|
|
}
|
|
const text = verses
|
|
.filter((v) => v.number >= parsed.verse && v.number <= parsed.endVerse)
|
|
.map((v) => v.text)
|
|
.join(' ');
|
|
return text;
|
|
};
|
|
|
|
const addSuggestedCrossRef = (chunkId, ref) => {
|
|
updateChunk(chunkId, {
|
|
crossReferences: [...(selectedChunk?.crossReferences ?? []), ref],
|
|
});
|
|
setCrossRefSuggestions((cur) => cur.filter((r) => r !== ref));
|
|
};
|
|
|
|
return {
|
|
crossRefInput, setCrossRefInput,
|
|
suggestingCrossRefs,
|
|
crossRefSuggestions,
|
|
addCrossRef,
|
|
removeCrossRef,
|
|
suggestCrossRefsForChunk,
|
|
addSuggestedCrossRef,
|
|
loadVerseText,
|
|
};
|
|
}
|