Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions scripts/auditFormulas.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 公式审计:扫描全部 10 章课程数据里的 $$/$ 公式,用 KaTeX 严格解析找出渲染失败项
import katex from 'katex'
import { NLP_LESSONS } from '../src/data/ai/chapters/nlp.js'
import { CV_LESSONS } from '../src/data/ai/chapters/cv.js'
import { RL_LESSONS } from '../src/data/ai/chapters/rl.js'
import { LLM_LESSONS } from '../src/data/ai/chapters/llm.js'
import { OPTIM_LESSONS } from '../src/data/ai/chapters/optim.js'
import { ML_LESSONS } from '../src/data/ai/chapters/ml.js'
import { DL_LESSONS } from '../src/data/ai/chapters/dl.js'
import { OR_LESSONS } from '../src/data/ai/chapters/or.js'
import { FEATURE_LESSONS } from '../src/data/ai/chapters/feature.js'
import { INFO_THEORY_LESSONS } from '../src/data/ai/chapters/it.js'

const groups = {
nlp: NLP_LESSONS, cv: CV_LESSONS, rl: RL_LESSONS, llm: LLM_LESSONS,
optim: OPTIM_LESSONS, ml: ML_LESSONS, dl: DL_LESSONS, or: OR_LESSONS,
feature: FEATURE_LESSONS, it: INFO_THEORY_LESSONS,
}
let bad = 0

for (const [name, lessons] of Object.entries(groups)) {
for (const lesson of lessons) {
const theory = lesson.theory || ''
const formulas = []
for (const m of theory.matchAll(/\$\$([\s\S]+?)\$\$/g)) formulas.push({ tex: m[1], mode: 'block' })
const noBlocks = theory.replace(/\$\$[\s\S]+?\$\$/g, '')
for (const m of noBlocks.matchAll(/\$([^$\n]+?)\$/g)) formulas.push({ tex: m[1], mode: 'inline' })

for (const f of formulas) {
try {
katex.renderToString(f.tex, { displayMode: f.mode === 'block', throwOnError: true, strict: false })
} catch (err) {
bad++
console.log(`[${name}] ${lesson.id} (${f.mode}):`)
console.log(` TEX: ${f.tex.slice(0, 120)}`)
console.log(` ERR: ${String(err.message).slice(0, 160)}`)
}
}
}
}
console.log(bad === 0 ? 'ALL FORMULAS OK' : `${bad} broken formulas found`)
15 changes: 13 additions & 2 deletions src/components/StepController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,15 @@ export default function StepController({
}) {
const scrubberRef = useRef(null)
const isDragging = useRef(false)
// 拖拽期间挂在 window 上的监听器的卸载函数;组件若在拖拽中途 unmount,
// 由下面的 useEffect cleanup 兜底移除,避免监听器泄漏。
const detachDragListeners = useRef(null)
const isPhone = useIsPhone()

useEffect(() => {
return () => { detachDragListeners.current?.() }
}, [])

// 活跃实例登记:挂载即认领;卸载时若仍是自己则释放
const idRef = useRef(null)
if (!idRef.current) idRef.current = Symbol('step-controller')
Expand Down Expand Up @@ -102,14 +109,18 @@ export default function StepController({
seek(posToStep(e.clientX))
window.addEventListener('mousemove', handleScrubberMouseMove)
window.addEventListener('mouseup', handleScrubberMouseUp)
detachDragListeners.current = () => {
window.removeEventListener('mousemove', handleScrubberMouseMove)
window.removeEventListener('mouseup', handleScrubberMouseUp)
detachDragListeners.current = null
}
}
function handleScrubberMouseMove(e) {
if (isDragging.current) seek(posToStep(e.clientX))
}
function handleScrubberMouseUp() {
isDragging.current = false
window.removeEventListener('mousemove', handleScrubberMouseMove)
window.removeEventListener('mouseup', handleScrubberMouseUp)
detachDragListeners.current?.()
}
function handleScrubberTouch(e) {
seek(posToStep(e.touches[0].clientX))
Expand Down
11 changes: 8 additions & 3 deletions src/components/learning/AlgorithmTabs.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lazy, Suspense, useEffect, useState, useCallback, memo } from 'react'
import ErrorBoundary from '../ErrorBoundary'
import { Prose } from './Section'
import CodeBlock from './CodeBlock'
import ComplexityCards from './ComplexityCards'
Expand Down Expand Up @@ -157,9 +158,13 @@ const AlgorithmTabs = memo(function AlgorithmTabs({ algo }) {

{active === 'notes' && (
<Panel id="notes" title={null}>
<Suspense fallback={<div className="text-sm text-[var(--text-tertiary)]">正在加载笔记...</div>}>
<Notes slug={algo.slug} />
</Suspense>
<ErrorBoundary fallback={
<div className="text-sm text-fg-muted">笔记模块加载失败,请刷新页面重试。</div>
}>
<Suspense fallback={<div className="text-sm text-fg-faint">正在加载笔记...</div>}>
<Notes slug={algo.slug} />
</Suspense>
</ErrorBoundary>
</Panel>
)}
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/contexts/ProgressContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ export function ProgressProvider({ children }) {
return () => { cancelled = true }
}, [userId, store])

// 页面隐藏 / 卸载时立即冲刷防抖队列,避免刚产生的进度变更丢在 pending 里。
// pagehide 在移动端比 beforeunload 更可靠;flush 是 best-effort,失败也不影响
// 本地副本(下次登录 syncWithRemote 会整体合并推回)。
useEffect(() => {
if (!userId) return
const onPageHide = () => { sync.flushNow() }
window.addEventListener('pagehide', onPageHide)
return () => window.removeEventListener('pagehide', onPageHide)
}, [userId])

// 登录态下订阅 realtime
useEffect(() => {
if (!userId) return
Expand Down
Loading