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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Commentary and interlinear text for the chapter of the selected chunk.
|
||||
// Each loads only while its section is expanded; results are cached for the visit.
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const commentaryCache = {}; // "commentaryId/BOOK/chapter" -> data
|
||||
const interlinearCache = {}; // BOOK -> whole-book interlinear data
|
||||
|
||||
export function useStudyResources({ project, selectedChunk, selectedChunkChapterIndex, collapsedSections }) {
|
||||
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 [interlinearData, setInterlinearData] = useState(null);
|
||||
const [interlinearLoading, setInterlinearLoading] = useState(false);
|
||||
const [interlinearError, setInterlinearError] = useState('');
|
||||
|
||||
const loadInterlinearChapter = async (bookAbbrev, chapterNumber) => {
|
||||
if (interlinearCache[bookAbbrev]) return interlinearCache[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();
|
||||
interlinearCache[bookAbbrev] = data;
|
||||
return data;
|
||||
};
|
||||
|
||||
const loadCommentaryChapter = async (commentaryId, bookAbbrev, chapterNumber) => {
|
||||
const cacheKey = `${commentaryId}/${bookAbbrev}/${chapterNumber}`;
|
||||
if (commentaryCache[cacheKey]) return commentaryCache[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();
|
||||
commentaryCache[cacheKey] = data;
|
||||
return data;
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
return {
|
||||
commentarySource, setCommentarySource,
|
||||
commentaryData,
|
||||
commentaryLoading,
|
||||
commentaryError,
|
||||
interlinearData,
|
||||
interlinearLoading,
|
||||
interlinearError,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// Greek/Hebrew word studies on a chunk: word entries, Strong's lookups, and the
|
||||
// "suggest words from this passage" picker.
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
NT_BOOK_NUMBER,
|
||||
chunkSpansNextChapter,
|
||||
extractPartOfSpeech,
|
||||
getChunkVerseEntries,
|
||||
makeId,
|
||||
} from '../utils/bibleUtils.jsx';
|
||||
import {
|
||||
ENGLISH_STOP_WORDS,
|
||||
GREEK_FUNCTION_WORDS,
|
||||
buildGreekDefinitionHtml,
|
||||
fetchBollsDefinition,
|
||||
loadGreekDict,
|
||||
loadHebrewDict,
|
||||
loadNtConcordance,
|
||||
loadNtGloss,
|
||||
shortHebrewGloss,
|
||||
} from '../utils/wordStudy.js';
|
||||
|
||||
// notify(message, ms) shows a short status message on the study page.
|
||||
export function useWordStudy({ project, allChunks, updateProject, notify }) {
|
||||
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 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]) {
|
||||
notify('Auto-suggest only works for New Testament books.', 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) {
|
||||
notify("No Strong's data found for this passage.", 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) {
|
||||
notify('All words from this passage are already added.', 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) {
|
||||
notify(`Suggest failed: ${err.message}`, 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]) {
|
||||
notify('Hebrew suggestions are for Old Testament passages.', 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) {
|
||||
notify('No Hebrew suggestion candidates found in this passage.', 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) {
|
||||
notify('No Hebrew suggestions found from this passage text.', 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) {
|
||||
notify(`Hebrew suggest failed: ${err.message}`, 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';
|
||||
notify(`Added ${newWords.length} ${label} word${newWords.length > 1 ? 's' : ''}.`, 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 });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
suggestingGreekForChunkId,
|
||||
suggestingHebrewForChunkId,
|
||||
suggestModal, setSuggestModal,
|
||||
suggestSelection, setSuggestSelection,
|
||||
updateChunkWord,
|
||||
addGreekWord,
|
||||
removeGreekWord,
|
||||
lookupWord,
|
||||
suggestGreekWordsForChunk,
|
||||
suggestHebrewWordsForChunk,
|
||||
confirmSuggestWords,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user