From ea23681e69828d0eae004032576e06736dcc320d Mon Sep 17 00:00:00 2001 From: heznpc Date: Wed, 10 Jun 2026 15:53:50 +0900 Subject: [PATCH] feat(tutor): viewport-centred lesson grounding + quiz/summarize chips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Item 4a of the learning-companion deepening: - getPageContext now grounds the tutor in what the user is ACTUALLY reading. Long lessons used to send only the first 2,000 chars, so questions about anything past the fold had no grounding. The context now carries a short lesson opening (global context) + the text from the block currently in the viewport, and the heading map (up to 8) marks the current section with "▶". Hard-capped at 2,000 chars total — the privacy policy's "≤2,000 chars of lesson context" disclosure holds regardless of how the pieces combine. Exam-page behavior unchanged. - Two new suggestion chips in all 12 UI languages: "Quiz me on this lesson" and "Summarize this lesson" — the two highest-value learning actions the grounded tutor can serve ("key takeaways" chip kept; the redundant third example rotated out to keep the chip row tight). Gates: 527 unit tests, full E2E 19/19, lint, prettier — green. --- src/content/content.js | 41 +++++++++++++++++++++++++++++---- src/lib/constants.js | 52 +++++++++++++++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/content/content.js b/src/content/content.js index c8e1243..28446b9 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -791,12 +791,45 @@ if (isExamPage) { return `Certification Exam: ${title}. Page type: exam/assessment. DO NOT help with answers.`; } - const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4')) - .map((h) => h.textContent.trim()) - .slice(0, 5) + // Heading map with the section the user is currently reading marked — + // gives the tutor a table of contents plus "you are here". + const hs = Array.from(document.querySelectorAll('h1, h2, h3, h4')); + let currentIdx = -1; + for (let i = 0; i < hs.length; i++) { + if (hs[i].getBoundingClientRect().top <= 80) currentIdx = i; + } + const headings = hs + .slice(0, 8) + .map((h, i) => (i === currentIdx ? `▶ ${h.textContent.trim()}` : h.textContent.trim())) .join(', '); const lessonBody = document.querySelector('#lesson-main, .lesson-content, .course-content, main'); - const bodyText = lessonBody ? lessonBody.innerText.replace(/\s+/g, ' ').trim().slice(0, 2000) : ''; + // Viewport-centred extract: long lessons used to send only the FIRST + // 2,000 chars, so questions about anything past the fold had no grounding. + // Now: a short lesson opening for global context + the text from the block + // the user is actually looking at. Hard-capped at 2,000 chars total — the + // privacy policy discloses "≤2,000 chars of lesson context" and that cap + // must hold no matter how the pieces combine. + let bodyText = ''; + if (lessonBody) { + const flat = lessonBody.innerText.replace(/\s+/g, ' ').trim(); + const blocks = Array.from(lessonBody.querySelectorAll('p, li, h2, h3, h4, pre, td')); + const vpIdx = blocks.findIndex((el) => el.getBoundingClientRect().bottom > 0); + if (vpIdx > 0) { + let current = ''; + for (let i = Math.max(0, vpIdx - 1); i < blocks.length && current.length < 1500; i++) { + current += blocks[i].innerText.replace(/\s+/g, ' ').trim() + ' '; + } + current = current.trim().slice(0, 1500); + const opening = flat.slice(0, 400); + bodyText = + current && !opening.includes(current.slice(0, 80)) + ? `${opening}\n\n[User is currently viewing:]\n${current}` + : flat.slice(0, 2000); + } else { + bodyText = flat.slice(0, 2000); + } + bodyText = bodyText.slice(0, 2000); + } return `Course: ${title}. Sections: ${headings}${bodyText ? `\n\nLesson content:\n${bodyText}` : ''}`; } diff --git a/src/lib/constants.js b/src/lib/constants.js index 3f1964b..ab36cb9 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -422,17 +422,47 @@ const ONBOARDING_LABELS = { }; const EXAMPLE_QUESTIONS = { - en: ['Explain this concept simply', 'What are the key takeaways?', 'Give me a practical example'], - ko: ['이 개념을 쉽게 설명해줘', '핵심 포인트가 뭐야?', '실제 예시를 들어줘'], - ja: ['この概念を簡単に説明して', '重要なポイントは?', '実例を教えて'], - 'zh-CN': ['简单解释一下这个概念', '关键要点是什么?', '给我一个实际例子'], - 'zh-TW': ['簡單解釋一下這個概念', '關鍵要點是什麼?', '給我一個實際例子'], - es: ['Explica este concepto de forma simple', '¿Cuáles son los puntos clave?', 'Dame un ejemplo práctico'], - fr: ['Explique ce concept simplement', 'Quels sont les points clés ?', 'Donne-moi un exemple pratique'], - de: ['Erkläre dieses Konzept einfach', 'Was sind die wichtigsten Punkte?', 'Gib mir ein praktisches Beispiel'], - 'pt-BR': ['Explique este conceito de forma simples', 'Quais são os pontos-chave?', 'Me dê um exemplo prático'], - ru: ['Объясни эту концепцию простыми словами', 'Какие ключевые выводы?', 'Приведи практический пример'], - vi: ['Giải thích khái niệm này một cách đơn giản', 'Những điểm chính là gì?', 'Cho tôi một ví dụ thực tế'], + en: ['Explain this concept simply', 'What are the key takeaways?', 'Quiz me on this lesson', 'Summarize this lesson'], + ko: ['이 개념을 쉽게 설명해줘', '핵심 포인트가 뭐야?', '이 레슨으로 퀴즈 내줘', '이 레슨 요약해줘'], + ja: ['この概念を簡単に説明して', '重要なポイントは?', 'このレッスンでクイズを出して', 'このレッスンを要約して'], + 'zh-CN': ['简单解释一下这个概念', '关键要点是什么?', '就这节课考考我', '总结一下这节课'], + 'zh-TW': ['簡單解釋一下這個概念', '關鍵要點是什麼?', '就這節課考考我', '總結一下這節課'], + es: [ + 'Explica este concepto de forma simple', + '¿Cuáles son los puntos clave?', + 'Hazme un quiz sobre esta lección', + 'Resume esta lección', + ], + fr: [ + 'Explique ce concept simplement', + 'Quels sont les points clés ?', + 'Fais-moi un quiz sur cette leçon', + 'Résume cette leçon', + ], + de: [ + 'Erkläre dieses Konzept einfach', + 'Was sind die wichtigsten Punkte?', + 'Frag mich zu dieser Lektion ab', + 'Fasse diese Lektion zusammen', + ], + 'pt-BR': [ + 'Explique este conceito de forma simples', + 'Quais são os pontos-chave?', + 'Me faça um quiz sobre esta lição', + 'Resuma esta lição', + ], + ru: [ + 'Объясни эту концепцию простыми словами', + 'Какие ключевые выводы?', + 'Проверь меня по этому уроку', + 'Кратко изложи этот урок', + ], + vi: [ + 'Giải thích khái niệm này một cách đơn giản', + 'Những điểm chính là gì?', + 'Đố tôi về bài học này', + 'Tóm tắt bài học này', + ], }; const A11Y_LABELS = {