Move study editor state into StudyPage and hooks
- 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>
This commit is contained in:
1 parent
4b14b3fbe6
commit
bf02e071fa
8 files changed
+749
-771
No files matched your search
@@ -0,0 +1,129 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user