feat(reader): multi-word and phrasal verb detector in reader (Task 14)

This commit is contained in:
Antigravity committed 2026-10-06 15:16:02 +03:00
1 parent 37a1f1ab35
commit 1c3f03a64c
9 files changed
+738 -16

No files matched your search

+10 -2
View File
@@ -96,13 +96,21 @@ export const api = {
// Dictionary Lookup & AI Sentence Translation
lookup: {
async search(term) {
const resp = await fetch(`/api/lookup?term=${encodeURIComponent(term)}`);
async search(term, context = '') {
let url = `/api/lookup?term=${encodeURIComponent(term)}`;
if (context) {
url += `&context=${encodeURIComponent(context)}`;
}
const resp = await fetch(url);
if (resp.ok) {
return await resp.json();
}
throw new Error(`Lookup failed: ${resp.status}`);
},
async detectPhrasalVerb(sentence, word) {
const resp = await fetch(`/api/detect-phrasal-verb?sentence=${encodeURIComponent(sentence)}&word=${encodeURIComponent(word)}`);
return resp.ok ? await resp.json() : { detected: false, phrasal_verb: null };
},
async translateSentence(sentence, targetWord = '') {
const resp = await fetch('/api/translate-sentence', {
method: 'POST',
+95 -8
View File
@@ -1160,6 +1160,17 @@ export async function openWordPopup(term, contextSentence, targetElement) {
const popupContextMeaningVal = document.getElementById('popup-context-meaning-val');
const popupSentenceAiBar = document.querySelector('.popup-sentence-ai-bar');
const popupPhrasalCard = document.getElementById('popup-phrasal-card');
const popupPhrasalTerm = document.getElementById('popup-phrasal-term');
const popupPhrasalRaw = document.getElementById('popup-phrasal-raw');
const popupPhrasalTranslation = document.getElementById('popup-phrasal-translation');
const popupPhrasalLibraryBadge = document.getElementById('popup-phrasal-library-badge');
const popupSwitchPhrasalBtn = document.getElementById('popup-switch-phrasal-btn');
if (popupPhrasalCard) popupPhrasalCard.classList.add('hidden');
popupCurrentData.phrasalVerb = null;
popupCurrentData.isPhrasalMode = false;
if (popupSentenceLoading) popupSentenceLoading.classList.add('hidden');
if (popupSentenceTranslationWrap) popupSentenceTranslationWrap.classList.add('hidden');
if (popupSentenceRu) popupSentenceRu.textContent = '';
@@ -1202,9 +1213,27 @@ export async function openWordPopup(term, contextSentence, targetElement) {
positionWordPopup(targetElement);
try {
const data = await api.lookup.search(cleanTerm);
const data = await api.lookup.search(cleanTerm, contextSentence);
popupCurrentData.lookup = data;
// Phrasal verb detection
if (data.phrasal_verb) {
const pv = data.phrasal_verb;
popupCurrentData.phrasalVerb = pv;
if (popupPhrasalCard) {
popupPhrasalCard.classList.remove('hidden');
if (popupPhrasalTerm) popupPhrasalTerm.textContent = pv.term;
if (popupPhrasalRaw) popupPhrasalRaw.textContent = `(в тексте: «${pv.raw}»)`;
if (popupPhrasalTranslation) popupPhrasalTranslation.textContent = pv.translation;
if (popupPhrasalLibraryBadge) popupPhrasalLibraryBadge.classList.toggle('hidden', !pv.in_library);
if (popupSwitchPhrasalBtn) {
popupSwitchPhrasalBtn.textContent = `🔗 Переключить на «${pv.term}»`;
}
}
} else {
if (popupPhrasalCard) popupPhrasalCard.classList.add('hidden');
}
const trans = data.translation || data.translation_ru || (data.translations_list && data.translations_list.length > 0 ? data.translations_list.join(', ') : '');
if (popupPhonetic) popupPhonetic.textContent = data.phonetic || '';
if (popupTranslation) popupTranslation.textContent = trans || 'Перевод не найден';
@@ -1924,23 +1953,81 @@ export function initReader() {
});
}
const popupSwitchPhrasalBtn = document.getElementById('popup-switch-phrasal-btn');
if (popupSwitchPhrasalBtn) {
popupSwitchPhrasalBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (!popupCurrentData || !popupCurrentData.phrasalVerb) return;
const pv = popupCurrentData.phrasalVerb;
if (!popupCurrentData.isPhrasalMode) {
// Switch to phrasal verb mode
popupCurrentData.isPhrasalMode = true;
if (popupTerm) popupTerm.textContent = pv.term;
if (popupTranslation) popupTranslation.textContent = pv.translation;
if (popupPhonetic) popupPhonetic.textContent = '';
const inLib = Boolean(pv.in_library);
if (popupInLibrary) popupInLibrary.classList.toggle('hidden', !inLib);
if (popupSaveBtn) {
popupSaveBtn.classList.toggle('hidden', inLib);
popupSaveBtn.textContent = `💾 Сохранить «${pv.term}» в словарь`;
}
if (popupSavedBadge) {
popupSavedBadge.classList.toggle('hidden', !inLib);
popupSavedBadge.textContent = '⭐ Фразовый глагол уже в библиотеке';
}
if (popupAddContextBtn) popupAddContextBtn.classList.add('hidden');
popupSwitchPhrasalBtn.textContent = `↩️ Вернуться к «${popupCurrentData.term}»`;
} else {
// Revert back to single word
popupCurrentData.isPhrasalMode = false;
const origTrans = popupCurrentData.lookup ? (popupCurrentData.lookup.translation || popupCurrentData.lookup.translation_ru || '') : '';
if (popupTerm) popupTerm.textContent = popupCurrentData.term;
if (popupTranslation) popupTranslation.textContent = origTrans;
if (popupPhonetic) popupPhonetic.textContent = popupCurrentData.lookup?.phonetic || '';
const inLib = Boolean(popupCurrentData.lookup?.in_library || popupCurrentData.existingCard);
if (popupInLibrary) popupInLibrary.classList.toggle('hidden', !inLib);
if (popupSaveBtn) {
popupSaveBtn.classList.toggle('hidden', inLib);
popupSaveBtn.textContent = '💾 Добавить в словарь';
}
if (popupSavedBadge) {
popupSavedBadge.classList.toggle('hidden', !inLib);
popupSavedBadge.textContent = '⭐ Слово уже в библиотеке';
}
popupSwitchPhrasalBtn.textContent = `🔗 Переключить на «${pv.term}»`;
}
positionWordPopup(popupCurrentData.targetElement);
});
}
if (popupSaveBtn) {
popupSaveBtn.addEventListener('click', async () => {
if (!popupCurrentData) return;
const d = popupCurrentData;
const lookup = d.lookup || {};
const bookTag = currentBook ? slugify(currentBook.title) : 'reader';
const isPv = Boolean(d.isPhrasalMode && d.phrasalVerb);
const termToSave = isPv ? d.phrasalVerb.term : d.term;
const transToSave = isPv
? d.phrasalVerb.translation
: (lookup.translation || lookup.translation_ru || (popupTranslation ? popupTranslation.textContent : ''));
const collectionToSave = isPv ? 'Фразовые глаголы & Идиомы' : 'Книги & Сериалы';
const tagsToSave = isPv ? `${bookTag}, phrasal-verbs, reader` : `${bookTag}, reader`;
const payload = {
term: d.term,
translation: lookup.translation || lookup.translation_ru || (popupTranslation ? popupTranslation.textContent : ''),
term: termToSave,
translation: transToSave,
context_sentence: d.contextSentence || '',
phonetic: lookup.phonetic || '',
audio_url: lookup.audio_url || '',
part_of_speech: lookup.part_of_speech || '',
phonetic: isPv ? '' : (lookup.phonetic || ''),
audio_url: isPv ? '' : (lookup.audio_url || ''),
part_of_speech: isPv ? 'verb phrase' : (lookup.part_of_speech || ''),
definition_en: lookup.definition_en || '',
collection: 'Книги & Сериалы',
tags: `${bookTag}, reader`,
collection: collectionToSave,
tags: tagsToSave,
notes: currentBook ? `Из книги: ${currentBook.title}` : '',
};