Files
ClassFeedback/.claude/memory/temp/inject_prob_details.py

156 lines
6.7 KiB
Python

# -*- coding: utf-8 -*-
"""Inject a collapsible 题目讲解 panel (题目 / 孩子代码 / 答案 / 解析) into each
student's OJ section on the two CSP04 class pages (0830 & 1400). Additive only.
Usage: python inject_prob_details.py <page1.html> <page2.html> ...
"""
import sys, os, json, html, re, copy
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent.parent
TMP = ROOT / '.claude' / 'memory' / 'temp'
CODE = json.load(open(TMP / 'student_code_CSP04_L01.json', encoding='utf-8'))
META = json.load(open(TMP / 'problems_meta.json', encoding='utf-8'))
# display prefix -> numeric pid (as used in fetched codes)
PIDNUM = {v.split(' ')[0]: k for k, v in CODE['pid_map'].items()}
PROB_ORDER = ['S0401P1', 'S0401P2', 'S0401P3', 'S0401P4', 'S0401P5']
STATUS_TXT = {'AC': '✅ 已通过 AC', 'WA': '❌ 答案错误 WA', 'CE': '❌ 编译错误 CE',
'RE': '❌ 运行错误 RE', 'TLE': '❌ 超时 TLE', 'PE': '⚠️ 格式错误 PE', 'MLE': '❌ 超内存 MLE'}
CSS_BLOCK = '''
/* ===== 题目讲解(题目/孩子代码/答案/解析) ===== */
.oj-explains{margin-top:16px;}
.oj-explains-title{font-size:0.9em;font-weight:600;color:var(--ink);margin-bottom:8px;display:flex;align-items:center;gap:6px;}
.prob-detail{border:1px solid var(--border);border-radius:10px;margin-bottom:8px;overflow:hidden;background:var(--cream);}
.prob-head{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;font-size:0.85em;}
.prob-head:hover{background:var(--paper-warm);}
.prob-title{font-weight:600;color:var(--ink);}
.prob-sub{font-size:0.74em;color:var(--ink-soft);flex:1;text-align:right;}
.prob-caret{font-size:0.85em;color:var(--ink-soft);transition:transform 0.2s ease;}
.prob-detail.open .prob-caret{transform:rotate(90deg);}
.prob-body{display:none;padding:12px 14px;border-top:1px solid var(--border);}
.prob-detail.open .prob-body{display:block;}
.prob-sec{margin-bottom:12px;}
.prob-sec:last-child{margin-bottom:0;}
.prob-sec-h{font-size:0.78em;font-weight:700;color:var(--ink-soft);margin-bottom:5px;}
.prob-sec-c{font-size:0.83em;line-height:1.75;color:var(--ink-soft);background:var(--paper);border-radius:8px;padding:8px 11px;}
.prob-code{background:#1b2733;color:#e6eef7;border-radius:8px;padding:10px 12px;font-size:0.76em;line-height:1.6;overflow:auto;white-space:pre;max-height:240px;font-family:Consolas,'Courier New',monospace;}
/* 未提交 -> 课后作业(不展开、不做分析) */
.prob-detail.homework{background:var(--paper-warm);border-style:dashed;}
.prob-detail.homework .prob-head{cursor:default;}
.prob-detail.homework .prob-sub{color:#a8842c;font-weight:600;}
'''
JS_BLOCK = '\n function toggleProb(el){el.classList.toggle("open");var c=el.querySelector(".prob-caret");if(c)c.textContent=el.classList.contains("open")?"▼":"▶";}\n'
def esc(t):
return html.escape(str(t))
def build_panels(student):
panels = ['<div class="oj-explains">',
'<div class="oj-explains-title">📖 题目讲解(已提交的题 · 题目 / 孩子代码 / 参考答案 / 解析)</div>']
codes = CODE['students'].get(student, {})
for pid in PROB_ORDER:
meta = META.get(pid)
if not meta:
continue
entry = codes.get(PIDNUM.get(pid, ''), {})
code = entry.get('code', '')
status = entry.get('status', '')
attempts = entry.get('attempts', 0)
if code:
sub = '%s · %d次提交' % (STATUS_TXT.get(status, status), attempts)
panels.append(
'<div class="prob-detail" onclick="toggleProb(this)">'
'<div class="prob-head">'
'<span class="prob-title">%s</span>'
'<span class="prob-sub">%s</span>'
'<span class="prob-caret">▶</span>'
'</div>'
'<div class="prob-body">'
'<div class="prob-sec"><div class="prob-sec-h">📋 题目</div><div class="prob-sec-c">%s</div></div>'
'<div class="prob-sec"><div class="prob-sec-h">👦 孩子的代码</div><pre class="prob-code">%s</pre></div>'
'<div class="prob-sec"><div class="prob-sec-h">✅ 参考答案</div><pre class="prob-code">%s</pre></div>'
'<div class="prob-sec"><div class="prob-sec-h">💡 解析</div><div class="prob-sec-c">%s</div></div>'
'</div></div>' % (esc(meta['key']), sub, esc(meta['statement']), esc(code), esc(meta['ref_code']), esc(meta['key_points'])))
else:
# 未提交 -> 只标为课后作业,不做分析
panels.append(
'<div class="prob-detail homework">'
'<div class="prob-head">'
'<span class="prob-title">%s</span>'
'<span class="prob-sub">📚 未提交 · 课后作业</span>'
'</div></div>' % esc(meta['key']))
panels.append('</div>')
return '\n'.join(panels)
def find_oj_section(html, start):
"""Return end index (after the matching </div>) for the oj-section starting at `start`."""
n = len(html)
depth = 1
i = start + len('<div class="oj-section"')
while depth > 0 and i < n:
o = html.find('<div', i)
c = html.find('</div>', i)
if c == -1:
return -1
if o != -1 and o < c:
depth += 1
i = o + 4
else:
depth -= 1
i = c + 6
return i
def inject_style(html, css):
idx = html.rfind('</style>')
if idx == -1:
return html
return html[:idx] + css + html[idx:]
def inject_script(html, js):
idx = html.rfind('</script>')
if idx == -1:
return html
return html[:idx] + js + html[idx:]
def process(html):
# inject CSS/JS once
if '.prob-detail' not in html:
html = inject_style(html, CSS_BLOCK)
if 'function toggleProb' not in html:
html = inject_script(html, JS_BLOCK)
# inject panels per student oj-section (insert RIGHT-TO-LEFT so earlier
# positions stay valid after the html string changes length)
pat = re.compile(r'<div class="oj-section" data-oj-section="([^"]+)">')
matches = [(m.group(1), m.start()) for m in pat.finditer(html)]
for name, start in reversed(matches):
end = find_oj_section(html, start)
if end == -1:
continue
closing = end - 6 # position of the </div> that closes oj-section
panels = build_panels(name)
html = html[:closing] + panels + '\n' + html[closing:]
return html
def main(files):
for p in files:
orig = open(p, encoding='utf-8').read()
out = process(orig)
changed = out != orig
open(p, 'w', encoding='utf-8').write(out)
print(('CHANGED ' if changed else 'same ') + os.path.basename(p))
if __name__ == '__main__':
main(sys.argv[1:])