From fd2a5f7ddf239ca8fc773965605051b33220031c Mon Sep 17 00:00:00 2001 From: fly1d <309400591+fly1d@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:56:49 +0800 Subject: [PATCH 1/2] feat: add flashcards and quick quiz --- README.md | 1 + app.js | 133 +++++++++++++++++++++++++++++++++++++++++++- styles.css | 41 ++++++++++++++ test/smoke.test.mjs | 16 ++++++ 4 files changed, 190 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc82e35..6ea674d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - 模糊名词先澄清含义,再按所选方向学习。 - 公开网页提取标题、摘要、章节与正文要点。 - 围绕当前主题继续询问用法、示例和误区;百科主题优先依据已取得的来源摘录回答。 +- 使用闪卡复习关键概念,并通过一道即时反馈的小测验巩固理解。 - 浮窗可以隐藏为右下角入口并随时恢复。 ## 运行 diff --git a/app.js b/app.js index 2110f36..7ab0f58 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,8 @@ const state = { current: null, lastInput: "", - asking: false + asking: false, + study: { mode: null, cardIndex: 0, cardRevealed: false, quizAnswer: null } }; const $ = (selector, root = document) => root.querySelector(selector); @@ -34,6 +35,7 @@ function showView(name) { function reset() { state.current = null; state.lastInput = ""; + resetStudy(); $("#mainInput").value = ""; $("#conversation").innerHTML = ""; showView("start"); @@ -47,6 +49,7 @@ async function analyze(input) { return; } state.lastInput = value; + resetStudy(); $("#loadingTopic").textContent = value; showView("loading"); try { @@ -89,6 +92,11 @@ function renderSummary(result) { ? [["请用简单的话解释", "简单解释"], ["再讲详细一点", "再讲详细"], ["还有什么重要背景?", "重要背景"]] : [["如何使用?", "如何使用?"], ["给我一个简单的例子", "给我例子"], ["有什么常见误区?", "常见误区"]]; const quickQuestions = questions.map(([question, label]) => ``).join(""); + const cardCount = validCards(result).length; + const studyActions = [ + cardCount ? `` : "", + validQuiz(result) ? `` : "" + ].filter(Boolean).join(""); $("#conversation").innerHTML = `
@@ -100,11 +108,106 @@ function renderSummary(result) {
${quickQuestions}
+ ${studyActions ? `
${studyActions}
` : ""}
`; showView("conversation"); icons(); } +function resetStudy() { + state.study = { mode: null, cardIndex: 0, cardRevealed: false, quizAnswer: null }; +} + +function validCards(result = state.current) { + return Array.isArray(result?.cards) + ? result.cards + .filter((card) => Array.isArray(card) && typeof card[0] === "string" && card[0].trim() && typeof card[1] === "string" && card[1].trim()) + .map(([question, answer]) => [question.trim().slice(0, 300), answer.trim().slice(0, 600)]) + .slice(0, 12) + : []; +} + +function validQuiz(result = state.current) { + const quiz = result?.quiz; + const answer = quiz?.answer; + if (typeof quiz?.question !== "string" || !quiz.question.trim() || !Array.isArray(quiz.options)) return null; + if (quiz.options.length < 2 || quiz.options.length > 6 || quiz.options.some((option) => typeof option !== "string" || !option.trim())) return null; + const options = quiz.options.map((option) => option.trim()); + if (!Number.isInteger(answer) || answer < 0 || answer >= options.length) return null; + return { + question: quiz.question.trim().slice(0, 400), + options: options.map((option) => option.slice(0, 240)), + answer, + explanation: typeof quiz.explanation === "string" ? quiz.explanation.slice(0, 600) : "" + }; +} + +function showStudy(mode) { + if (mode === "cards" && !validCards().length) return; + if (mode === "quiz" && !validQuiz()) return; + state.study = { mode, cardIndex: 0, cardRevealed: false, quizAnswer: null }; + renderStudyPanel(); +} + +function renderStudyPanel() { + $("#studyPanel")?.remove(); + if (!state.study.mode) return; + + const conversation = $("#conversation"); + if (state.study.mode === "cards") { + const cards = validCards(); + const index = Math.min(state.study.cardIndex, cards.length - 1); + const [question, answer] = cards[index]; + const revealed = state.study.cardRevealed; + conversation.insertAdjacentHTML("beforeend", ` +
+
+
闪卡${index + 1} / ${cards.length}
+ +
+
+ 问题 +

${escapeHtml(question)}

+
+ ${revealed ? `答案

${escapeHtml(answer)}

` : ``} +
+
+ +
`); + } else { + const quiz = validQuiz(); + const answered = state.study.quizAnswer !== null; + const selected = state.study.quizAnswer; + const options = quiz.options.map((option, index) => { + const isCorrect = answered && index === quiz.answer; + const isWrong = answered && index === selected && index !== quiz.answer; + const status = isCorrect ? `` : isWrong ? `` : ""; + return ``; + }).join(""); + const correct = selected === quiz.answer; + conversation.insertAdjacentHTML("beforeend", ` +
+
+
测一测1 题
+ +
+
+ 选择题 +

${escapeHtml(quiz.question)}

+
${options}
+ ${answered ? `
${correct ? "回答正确" : "再想一步"}${quiz.explanation ? `

${escapeHtml(quiz.explanation)}

` : ""}
` : ""} +
+ ${answered ? `` : ""} +
`); + } + icons(); + scrollToBottom(); +} + async function ask(question) { const value = question.trim(); if (!value || !state.current || state.asking) return; @@ -157,9 +260,37 @@ document.addEventListener("click", (event) => { const example = event.target.closest("[data-example]"); const clarify = event.target.closest("[data-clarify]"); const question = event.target.closest("[data-question]"); + const study = event.target.closest("[data-study]"); + const studyClose = event.target.closest("[data-study-close]"); + const cardReveal = event.target.closest("[data-card-reveal]"); + const cardNav = event.target.closest("[data-card-nav]"); + const quizOption = event.target.closest("[data-quiz-option]"); + const quizReset = event.target.closest("[data-quiz-reset]"); if (example) analyze(example.dataset.example); if (clarify) analyze(clarify.dataset.clarify); if (question) ask(question.dataset.question); + if (study) showStudy(study.dataset.study); + if (studyClose) { + resetStudy(); + $("#studyPanel")?.remove(); + } + if (cardReveal) { + state.study.cardRevealed = !state.study.cardRevealed; + renderStudyPanel(); + } + if (cardNav && !cardNav.disabled) { + state.study.cardIndex += Number(cardNav.dataset.cardNav); + state.study.cardRevealed = false; + renderStudyPanel(); + } + if (quizOption && state.study.quizAnswer === null) { + state.study.quizAnswer = Number(quizOption.dataset.quizOption); + renderStudyPanel(); + } + if (quizReset) { + state.study.quizAnswer = null; + renderStudyPanel(); + } }); $("#newButton").addEventListener("click", reset); diff --git a/styles.css b/styles.css index d132fdb..287b0f4 100644 --- a/styles.css +++ b/styles.css @@ -118,6 +118,47 @@ input::placeholder { color: #9ca29b; } .quick-questions { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 2px; } .quick-questions button { height: 29px; padding: 0 10px; border: 1px solid #d7dbd4; border-radius: 15px; color: #666c66; background: white; font-size: 9px; } .quick-questions button:hover { border-color: var(--blue); color: var(--blue); } +.study-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; padding-top: 4px; } +.study-action { min-width: 0; height: 36px; display: grid; grid-template-columns: 18px minmax(0, 1fr) 18px; align-items: center; gap: 6px; padding: 0 9px; border: 1px solid var(--line); border-radius: 6px; color: #555b55; background: var(--surface-2); font-size: 10px; text-align: left; } +.study-action:hover { border-color: var(--blue); color: var(--blue); background: white; } +.study-action svg { width: 14px; } +.study-action small { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 4px; color: #59655a; background: #e2e6df; font-size: 8px; } +.study-panel { min-width: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 7px; background: white; } +.study-panel-head { height: 45px; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 10px 0 13px; border-bottom: 1px solid var(--line); } +.study-panel-head > div { min-width: 0; display: flex; align-items: center; gap: 7px; } +.study-panel-head > div > svg { width: 14px; color: var(--blue); } +.study-panel-head strong { font-size: 11px; } +.study-panel-head small { color: var(--muted); font-size: 8px; } +.study-icon-button { width: 30px; height: 30px; flex: 0 0 auto; display: grid; place-items: center; padding: 0; border: 0; border-radius: 5px; color: #747a73; background: transparent; } +.study-icon-button:hover:not(:disabled) { color: var(--ink); background: var(--surface-2); } +.study-icon-button:disabled { cursor: default; opacity: .3; } +.study-icon-button svg { width: 14px; } +.flashcard-content, .quiz-content { min-width: 0; padding: 17px 15px; } +.flashcard-content > span, .quiz-content > span, .flashcard-answer > span { display: block; margin-bottom: 7px; color: var(--blue); font-size: 8px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; } +.flashcard-content h2, .quiz-content h2 { margin: 0; overflow-wrap: anywhere; font-size: 14px; line-height: 1.55; } +.flashcard-answer { min-height: 90px; display: grid; place-content: center; margin-top: 15px; padding-top: 14px; border-top: 1px solid var(--line); color: #9aa099; } +.flashcard-answer.is-visible { display: block; color: inherit; } +.flashcard-answer > svg { width: 19px; } +.flashcard-answer p { margin: 0; overflow-wrap: anywhere; color: #555b55; font-size: 10px; line-height: 1.7; } +.study-controls { height: 47px; display: grid; grid-template-columns: 30px minmax(0, 1fr) 30px; align-items: center; justify-items: center; gap: 8px; padding: 0 11px; border-top: 1px solid var(--line); } +.study-controls-single { grid-template-columns: 1fr; } +.reveal-button { height: 29px; padding: 0 12px; border: 1px solid #cfd4cc; border-radius: 5px; color: #525852; background: white; font-size: 9px; } +.reveal-button:hover { border-color: var(--blue); color: var(--blue); } +.quiz-options { display: grid; gap: 7px; margin-top: 14px; } +.quiz-option { min-width: 0; min-height: 42px; display: grid; grid-template-columns: 24px minmax(0, 1fr) 18px; align-items: center; gap: 8px; padding: 7px 9px; border: 1px solid var(--line); border-radius: 6px; color: #555b55; background: white; text-align: left; } +.quiz-option:not(:disabled):hover { border-color: var(--blue); color: var(--blue); } +.quiz-option > span { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 4px; color: #4f5750; background: var(--surface-2); font-size: 8px; font-weight: 700; } +.quiz-option strong { min-width: 0; overflow-wrap: anywhere; font-size: 10px; font-weight: 500; line-height: 1.45; } +.quiz-option small { width: 18px; height: 18px; display: grid; place-items: center; } +.quiz-option small svg { width: 13px; } +.quiz-option.is-correct { border-color: #80b38b; color: #397047; background: #eff8f1; } +.quiz-option.is-wrong { border-color: #e59a8d; color: #a54f41; background: #fff3f0; } +.quiz-option:disabled { cursor: default; } +.quiz-feedback { margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--line); } +.quiz-feedback strong { color: #3f7850; font-size: 10px; } +.quiz-feedback.is-wrong strong { color: #ad5546; } +.quiz-feedback p { margin: 5px 0 0; overflow-wrap: anywhere; color: #606660; font-size: 9px; line-height: 1.6; } +.study-action:focus-visible, .study-icon-button:focus-visible, .reveal-button:focus-visible, .quiz-option:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; } .user-message { justify-self: end; max-width: 82%; padding: 9px 12px; border-radius: 7px 7px 2px 7px; color: white; background: var(--ink); font-size: 11px; line-height: 1.55; } .follow-answer { display: grid; gap: 8px; padding: 13px 14px; border: 1px solid var(--line); border-radius: 7px; background: white; } .follow-answer strong { font-size: 12px; } diff --git a/test/smoke.test.mjs b/test/smoke.test.mjs index 038fda8..02e07cd 100644 --- a/test/smoke.test.mjs +++ b/test/smoke.test.mjs @@ -60,6 +60,22 @@ test("analyzes a precise term", async () => { assert.ok(result.summary.length > 20); assert.ok(result.concepts.length >= 3); assert.ok(result.steps.length >= 3); + assert.equal(result.cards.length, 3); + assert.equal(result.quiz.options.length, 4); + assert.ok(result.quiz.answer >= 0 && result.quiz.answer < result.quiz.options.length); +}); + +test("serves the flashcard and quiz interactions", async () => { + const [scriptResponse, styleResponse] = await Promise.all([ + fetch(`${baseUrl}/app.js`), + fetch(`${baseUrl}/styles.css`) + ]); + assert.equal(scriptResponse.status, 200); + assert.equal(styleResponse.status, 200); + const [script, styles] = await Promise.all([scriptResponse.text(), styleResponse.text()]); + assert.match(script, /data-card-reveal/); + assert.match(script, /aria-label="打开闪卡,共/); + assert.match(styles, /\.quiz-option\.is-correct/); }); test("asks for clarification when a term is ambiguous", async () => { From 5bfd8d7a0d20e89f46cc0e7e2cd9d63e8f255310 Mon Sep 17 00:00:00 2001 From: fly1d <309400591+fly1d@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:33:27 +0800 Subject: [PATCH 2/2] fix: stabilize study label spacing --- styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.css b/styles.css index 287b0f4..dcae8f5 100644 --- a/styles.css +++ b/styles.css @@ -134,7 +134,7 @@ input::placeholder { color: #9ca29b; } .study-icon-button:disabled { cursor: default; opacity: .3; } .study-icon-button svg { width: 14px; } .flashcard-content, .quiz-content { min-width: 0; padding: 17px 15px; } -.flashcard-content > span, .quiz-content > span, .flashcard-answer > span { display: block; margin-bottom: 7px; color: var(--blue); font-size: 8px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; } +.flashcard-content > span, .quiz-content > span, .flashcard-answer > span { display: block; margin-bottom: 7px; color: var(--blue); font-size: 8px; font-weight: 700; letter-spacing: 0; text-transform: uppercase; } .flashcard-content h2, .quiz-content h2 { margin: 0; overflow-wrap: anywhere; font-size: 14px; line-height: 1.55; } .flashcard-answer { min-height: 90px; display: grid; place-content: center; margin-top: 15px; padding-top: 14px; border-top: 1px solid var(--line); color: #9aa099; } .flashcard-answer.is-visible { display: block; color: inherit; }