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:
nmemmertandClaude Opus 5.5 committed 2026-09-22 14:59:22 -04:00
1 parent 4b14b3fbe6
commit bf02e071fa
8 files changed
+749 -771

No files matched your search

+1 -670
View File
@@ -6,28 +6,12 @@ import { useBibleReader } from './hooks/useBibleReader.js';
import { useProjectLibrary } from './hooks/useProjectLibrary.js';
import { useReadingPlan } from './hooks/useReadingPlan.js';
import { useSharedView } from './hooks/useSharedView.js';
import {
loadNtGloss,
loadNtConcordance,
loadHebrewDict,
loadGreekDict,
GREEK_FUNCTION_WORDS,
shortHebrewGloss,
ENGLISH_STOP_WORDS,
buildGreekDefinitionHtml,
fetchBollsDefinition,
formatCrossRef,
} from './utils/wordStudy.js';
import HomePage from './pages/HomePage.jsx';
import {
bookOptions,
NT_BOOK_NUMBER,
makeId,
getChunkVerseEntries,
chunkSpansNextChapter,
buildExportHtml,
extractPartOfSpeech,
parseBibleChapter,
loadProjectIndex,
switchStorageUser,
@@ -36,9 +20,6 @@ import {
saveProjectToStorage,
migrateOldStorageKeys,
} from './utils/projectStorage.js';
import {
parseCrossRefString,
} from './utils/importParsers.js';
import {
saveRemoteProject,
@@ -78,7 +59,6 @@ const App = () => {
const [projectIndex, setProjectIndex] = useState([]);
const [activeChapterIndex, setActiveChapterIndex] = useState(0);
const [showAddChapterForm, setShowAddChapterForm] = useState(false);
const [crossRefInput, setCrossRefInput] = useState('');
const [saveStatus, setSaveStatus] = useState('');
const [rangeStart, setRangeStart] = useState(null);
const [rangeEnd, setRangeEnd] = useState(null);
@@ -92,11 +72,6 @@ const App = () => {
const [typedChunkBulk, setTypedChunkBulk] = useState('');
const saveTimerRef = useRef(null);
const [syncStatus, setSyncStatus] = useState(''); // '' | 'syncing' | 'synced' | 'error'
const [suggestingGreekForChunkId, setSuggestingGreekForChunkId] = useState(null);
const [suggestingHebrewForChunkId, setSuggestingHebrewForChunkId] = useState(null);
// suggestModal: null | { chunkId, words: [{ strongKey, lexeme, translit, def }] }
const [suggestModal, setSuggestModal] = useState(null);
const [suggestSelection, setSuggestSelection] = useState(new Set());
const reader = useBibleReader(currentPage);
const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter);
const readingPlan = useReadingPlan();
@@ -122,64 +97,11 @@ const App = () => {
setCurrentPage,
setActiveChapterIndex,
});
const [studyLayout, setStudyLayout] = useState(() => {
const saved = localStorage.getItem('studyLayout');
return saved === 'stacked' ? 'stacked' : 'split'; // 'annotate' (Draw) was removed
}); // 'stacked' | 'split'
const [activeStudyTab, setActiveStudyTab] = useState(
() => localStorage.getItem('activeStudyTab') || 'notes',
);
const [mobileStudyTab, setMobileStudyTab] = useState('scripture');
useEffect(() => {
localStorage.setItem('studyLayout', studyLayout);
}, [studyLayout]);
useEffect(() => {
localStorage.setItem('activeStudyTab', activeStudyTab);
}, [activeStudyTab]);
const [verseSearch, setVerseSearch] = useState('');
const [collapsedSections, setCollapsedSections] = useState({});
const [commentarySource, setCommentarySource] = useState('matthew-henry');
const [commentaryData, setCommentaryData] = useState(null); // { content: [...] } for current chapter
const [commentaryLoading, setCommentaryLoading] = useState(false);
const [commentaryError, setCommentaryError] = useState('');
const _commentaryCacheRef = useRef({});
const [interlinearData, setInterlinearData] = useState(null);
const [interlinearLoading, setInterlinearLoading] = useState(false);
const [interlinearError, setInterlinearError] = useState('');
const _interlinearCacheRef = useRef({});
const loadInterlinearChapter = async (bookAbbrev, chapterNumber) => {
if (_interlinearCacheRef.current[bookAbbrev]) return _interlinearCacheRef.current[bookAbbrev];
const res = await fetch(`/interlinear/${bookAbbrev}.json`);
if (!res.ok) throw new Error('No interlinear data for this book.');
const data = await res.json();
_interlinearCacheRef.current[bookAbbrev] = data;
return data;
};
const loadCommentaryChapter = async (commentaryId, bookAbbrev, chapterNumber) => {
const cacheKey = `${commentaryId}/${bookAbbrev}/${chapterNumber}`;
if (_commentaryCacheRef.current[cacheKey]) return _commentaryCacheRef.current[cacheKey];
const res = await fetch(`https://bible.helloao.org/api/c/${commentaryId}/${bookAbbrev}/${chapterNumber}.json`);
const contentType = res.headers.get('content-type') || '';
if (!res.ok || !contentType.includes('json')) throw new Error(`No ${commentaryId} commentary for this chapter.`);
const data = await res.json();
_commentaryCacheRef.current[cacheKey] = data;
return data;
};
const openBibleReader = () => {
setCurrentPage('reader');
};
const copyVerse = (bookName, chapterNum, verseNum, text) => {
const citation = `${bookName} ${chapterNum}:${verseNum} BSB — ${text}`;
navigator.clipboard?.writeText(citation);
};
// ---------------------------------------------------------------------------
// Startup: migrate old keys and load index
// ---------------------------------------------------------------------------
@@ -197,88 +119,8 @@ const App = () => {
}
}, [setup.book, setup.chapter, titleEdited]);
// Reset cross-ref input when selected chunk changes
const allChunks = project ? project.chapters.flatMap((ch) => ch.chunks) : [];
const activeChapter = project?.chapters[activeChapterIndex] ?? null;
const selectedChunk = allChunks.find((c) => c.id === project?.selectedChunkId) ?? allChunks[0] ?? null;
const selectedChunkChapterIndex = selectedChunk
? project.chapters.findIndex((ch) => ch.chunks.some((c) => c.id === selectedChunk.id))
: -1;
useEffect(() => {
if (collapsedSections.commentary) return;
if (selectedChunkChapterIndex < 0) return;
const chapter = project.chapters[selectedChunkChapterIndex];
setCommentaryLoading(true);
setCommentaryError('');
loadCommentaryChapter(commentarySource, chapter.bookAbbrev, chapter.chapter)
.then((data) => setCommentaryData(data))
.catch((err) => {
setCommentaryData(null);
setCommentaryError(err.message);
})
.finally(() => setCommentaryLoading(false));
}, [commentarySource, selectedChunk?.id, selectedChunkChapterIndex, collapsedSections.commentary]);
useEffect(() => {
if (collapsedSections.interlinear) return;
if (selectedChunkChapterIndex < 0) return;
const chapter = project.chapters[selectedChunkChapterIndex];
setInterlinearLoading(true);
setInterlinearError('');
loadInterlinearChapter(chapter.bookAbbrev, chapter.chapter)
.then((data) => setInterlinearData(data?.[String(chapter.chapter)] ?? null))
.catch((err) => {
setInterlinearData(null);
setInterlinearError(err.message);
})
.finally(() => setInterlinearLoading(false));
}, [selectedChunk?.id, selectedChunkChapterIndex, collapsedSections.interlinear]);
useEffect(() => {
setCrossRefSuggestions([]);
setTagInput('');
}, [selectedChunk?.id]);
const selectedChunkChapter = selectedChunkChapterIndex >= 0
? project.chapters[selectedChunkChapterIndex]
: null;
const selectedChunkVerses = selectedChunk
? getChunkVerseEntries(project, selectedChunkChapterIndex, selectedChunk)
: [];
const selectedChunkGlobalIndex = allChunks.findIndex((c) => c.id === project?.selectedChunkId);
useEffect(() => {
setCrossRefInput('');
}, [project?.selectedChunkId]);
// Keyboard shortcuts: arrow keys navigate chunks on the study page, Esc closes modals
useEffect(() => {
const handleKeyDown = (event) => {
const tag = event.target?.tagName;
const isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable;
if (event.key === 'Escape') {
if (suggestModal) {
setSuggestModal(null);
setSuggestSelection(new Set());
}
return;
}
if (currentPage !== 'study' || isEditable || suggestModal) return;
if (event.key === 'ArrowLeft') {
event.preventDefault();
goToPreviousChunk();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
goToNextChunk();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [currentPage, suggestModal, selectedChunkGlobalIndex, allChunks]);
// Autosave
useEffect(() => {
if (!project) return;
@@ -406,26 +248,6 @@ const App = () => {
setCurrentPage('study');
};
// ---------------------------------------------------------------------------
// Chunk navigation
// ---------------------------------------------------------------------------
const goToPreviousChunk = () => {
if (selectedChunkGlobalIndex <= 0) return;
updateProject((current) => ({
...current,
selectedChunkId: allChunks[selectedChunkGlobalIndex - 1].id,
}));
};
const goToNextChunk = () => {
if (selectedChunkGlobalIndex < 0 || selectedChunkGlobalIndex >= allChunks.length - 1) return;
updateProject((current) => ({
...current,
selectedChunkId: allChunks[selectedChunkGlobalIndex + 1].id,
}));
};
// ---------------------------------------------------------------------------
// Chunk CRUD (operates on activeChapter in setup view, on selectedChunkChapter in study)
// ---------------------------------------------------------------------------
@@ -777,455 +599,6 @@ const App = () => {
});
};
// ---------------------------------------------------------------------------
// Cross-references
// ---------------------------------------------------------------------------
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 [tagInput, setTagInput] = useState('');
const addTag = (chunkId) => {
const tag = tagInput.trim();
if (!tag) return;
const existing = selectedChunk?.tags ?? [];
if (existing.some((t) => t.toLowerCase() === tag.toLowerCase())) {
setTagInput('');
return;
}
updateChunk(chunkId, { tags: [...existing, tag] });
setTagInput('');
};
const removeTag = (chunkId, tag) => {
updateChunk(chunkId, {
tags: (selectedChunk?.tags ?? []).filter((t) => t !== tag),
});
};
const importFinalScriptDocx = async (chunkId, file) => {
if (!file) return;
try {
const arrayBuffer = await file.arrayBuffer();
const { default: mammoth } = await import('mammoth');
const { value } = await mammoth.extractRawText({ arrayBuffer });
updateChunk(chunkId, { finalScript: value.trim() });
setStatusMessage('Imported script from DOCX.');
window.setTimeout(() => setStatusMessage(''), 1800);
} catch (err) {
setStatusMessage('Could not read that DOCX file.');
window.setTimeout(() => setStatusMessage(''), 1800);
}
};
const [suggestingCrossRefs, setSuggestingCrossRefs] = useState(false);
const [crossRefSuggestions, setCrossRefSuggestions] = useState([]);
const _crossRefDatasetCacheRef = useRef({});
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 = _crossRefDatasetCacheRef.current[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();
_crossRefDatasetCacheRef.current[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) {
setStatusMessage('No new cross-reference suggestions found for this passage.');
window.setTimeout(() => setStatusMessage(''), 2500);
return;
}
setCrossRefSuggestions(refs.slice(0, 12).map((r) => r.formatted));
} catch (err) {
setStatusMessage(`Cross-reference suggest failed: ${err.message}`);
window.setTimeout(() => setStatusMessage(''), 3000);
} finally {
setSuggestingCrossRefs(false);
}
};
const _verseChapterCacheRef = useRef({});
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 = _verseChapterCacheRef.current[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);
_verseChapterCacheRef.current[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));
};
// ---------------------------------------------------------------------------
// Greek word helpers
// ---------------------------------------------------------------------------
const updateChunkWord = (chunkId, wordId, patch) => {
updateProject((current) => ({
...current,
chapters: current.chapters.map((ch) => ({
...ch,
chunks: ch.chunks.map((chunk) => {
if (chunk.id !== chunkId) return chunk;
return {
...chunk,
greekWords: chunk.greekWords.map((word) =>
word.id === wordId ? { ...word, ...patch } : word,
),
};
}),
})),
}));
};
const addGreekWord = (chunkId) => {
const newWord = {
id: makeId(),
query: '',
strongNumber: '',
englishGloss: '',
lexeme: '',
transliteration: '',
partOfSpeech: '',
shortDefinition: '',
definitionHtml: '',
loading: false,
};
updateProject((current) => ({
...current,
chapters: current.chapters.map((ch) => ({
...ch,
chunks: ch.chunks.map((chunk) =>
chunk.id === chunkId
? { ...chunk, greekWords: [...chunk.greekWords, newWord] }
: chunk,
),
})),
}));
};
const removeGreekWord = (chunkId, wordId) => {
updateProject((current) => ({
...current,
chapters: current.chapters.map((ch) => ({
...ch,
chunks: ch.chunks.map((chunk) =>
chunk.id === chunkId
? { ...chunk, greekWords: chunk.greekWords.filter((w) => w.id !== wordId) }
: chunk,
),
})),
}));
};
const suggestGreekWordsForChunk = 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;
if (!NT_BOOK_NUMBER[chapter.bookAbbrev]) {
setStatusMessage('Auto-suggest only works for New Testament books.');
window.setTimeout(() => setStatusMessage(''), 2500);
return;
}
setSuggestingGreekForChunkId(chunkId);
try {
const [concordance, dict, gloss] = await Promise.all([loadNtConcordance(), loadGreekDict(), loadNtGloss()]);
const bookData = concordance[chapter.bookAbbrev] ?? {};
const chapterData = bookData[chapter.chapter] ?? {};
const nextChapterData = chunkSpansNextChapter(project, chapterIndex, chunk)
? bookData[project.chapters[chapterIndex + 1].chapter] ?? {}
: {};
const strongsInRange = new Set();
for (let v = chunk.startVerse; v <= chunk.endVerse; v++) {
for (const s of chapterData[String(v)] ?? []) strongsInRange.add(s);
}
if (chunkSpansNextChapter(project, chapterIndex, chunk)) {
for (let v = 1; v <= chunk.spilloverEndVerse; v++) {
for (const s of nextChapterData[String(v)] ?? []) strongsInRange.add(s);
}
}
if (strongsInRange.size === 0) {
setStatusMessage("No Strong's data found for this passage.");
window.setTimeout(() => setStatusMessage(''), 3000);
return;
}
const existingNumbers = new Set(
chunk.greekWords.map((w) => w.strongNumber.toUpperCase()).filter(Boolean)
);
const modalWords = [];
for (const strongKey of strongsInRange) {
if (existingNumbers.has(strongKey)) continue;
const entry = dict[strongKey];
modalWords.push({
strongKey,
lexeme: entry?.lemma ?? '',
translit: entry?.translit ?? '',
def: gloss[strongKey] ?? entry?.kjv_def ?? 'No definition found.',
entry,
});
}
if (modalWords.length === 0) {
setStatusMessage('All words from this passage are already added.');
window.setTimeout(() => setStatusMessage(''), 2000);
return;
}
// Pre-select content words; pre-uncheck known function words
const preSelected = new Set(
modalWords
.filter((w) => !GREEK_FUNCTION_WORDS.has(w.strongKey))
.map((w) => w.strongKey)
);
setSuggestModal({
chunkId,
language: 'greek',
helperText: "Content words are pre-checked. Uncheck any you don't need.",
words: modalWords,
});
setSuggestSelection(preSelected);
} catch (err) {
setStatusMessage(`Suggest failed: ${err.message}`);
window.setTimeout(() => setStatusMessage(''), 3000);
} finally {
setSuggestingGreekForChunkId(null);
}
};
const suggestHebrewWordsForChunk = 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;
if (NT_BOOK_NUMBER[chapter.bookAbbrev]) {
setStatusMessage('Hebrew suggestions are for Old Testament passages.');
window.setTimeout(() => setStatusMessage(''), 2500);
return;
}
setSuggestingHebrewForChunkId(chunkId);
try {
const dict = await loadHebrewDict();
const versesInChunk = getChunkVerseEntries(project, chapterIndex, chunk)
.map((v) => v.text)
.join(' ')
.toLowerCase();
const candidateWords = Array.from(new Set(
versesInChunk
.replace(/[^a-z\s]/g, ' ')
.split(/\s+/)
.map((w) => w.trim())
.filter((w) => w.length >= 4 && !ENGLISH_STOP_WORDS.has(w)),
));
if (candidateWords.length === 0) {
setStatusMessage('No Hebrew suggestion candidates found in this passage.');
window.setTimeout(() => setStatusMessage(''), 2500);
return;
}
const existingNumbers = new Set(
chunk.greekWords.map((w) => (w.strongNumber || '').toUpperCase()).filter(Boolean)
);
const seen = new Set();
const modalWords = [];
const entries = Object.entries(dict);
for (const token of candidateWords) {
const match = entries.find(([, entry]) => {
const defs = `${entry.kjv_def || ''} ${entry.strongs_def || ''}`.toLowerCase();
return defs.split(/[,;()\s]+/).includes(token);
});
if (!match) continue;
const [strongKey, entry] = match;
if (!/^H\d+$/i.test(strongKey)) continue;
const normalized = strongKey.toUpperCase();
if (existingNumbers.has(normalized) || seen.has(normalized)) continue;
seen.add(normalized);
modalWords.push({
strongKey: normalized,
lexeme: entry?.lemma ?? '',
translit: entry?.xlit ?? '',
def: shortHebrewGloss(entry?.kjv_def) || 'No definition found.',
fullDef: entry?.kjv_def ?? '',
entry,
});
if (modalWords.length >= 20) break;
}
if (modalWords.length === 0) {
setStatusMessage('No Hebrew suggestions found from this passage text.');
window.setTimeout(() => setStatusMessage(''), 2500);
return;
}
setSuggestModal({
chunkId,
language: 'hebrew',
helperText: 'Heuristic matches from passage text; uncheck anything not useful.',
words: modalWords,
});
setSuggestSelection(new Set(modalWords.map((w) => w.strongKey)));
} catch (err) {
setStatusMessage(`Hebrew suggest failed: ${err.message}`);
window.setTimeout(() => setStatusMessage(''), 3000);
} finally {
setSuggestingHebrewForChunkId(null);
}
};
const confirmSuggestWords = () => {
if (!suggestModal) return;
const { chunkId, words } = suggestModal;
const newWords = words
.filter((w) => suggestSelection.has(w.strongKey))
.map((w) => ({
id: makeId(),
query: w.strongKey,
strongNumber: w.strongKey,
englishGloss: w.def,
lexeme: w.lexeme,
transliteration: w.translit,
partOfSpeech: '',
shortDefinition: w.fullDef || w.def,
definitionHtml: w.entry ? buildGreekDefinitionHtml(w.strongKey, w.entry) : '',
loading: false,
}));
if (newWords.length > 0) {
updateProject((current) => ({
...current,
chapters: current.chapters.map((ch) => ({
...ch,
chunks: ch.chunks.map((c) =>
c.id === chunkId ? { ...c, greekWords: [...c.greekWords, ...newWords] } : c
),
})),
}));
const label = suggestModal?.language === 'hebrew' ? 'Hebrew' : 'Greek';
setStatusMessage(`Added ${newWords.length} ${label} word${newWords.length > 1 ? 's' : ''}.`);
window.setTimeout(() => setStatusMessage(''), 2000);
}
setSuggestModal(null);
setSuggestSelection(new Set());
};
const lookupWord = async (chunkId, wordId, language = 'greek') => {
const chunk = allChunks.find((c) => c.id === chunkId);
const word = chunk?.greekWords.find((w) => w.id === wordId);
if (!word || !word.query.trim()) return;
updateChunkWord(chunkId, wordId, { loading: true });
try {
// Normalize bare numbers based on selected lookup language.
const raw = word.query.trim();
const inferredPrefix = language === 'hebrew' ? 'H' : 'G';
const normalized = /^\d+$/.test(raw)
? `${inferredPrefix}${raw}`
: /^[gGhH]\d+$/.test(raw)
? raw.toUpperCase()
: raw;
const [definitions, gloss] = await Promise.all([
fetchBollsDefinition(normalized),
loadNtGloss().catch(() => ({})),
]);
if (!definitions || definitions.length === 0) {
updateChunkWord(chunkId, wordId, {
strongNumber: '',
shortDefinition: 'No definition found.',
definitionHtml: '',
});
return;
}
const first = definitions[0];
const strongKey = (first.topic || normalized).toUpperCase();
const extractedPartOfSpeech = extractPartOfSpeech(first.definition || '');
updateChunkWord(chunkId, wordId, {
strongNumber: first.topic || normalized,
englishGloss: gloss[strongKey] || first.short_definition || '',
lexeme: first.lexeme || '',
transliteration: first.transliteration || '',
partOfSpeech: extractedPartOfSpeech || word.partOfSpeech || '',
shortDefinition: first.short_definition || '',
definitionHtml: first.definition || '',
});
} catch {
updateChunkWord(chunkId, wordId, { shortDefinition: 'Lookup failed.' });
} finally {
updateChunkWord(chunkId, wordId, { loading: false });
}
};
// ---------------------------------------------------------------------------
// Home page helpers
// ---------------------------------------------------------------------------
@@ -1295,7 +668,6 @@ const App = () => {
...audio,
// Reader
...reader,
copyVerse,
// Setup
project, setProject,
setup,
@@ -1325,49 +697,8 @@ const App = () => {
moveChunk,
deleteChunk,
updateProject,
// Study
selectedChunk,
selectedChunkChapterIndex,
selectedChunkChapter,
selectedChunkVerses,
selectedChunkGlobalIndex,
studyLayout, setStudyLayout,
activeStudyTab, setActiveStudyTab,
mobileStudyTab, setMobileStudyTab,
collapsedSections, setCollapsedSections,
interlinearData,
interlinearLoading,
interlinearError,
commentarySource, setCommentarySource,
commentaryData,
commentaryLoading,
commentaryError,
crossRefInput, setCrossRefInput,
suggestingCrossRefs,
crossRefSuggestions,
tagInput, setTagInput,
suggestModal, setSuggestModal,
suggestSelection, setSuggestSelection,
suggestingGreekForChunkId,
suggestingHebrewForChunkId,
goToPreviousChunk,
goToNextChunk,
// Study (editor state lives in StudyPage and its hooks)
updateChunk,
addCrossRef,
removeCrossRef,
suggestCrossRefsForChunk,
addSuggestedCrossRef,
addTag,
removeTag,
addGreekWord,
removeGreekWord,
updateChunkWord,
lookupWord,
suggestGreekWordsForChunk,
suggestHebrewWordsForChunk,
confirmSuggestWords,
loadVerseText,
importFinalScriptDocx,
};
return (