import { STOP_WORDS } from '../data/stopwords.js'; import { state } from '../core/state.js'; import { saveSchedule, saveWords } from '../core/storage.js'; import { showToast } from '../ui/toast.js'; import { speak } from '../services/tts.js'; import { callAI, translateWord } from '../services/ai.js'; import { initWordSchedule } from '../services/ebbinghaus.js'; import { getMastery } from '../services/stats.js'; import { escapeHtml, formatFormsHtml } from '../ui/dom.js'; import { findWordContext } from '../services/extractor.js'; import { getNextId } from './words.js'; import { beginMailLearnRequest, isCurrentMailLearnRequest } from './mail-learn.js'; export let readerTranslations = {}; export let readerSentences = []; let readerSessionGeneration = 0; // ==================== Full-Text Reading Mode ==================== export function splitSentences(text) { const raw = text.split(/(?<=[.!?])\s+|(?:\r?\n){2,}/); return raw.map(s => s.trim()).filter(s => s.length > 0); } export function isStopWord(w) { return STOP_WORDS.has(w.toLowerCase()); } export function buildReadingSentenceHtml(sentence) { const knownMap = {}; state.words.forEach(w => { knownMap[w.english.toLowerCase()] = w; }); const tokens = sentence.split(/(\s+|[,.;:!?'"()\[\]{}<>\/\\—–\-])/); let html = ''; tokens.forEach(tok => { const clean = tok.toLowerCase().replace(/[^a-z'-]/g, ''); const word = knownMap[clean]; if (word && clean.length >= 2) { html += `${escapeHtml(tok)}`; } else if (/^[a-zA-Z]{2,}$/.test(clean) && !isStopWord(clean)) { html += `${escapeHtml(tok)}`; } else { html += escapeHtml(tok); } }); return html; } export async function startFullReading() { const sessionGeneration = ++readerSessionGeneration; const text = document.getElementById('mail-learn-input').value.trim(); if (!text) { showToast('请粘贴邮件内容', 'error'); return; } const sentences = splitSentences(text); if (!sentences.length) { showToast('无法解析邮件内容', 'error'); return; } const resultEl = document.getElementById('mail-learn-result'); const request = beginMailLearnRequest(resultEl); resultEl.innerHTML = '
正在准备全文阅读...
'; let translations = {}; const hasAI = state.settings.apiKey && state.settings.apiUrl && state.settings.model; if (hasAI) { try { const numbered = sentences.map((s, i) => `${i + 1}. ${s}`).join('\n'); const content = await callAI([ { role: 'system', content: '你是翻译助手。用户给出编号的英文句子,请逐句翻译为中文,严格保持编号格式返回。只返回翻译结果,每行格式:编号. 中文翻译' }, { role: 'user', content: numbered } ]); if (sessionGeneration !== readerSessionGeneration || !isCurrentMailLearnRequest(request)) return; content.split('\n').forEach(line => { const m = line.match(/^(\d+)\.\s*(.+)/); if (m) translations[Number(m[1]) - 1] = m[2].trim(); }); } catch (err) { if (sessionGeneration !== readerSessionGeneration || !isCurrentMailLearnRequest(request)) return; showToast('AI 翻译失败,阅读模式仍可使用:' + err.message, 'warning'); } } if (sessionGeneration !== readerSessionGeneration || !isCurrentMailLearnRequest(request) || !resultEl.isConnected) return; const knownCount = countKnownWordsInText(text); const totalUniqueWords = countUniqueWords(text); const coveragePct = totalUniqueWords > 0 ? Math.round(knownCount / totalUniqueWords * 100) : 0; resultEl.innerHTML = `

全文阅读

${sentences.length} 句 词库覆盖 ${coveragePct}%
词库中的单词(点击查看详情) 生词(点击查询) 点击句子查看翻译
${sentences.map((s, i) => `
${i + 1}
${buildReadingSentenceHtml(s)}
${!translations[i] && hasAI ? `` : ''}
`).join('')}
`; readerTranslations = translations; readerSentences = sentences; } export function countKnownWordsInText(text) { const knownSet = new Set(state.words.map(w => w.english.toLowerCase())); const tokens = text.match(/\b[a-zA-Z]{2,}\b/g) || []; const unique = new Set(tokens.map(t => t.toLowerCase())); let count = 0; unique.forEach(w => { if (!STOP_WORDS.has(w) && knownSet.has(w)) count++; }); return count; } export function countUniqueWords(text) { const tokens = text.match(/\b[a-zA-Z]{2,}\b/g) || []; const unique = new Set(tokens.map(t => t.toLowerCase())); let count = 0; unique.forEach(w => { if (!STOP_WORDS.has(w)) count++; }); return count; } export function speakSentence(idx) { const sentences = readerSentences; if (sentences && sentences[idx]) speak(sentences[idx]); } export function toggleSentenceTranslation(idx) { const el = document.getElementById(`reader-cn-${idx}`); if (!el) return; el.style.display = el.style.display === 'none' ? 'block' : 'none'; } export function toggleAllTranslations() { const els = document.querySelectorAll('[id^="reader-cn-"]'); const anyHidden = [...els].some(e => e.style.display === 'none'); els.forEach(e => { e.style.display = anyHidden ? 'block' : 'none'; }); } export async function aiTranslateSentence(idx, btnEl) { const sessionGeneration = readerSessionGeneration; const sentence = readerSentences?.[idx]; if (!sentence) return; const cnEl = document.getElementById(`reader-cn-${idx}`); if (!cnEl) return; if (btnEl) btnEl.disabled = true; const isCurrentSentence = () => sessionGeneration === readerSessionGeneration && readerSentences?.[idx] === sentence && cnEl.isConnected && (!btnEl || btnEl.isConnected); try { const content = await callAI([ { role: 'system', content: '你是翻译助手,只返回中文翻译结果,不要任何解释。' }, { role: 'user', content: `翻译为中文:${sentence}` } ]); if (!isCurrentSentence()) return; const translation = content.trim(); cnEl.textContent = translation; cnEl.style.display = 'block'; readerTranslations[idx] = translation; if (btnEl) btnEl.remove(); } catch (err) { if (!isCurrentSentence()) return; showToast('翻译失败: ' + err.message, 'error'); if (btnEl) btnEl.disabled = false; } } export function positionReaderTooltip(tip, el) { const margin = 8; const gap = 6; const rect = el.getBoundingClientRect(); const scrollTop = window.scrollY || document.documentElement.scrollTop; const tipWidth = tip.offsetWidth; const tipHeight = tip.offsetHeight; const left = Math.min( Math.max(rect.left, margin), Math.max(margin, window.innerWidth - tipWidth - margin) ); const top = rect.bottom + gap + tipHeight <= window.innerHeight ? rect.bottom + scrollTop + gap : Math.max(scrollTop + margin, rect.top + scrollTop - tipHeight - gap); tip.style.left = `${left}px`; tip.style.top = `${top}px`; } let readerTipCloseHandler = null; let readerTipCloseTimer = null; let readerTipSeq = 0; function bindReaderTipClose(tip, anchorEl) { if (readerTipCloseTimer !== null) { clearTimeout(readerTipCloseTimer); readerTipCloseTimer = null; } if (readerTipCloseHandler) { document.removeEventListener('click', readerTipCloseHandler); readerTipCloseHandler = null; } const handler = (e) => { if (!tip.contains(e.target) && e.target !== anchorEl) { ++readerTipSeq; tip.style.display = 'none'; document.removeEventListener('click', handler); if (readerTipCloseHandler === handler) readerTipCloseHandler = null; } }; readerTipCloseHandler = handler; readerTipCloseTimer = setTimeout(() => { readerTipCloseTimer = null; if (readerTipCloseHandler === handler) { document.addEventListener('click', handler); } }, 10); } export function showReaderWordTip(el, wordId) { ++readerTipSeq; const word = state.words.find(w => w.id === wordId); if (!word) return; const tip = document.getElementById('reader-word-tooltip'); if (!tip) return; const mastery = getMastery(word.id); const masteryText = mastery === 'mastered' ? '已掌握' : mastery === 'learning' ? '学习中' : '新词'; const badgeClass = mastery === 'mastered' ? 'badge-success' : mastery === 'learning' ? 'badge-warning' : 'badge-primary'; tip.innerHTML = `
${escapeHtml(word.english)} ${escapeHtml(word.phonetic || '')} ${masteryText}
${escapeHtml(word.chinese || '')}
${word.forms ? `
${formatFormsHtml(word.forms)}
` : ''} ${word.example ? `
${escapeHtml(word.example.en)}
${escapeHtml(word.example.cn)}
` : ''} `; tip.style.display = 'block'; positionReaderTooltip(tip, el); bindReaderTipClose(tip, el); } export async function showReaderUnknownTip(el, word) { const tip = document.getElementById('reader-word-tooltip'); if (!tip) return; const seq = ++readerTipSeq; tip.innerHTML = `
${escapeHtml(word)}
查询中...
`; tip.style.display = 'block'; positionReaderTooltip(tip, el); bindReaderTipClose(tip, el); const hasAI = state.settings.apiKey && state.settings.apiUrl && state.settings.model; if (hasAI) { try { const result = await translateWord(word); if (seq !== readerTipSeq) return; tip.innerHTML = `
${escapeHtml(result.english || word)} ${escapeHtml(result.phonetic || '')}
${escapeHtml(result.chinese || '')}
${result.forms ? `
${formatFormsHtml(result.forms)}
` : ''} ${result.example ? `
${escapeHtml(result.example.en)}
${escapeHtml(result.example.cn)}
` : ''} `; positionReaderTooltip(tip, el); } catch (err) { if (seq !== readerTipSeq) return; const cnEl = tip.querySelector('.reader-tip-cn'); if (cnEl) cnEl.textContent = '查询失败: ' + err.message; } } else { const cnEl = tip.querySelector('.reader-tip-cn'); if (cnEl) cnEl.textContent = '请先配置 AI 设置以启用翻译'; } } export async function addWordFromReader(word, btnEl) { const key = String(word || '').trim().toLowerCase(); const libraryId = state.activeWordLibrary; const sessionGeneration = readerSessionGeneration; const wordExists = () => state.words.some(item => String(item.english || '').trim().toLowerCase() === key); const isCurrentOperation = () => state.activeWordLibrary === libraryId && readerSessionGeneration === sessionGeneration && (!btnEl || btnEl.isConnected); if (!key) return false; if (wordExists()) { showToast(`「${word}」已在词库中`, 'info'); if (btnEl) { btnEl.disabled = true; btnEl.textContent = '已添加 ✓'; } return true; } if (btnEl) { btnEl.disabled = true; btnEl.textContent = '添加中...'; } try { const result = await translateWord(word); if (!isCurrentOperation()) return false; if (wordExists()) { showToast(`「${word}」已在词库中`, 'info'); if (btnEl) { btnEl.textContent = '已添加 ✓'; btnEl.classList.remove('btn-primary'); btnEl.classList.add('btn-success'); } return true; } const emailText = document.getElementById('mail-learn-input')?.value || ''; const contextSentence = findWordContext(word, emailText); const emailTitle = emailText.slice(0, 40).replace(/\n/g, ' ') + '...'; const previousWords = state.words.slice(); const previousSchedule = { ...state.schedule }; const id = getNextId(); state.words.push({ id, ...result, sourceContext: contextSentence ? { emailTitle, sentence: contextSentence } : null }); initWordSchedule(id, false); if (!(saveWords() && saveSchedule())) { state.words = previousWords; state.schedule = previousSchedule; saveWords(); saveSchedule(); throw new Error('本地存储写入失败'); } showToast(`已添加「${word}」到词库`, 'success'); if (btnEl) { btnEl.textContent = '已添加 \u2713'; btnEl.classList.remove('btn-primary'); btnEl.classList.add('btn-success'); } return true; } catch (err) { if (!isCurrentOperation()) return false; showToast('添加失败: ' + err.message, 'error'); if (btnEl) { btnEl.disabled = false; btnEl.textContent = '重试'; } return false; } }