Skip to content

Commit 6dc27a0

Browse files
authored
Merge pull request #9 from zyh001/fix/quiz-ui-improvements
fix: quiz header overflow, LaTeX rendering, DeepSeek thinking, collap…
2 parents e84d326 + 3745751 commit 6dc27a0

5 files changed

Lines changed: 221 additions & 37 deletions

File tree

assets/static/quiz.css

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ input[type=range]::-webkit-slider-thumb:active{transform:scale(1.2)}
440440
background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;
441441
position:relative;
442442
}
443-
.quiz-progress-wrap{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:transparent}
443+
.quiz-progress-wrap{flex:1;min-width:72px;display:flex;flex-direction:column;gap:2px;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:transparent}
444444

445445
/* ── 极简刷题模式 ────────────────────────────────────────────── */
446446
/* 隐藏非必要控件 */
@@ -594,6 +594,12 @@ input[type=range]::-webkit-slider-thumb:active{transform:scale(1.2)}
594594
animation:lp-charge var(--lp-dur,600ms) linear forwards;
595595
}
596596
.quiz-timer.urgent{color:var(--danger);border-color:var(--danger);animation:pulse-timer 1s infinite}
597+
/* 小屏适配:计时器和按钮缩小,防止题号(如 120/230)被遮挡 */
598+
@media (max-width:400px){
599+
.quiz-timer{padding:4px 6px;font-size:12px;gap:2px}
600+
.quiz-timer>svg:first-child{display:none}
601+
.flag-btn{width:30px;height:30px;font-size:14px}
602+
}
597603
.flag-btn{
598604
width:34px;height:34px;border-radius:9px;background:var(--card);
599605
border:1px solid var(--border);color:var(--muted);font-size:16px;
@@ -838,6 +844,27 @@ input[type=range]::-webkit-slider-thumb:active{transform:scale(1.2)}
838844
padding:8px 16px;border-bottom:1px solid var(--border);background:var(--card);
839845
}
840846
.explain-correct-row span{color:var(--success);font-weight:600}
847+
/* ── 答案对比块 ── */
848+
.explain-ans-cmp{
849+
padding:10px 16px;border-bottom:1px solid var(--border);
850+
background:var(--card);display:flex;flex-direction:column;gap:7px;
851+
}
852+
.explain-ans-row{
853+
display:flex;align-items:center;gap:8px;flex-wrap:wrap;
854+
}
855+
.ans-row-label{
856+
font-size:12px;color:var(--muted);white-space:nowrap;min-width:52px;
857+
}
858+
.ans-badges{display:flex;gap:5px;flex-wrap:wrap;align-items:center}
859+
.ans-arrow{font-size:12px;color:var(--muted);margin:0 2px}
860+
.ans-badge{
861+
display:inline-flex;align-items:center;justify-content:center;
862+
min-width:26px;height:26px;padding:0 6px;border-radius:7px;
863+
font-size:13px;font-weight:700;line-height:1;
864+
}
865+
.ans-badge-ok {background:var(--success-bg);color:var(--success);border:1.5px solid var(--success)}
866+
.ans-badge-bad {background:var(--danger-bg);color:var(--danger);border:1.5px solid var(--danger)}
867+
.ans-badge-miss{background:var(--warn-bg);color:var(--warning);border:1.5px solid var(--warning);font-size:11px;min-width:auto}
841868
.explain-body{padding:14px 16px;font-size:var(--qfs-explain);line-height:1.85;color:var(--text)}
842869
.explain-point{
843870
margin-top:12px;padding:10px 14px;
@@ -1554,6 +1581,13 @@ button,a,[role=button],label{touch-action:manipulation}
15541581
color:#fff;box-shadow:0 3px 10px rgba(210,153,34,.4);
15551582
}
15561583
.gtm-btn.confirm:hover{opacity:.9}
1584+
.gtm-unanswered{
1585+
width:100%;padding:10px 14px;border-radius:10px;
1586+
background:rgba(220,53,69,.12);border:1px solid rgba(220,53,69,.35);
1587+
color:var(--danger);font-size:13px;font-weight:600;text-align:center;
1588+
display:flex;align-items:center;justify-content:center;gap:6px;
1589+
}
1590+
.gtm-unanswered::before{content:'⚠';font-size:15px}
15571591

15581592
/* ── 练习模式题目网格:对错颜色 ── */
15591593
.q-dot.correct {background:var(--success-bg);border-color:var(--success);color:var(--success)}

assets/static/quiz.js

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,10 +1832,31 @@ function examCanGoBack(fromIdx) {
18321832
return true;
18331833
}
18341834

1835-
/** 弹出题型切换确认框,cb 为用户点「确认」后的回调 */
1836-
function showGroupTransitionDialog(targetMode, cb) {
1835+
/** 统计某题型组内未作答题目数(考试模式用) */
1836+
function _countGroupUnanswered(gIdx) {
1837+
const g = S.modeGroups[gIdx];
1838+
if (!g) return 0;
1839+
let count = 0;
1840+
for (let i = g.startIdx; i <= g.endIdx; i++) {
1841+
const sel = S.ans[i];
1842+
if (sel === undefined || (sel instanceof Set && sel.size === 0)) count++;
1843+
}
1844+
return count;
1845+
}
1846+
1847+
/** 弹出题型切换确认框,unanswered=当前题型未答题数,cb 为用户点「确认」后的回调 */
1848+
function showGroupTransitionDialog(targetMode, unanswered, cb) {
18371849
S._groupModalCb = cb;
18381850
document.getElementById('gtm-mode').textContent = targetMode || '下一题型';
1851+
const warnEl = document.getElementById('gtm-unanswered');
1852+
if (warnEl) {
1853+
if (unanswered > 0) {
1854+
warnEl.textContent = `当前题型还有 ${unanswered} 道题未作答`;
1855+
warnEl.style.display = '';
1856+
} else {
1857+
warnEl.style.display = 'none';
1858+
}
1859+
}
18391860
document.getElementById('group-transition-modal').style.display = 'flex';
18401861
}
18411862
function confirmGroupModal() {
@@ -2021,7 +2042,7 @@ function startQuiz(remainingSeconds) {
20212042
fill.className = 'progress-fill';
20222043
document.getElementById('q-grid-panel').classList.remove('open');
20232044
buildGrid();
2024-
_showCalcBtn(false);
2045+
_showCalcBtn(true); // 练习模式也提供计算器
20252046
}
20262047

20272048
showScreen('s-quiz');
@@ -2393,7 +2414,7 @@ function selectOpt(letter, btn) {
23932414
}
23942415
if (nextGIdx !== curGIdx) {
23952416
const nextGroup = S.modeGroups[nextGIdx];
2396-
showGroupTransitionDialog(nextGroup.mode, () => {
2417+
showGroupTransitionDialog(nextGroup.mode, _countGroupUnanswered(curGIdx), () => {
23972418
S.currentGroupIdx = nextGIdx;
23982419
if (!nextGroup.allowBack) S.caseMaxReached[nextGIdx] = S.caseMaxReached[nextGIdx] ?? nextIdx;
23992420
S.cur = nextIdx; renderQ('forward'); updateGridDot();
@@ -2486,7 +2507,7 @@ function submitMulti() {
24862507
}
24872508
if (nextGIdx !== curGIdx) {
24882509
const nextGroup = S.modeGroups[nextGIdx];
2489-
showGroupTransitionDialog(nextGroup.mode, () => {
2510+
showGroupTransitionDialog(nextGroup.mode, _countGroupUnanswered(curGIdx), () => {
24902511
S.currentGroupIdx = nextGIdx;
24912512
if (!nextGroup.allowBack) S.caseMaxReached[nextGIdx] = S.caseMaxReached[nextGIdx] ?? nextIdx;
24922513
S.cur = nextIdx; renderQ('forward'); updateGridDot();
@@ -2545,15 +2566,103 @@ function buildExplain(q, selected) {
25452566
<span class="result-title ${isCorrect ? 'ok' : 'err'}">${isCorrect ? '回答正确!' : (isMulti ? '答案不完整或有误' : '回答错误')}</span>`;
25462567
inner.appendChild(resultRow);
25472568

2548-
if (!isCorrect || _zenMode) {
2549-
const corrRow = document.createElement('div');
2550-
corrRow.className = 'explain-correct-row';
2551-
if (isMulti) {
2552-
corrRow.innerHTML = `正确答案:<span class="multi-ans">${q.answer.split('').join(' ')}</span>`;
2553-
} else {
2554-
corrRow.innerHTML = `正确答案:<span>${q.answer}</span>`;
2569+
// ── 答案对比行 ────────────────────────────────────────────────────
2570+
// 多选题:始终显示(帮助识别少选/多选);单选题:答错时显示
2571+
{
2572+
const selSet = isMulti
2573+
? (selected instanceof Set ? selected : new Set())
2574+
: new Set(selected ? [selected] : []);
2575+
2576+
const showCmp = isMulti || !isCorrect || _zenMode;
2577+
if (showCmp) {
2578+
const cmpWrap = document.createElement('div');
2579+
cmpWrap.className = 'explain-ans-cmp';
2580+
2581+
if (isMulti) {
2582+
// 多选:两行对比 + 字母徽章着色
2583+
// 收集所有出现过的字母(用户选 + 正确),按字母序排列
2584+
const allLetters = [...new Set([...correctSet, ...selSet])].sort();
2585+
2586+
// — 你的答案行 —
2587+
const yourRow = document.createElement('div');
2588+
yourRow.className = 'explain-ans-row';
2589+
const yourLabel = document.createElement('span');
2590+
yourLabel.className = 'ans-row-label';
2591+
yourLabel.textContent = '你的答案';
2592+
yourRow.appendChild(yourLabel);
2593+
const yourBadges = document.createElement('span');
2594+
yourBadges.className = 'ans-badges';
2595+
if (selSet.size === 0) {
2596+
const dash = document.createElement('span');
2597+
dash.className = 'ans-badge ans-badge-miss';
2598+
dash.textContent = '未作答';
2599+
yourBadges.appendChild(dash);
2600+
} else {
2601+
// 只展示用户选的字母
2602+
[...selSet].sort().forEach(l => {
2603+
const b = document.createElement('span');
2604+
b.className = 'ans-badge ' + (correctSet.has(l) ? 'ans-badge-ok' : 'ans-badge-bad');
2605+
b.textContent = l;
2606+
yourBadges.appendChild(b);
2607+
});
2608+
// 漏选的字母补一个 miss 提示
2609+
[...correctSet].filter(l => !selSet.has(l)).sort().forEach(l => {
2610+
const b = document.createElement('span');
2611+
b.className = 'ans-badge ans-badge-miss';
2612+
b.textContent = l + ' 漏';
2613+
yourBadges.appendChild(b);
2614+
});
2615+
}
2616+
yourRow.appendChild(yourBadges);
2617+
cmpWrap.appendChild(yourRow);
2618+
2619+
// — 正确答案行 —
2620+
const corrRow2 = document.createElement('div');
2621+
corrRow2.className = 'explain-ans-row';
2622+
const corrLabel = document.createElement('span');
2623+
corrLabel.className = 'ans-row-label';
2624+
corrLabel.textContent = '正确答案';
2625+
corrRow2.appendChild(corrLabel);
2626+
const corrBadges = document.createElement('span');
2627+
corrBadges.className = 'ans-badges';
2628+
[...correctSet].sort().forEach(l => {
2629+
const b = document.createElement('span');
2630+
b.className = 'ans-badge ans-badge-ok';
2631+
b.textContent = l;
2632+
corrBadges.appendChild(b);
2633+
});
2634+
corrRow2.appendChild(corrBadges);
2635+
cmpWrap.appendChild(corrRow2);
2636+
2637+
} else {
2638+
// 单选:单行对比
2639+
const row = document.createElement('div');
2640+
row.className = 'explain-ans-row';
2641+
const lbl1 = document.createElement('span');
2642+
lbl1.className = 'ans-row-label';
2643+
lbl1.textContent = '你的答案';
2644+
row.appendChild(lbl1);
2645+
const yourB = document.createElement('span');
2646+
yourB.className = 'ans-badge ' + (!selected ? 'ans-badge-miss' : (!isCorrect ? 'ans-badge-bad' : 'ans-badge-ok'));
2647+
yourB.textContent = selected || '—';
2648+
row.appendChild(yourB);
2649+
const arrow = document.createElement('span');
2650+
arrow.className = 'ans-arrow';
2651+
arrow.textContent = '→';
2652+
row.appendChild(arrow);
2653+
const lbl2 = document.createElement('span');
2654+
lbl2.className = 'ans-row-label';
2655+
lbl2.textContent = '正确答案';
2656+
row.appendChild(lbl2);
2657+
const corrB = document.createElement('span');
2658+
corrB.className = 'ans-badge ans-badge-ok';
2659+
corrB.textContent = q.answer;
2660+
row.appendChild(corrB);
2661+
cmpWrap.appendChild(row);
2662+
}
2663+
2664+
inner.appendChild(cmpWrap);
25552665
}
2556-
inner.appendChild(corrRow);
25572666
}
25582667

25592668
// B型题:在解析前展示共享选项,高亮正确答案
@@ -2645,7 +2754,7 @@ function nextOrSubmit() {
26452754
if (nextGIdx !== curGIdx) {
26462755
// 跨题型组——弹窗提示
26472756
const nextGroup = S.modeGroups[nextGIdx];
2648-
showGroupTransitionDialog(nextGroup.mode, () => {
2757+
showGroupTransitionDialog(nextGroup.mode, _countGroupUnanswered(curGIdx), () => {
26492758
S.currentGroupIdx = nextGIdx;
26502759
// 案例分析组:记录最远到达位置
26512760
if (!nextGroup.allowBack) {
@@ -3171,7 +3280,7 @@ function buildGrid() {
31713280
if (!targetG.allowBack && i > (S.caseMaxReached[targetGIdx] ?? targetG.startIdx)) { toast('请按顺序作答案例分析题'); return; }
31723281
if (targetGIdx > curGIdx) {
31733282
const captured = i;
3174-
showGroupTransitionDialog(targetG.mode, () => {
3283+
showGroupTransitionDialog(targetG.mode, _countGroupUnanswered(curGIdx), () => {
31753284
S.currentGroupIdx = targetGIdx;
31763285
if (!targetG.allowBack) S.caseMaxReached[targetGIdx] = S.caseMaxReached[targetGIdx] ?? captured;
31773286
S.cur = captured; renderQ('forward'); updateGridDot();

assets/static/quiz_ai.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,8 @@ function makeStreamingRenderer(container, scrollTarget) {
499499
// 等 smd 流式阶段无法正确处理的元素,在这一步得到最终形态。
500500
try { container.innerHTML = markedRender(fullText); }
501501
catch (e) { /* 保留 smd 已渲染的结果 */ }
502+
// 补充渲染 \[...\] / \(...\) 格式的 LaTeX(AI 可能输出此类定界符)
503+
_renderMathInContainer(container);
502504
renderMermaidBlocks(container).catch(() => {});
503505
if (scrollTarget) scrollMessages(scrollTarget);
504506
}
@@ -831,7 +833,6 @@ function sendAIMessage(key) {
831833

832834
let fullReasoning = '';
833835
let hasReasoning = false;
834-
let thinkingCollapsed = false;
835836
let aborted = false;
836837
let truncated = false; // true when server signals finish_reason=length
837838
let contentRenderer = null; // streaming renderer for content
@@ -871,7 +872,7 @@ function sendAIMessage(key) {
871872
const obj = JSON.parse(data);
872873
if (obj.truncated) truncated = true;
873874
if (obj.content) { if (!contentRenderer) contentRenderer = makeStreamingRenderer(contentWrap, messages); contentRenderer.push(obj.content); fullRawText += obj.content; }
874-
if (obj.reasoning) { if (!reasoningRenderer) reasoningRenderer = makeStreamingRenderer(thinkingBody, messages); reasoningRenderer.push(obj.reasoning); fullReasoning += obj.reasoning; }
875+
if (obj.reasoning) { if (!reasoningRenderer) reasoningRenderer = makeStreamingRenderer(thinkingBody, messages); reasoningRenderer.push(obj.reasoning); fullReasoning += obj.reasoning; hasReasoning = true; }
875876
} catch(e) {}
876877
}
877878
}
@@ -906,7 +907,18 @@ function sendAIMessage(key) {
906907
hasReasoning = true;
907908
thinkingWrap.style.display = '';
908909
thinkingWrap.classList.add('ai-thinking-fadein');
910+
// 默认折叠:header 可见、body 隐藏,用户点击展开
911+
thinkingHeader.classList.add('ai-thinking-collapsed');
912+
thinkingBody.style.display = 'none';
909913
reasoningRenderer = makeStreamingRenderer(thinkingBody, messages);
914+
thinkingHeader.onclick = () => {
915+
const hidden = thinkingBody.style.display === 'none';
916+
thinkingBody.style.display = hidden ? '' : 'none';
917+
thinkingHeader.classList.toggle('ai-thinking-collapsed', !hidden);
918+
thinkingHeader.classList.toggle('ai-thinking-expanded', hidden);
919+
// 展开时重置暂停状态,让输出继续跟随滚动
920+
if (hidden) _resetScrollPause();
921+
};
910922
}
911923
reasoningRenderer.push(obj.reasoning);
912924
fullReasoning += obj.reasoning;
@@ -918,11 +930,6 @@ function sendAIMessage(key) {
918930

919931
// Handle main content — stream it paragraph by paragraph
920932
if (obj.content) {
921-
// If we had reasoning and now content starts, collapse thinking
922-
if (hasReasoning && !thinkingCollapsed) {
923-
thinkingCollapsed = true;
924-
collapseThinking(thinkingHeader, thinkingBody);
925-
}
926933
// Lazily create streaming renderer on first content chunk
927934
if (!contentRenderer) {
928935
contentRenderer = makeStreamingRenderer(contentWrap, messages);
@@ -954,7 +961,6 @@ function sendAIMessage(key) {
954961
state.streaming = false;
955962
msgEl.classList.remove('ai-typing');
956963

957-
// If reasoning was never collapsed (no content came after), collapse it now
958964
// Final flush of streaming renderers
959965
if (reasoningRenderer) {
960966
reasoningRenderer.flush();
@@ -964,10 +970,10 @@ function sendAIMessage(key) {
964970
contentRenderer.flush();
965971
contentRenderer = null;
966972
}
967-
// Collapse thinking after content is rendered
968-
if (hasReasoning && !thinkingCollapsed) {
969-
thinkingCollapsed = true;
970-
setTimeout(() => collapseThinking(thinkingHeader, thinkingBody), 50);
973+
// 思考完成:将 header 标签从“思考中…”改为“思考过程”
974+
if (hasReasoning) {
975+
const lbl = thinkingHeader.querySelector('.ai-thinking-label');
976+
if (lbl) lbl.textContent = '思考过程';
971977
}
972978

973979
// Save assistant response to history
@@ -1012,11 +1018,32 @@ function sendAIMessage(key) {
10121018
}
10131019
}
10141020

1021+
/**
1022+
* 在容器内补充渲染 \[...\] / \(...\) 格式的 LaTeX。
1023+
* marked 的自定义扩展只处理 $...$ / $$...$$,AI 有时输出 LaTeX 命令式定界符。
1024+
*/
1025+
function _renderMathInContainer(container) {
1026+
if (typeof renderMathInElement !== 'function') return;
1027+
try {
1028+
renderMathInElement(container, {
1029+
delimiters: [
1030+
{ left: '$$', right: '$$', display: true },
1031+
{ left: '$', right: '$', display: false },
1032+
{ left: '\\[', right: '\\]', display: true },
1033+
{ left: '\\(', right: '\\)', display: false },
1034+
],
1035+
ignoredTags: ['script','noscript','style','textarea','pre','code'],
1036+
throwOnError: false,
1037+
});
1038+
} catch(e) {}
1039+
}
1040+
10151041
/**
10161042
* Render markdown content into a container, optionally appending a cursor element.
10171043
*/
10181044
function renderContent(container, text, cursor) {
10191045
container.innerHTML = markedRender(text);
1046+
_renderMathInContainer(container);
10201047
// Post-render: replace mermaid code blocks with SVG diagrams
10211048
renderMermaidBlocks(container).catch(() => {});
10221049
if (cursor) container.appendChild(cursor);

assets/templates/quiz.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ <h2>题目解析</h2>
823823
下一题型为<strong id="gtm-mode"></strong><br>
824824
进入后<strong>不能返回</strong>当前题型,请确认已完成本题型所有作答。
825825
</div>
826+
<div class="gtm-unanswered" id="gtm-unanswered" style="display:none"></div>
826827
<div class="gtm-btns">
827828
<button class="gtm-btn cancel" onclick="dismissGroupModal()">暂不切换</button>
828829
<button class="gtm-btn confirm" onclick="confirmGroupModal()">确认进入 →</button>

0 commit comments

Comments
 (0)