diff --git a/.gitignore b/.gitignore
index 9f1082e..8bb7693 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,9 +18,17 @@ build/
*.db
*.sqlite
*.sqlite3
+*.log
+.superpowers/
tmp/
temp/
.cache/
# Local git worktrees used for parallel AI development.
.worktrees/
+
+# WeChat Mini Program — API key config (do not commit)
+wx-miniprogram/config.js
+
+# WeChat Mini Program — generated data bundle (rebuilt from content/)
+wx-miniprogram/data/all-chapters.js
diff --git a/app/fonts.css b/app/fonts.css
new file mode 100644
index 0000000..3afa196
--- /dev/null
+++ b/app/fonts.css
@@ -0,0 +1,218 @@
+/* 自托管字体 —— 由 scripts/fetch-fonts.mjs 生成,请勿手改。
+ 中文走本地宋体/思源回退(见 globals.css 字体栈)。 */
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/spectral-400-italic-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/spectral-400-italic-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/spectral-500-italic-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: italic;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/spectral-500-italic-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/spectral-400-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/spectral-400-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/spectral-500-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/spectral-500-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url('/fonts/spectral-600-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url('/fonts/spectral-600-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('/fonts/spectral-700-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('/fonts/spectral-700-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-400-italic-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-400-italic-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-400-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-400-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-500-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-500-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-600-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Source Serif 4';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url('/fonts/source-serif-4-600-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'IBM Plex Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/ibm-plex-mono-400-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'IBM Plex Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('/fonts/ibm-plex-mono-400-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'IBM Plex Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/ibm-plex-mono-500-normal-latin-ext.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'IBM Plex Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('/fonts/ibm-plex-mono-500-normal-latin.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/app/globals.css b/app/globals.css
index 678944d..d37b7d2 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -462,7 +462,9 @@ a {
/* 教学图示卡:像贴在书页上的插图 */
.diagram-grid {
display: grid;
- gap: 16px;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 16px 18px;
+ counter-reset: fig;
}
.diagram-card {
@@ -487,6 +489,14 @@ a {
color: var(--ink);
}
+/* 教科书式图号:图 1 / 图 2 … */
+.diagram-card figcaption strong::before {
+ counter-increment: fig;
+ content: "图 " counter(fig) " ";
+ color: var(--gold);
+ font-variant-numeric: lining-nums tabular-nums;
+}
+
.diagram-card svg {
width: 100%;
height: auto;
@@ -502,6 +512,50 @@ a {
font-style: italic;
}
+.diagram-card--concept {
+ background: var(--paper-page);
+ border-left: 3px solid var(--gold);
+ border-radius: 0 var(--radius) var(--radius) 0;
+}
+
+.diagram-card--concept .diagram-prompt {
+ margin-top: 14px;
+}
+
+.diagram-svg {
+ max-width: 560px;
+}
+
+.diagram-table-wrap {
+ margin-top: 12px;
+ overflow-x: auto;
+}
+
+.diagram-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-family: var(--serif-body);
+ font-size: 14px;
+ color: var(--ink);
+}
+
+.diagram-table th,
+.diagram-table td {
+ padding: 7px 12px;
+ border: 1px solid var(--rule);
+ text-align: left;
+}
+
+.diagram-table thead th {
+ background: var(--accent-wash);
+ color: var(--accent-deep);
+ font-weight: 600;
+}
+
+.diagram-table tbody tr:nth-child(even) td {
+ background: color-mix(in srgb, var(--paper-page) 70%, transparent);
+}
+
/* ----------------------------------------------------------------
练习区
----------------------------------------------------------------- */
@@ -558,6 +612,19 @@ button:disabled {
cursor: progress;
}
+button.is-primary {
+ background: var(--accent);
+ border-color: var(--accent-deep);
+ color: white;
+ font-weight: 600;
+}
+
+button.is-primary:hover:not(:disabled) {
+ background: var(--accent-deep);
+ border-color: var(--accent-deep);
+ color: white;
+}
+
textarea {
width: 100%;
min-height: 150px;
@@ -669,6 +736,20 @@ select:focus {
flex-wrap: wrap;
gap: 10px;
margin: 16px 0;
+ align-items: center;
+ padding: 12px 0;
+ border-top: 1px solid var(--rule);
+ border-bottom: 1px solid var(--rule);
+}
+
+.exercise-action-label {
+ width: 100%;
+ font-family: var(--serif-display);
+ font-size: 11.5px;
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--ink-faint);
}
.grading-error {
@@ -748,6 +829,28 @@ select:focus {
color: var(--gold);
}
+.mistake-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.mistake-list li {
+ position: relative;
+ margin: 8px 0;
+ padding-left: 20px;
+ line-height: 1.65;
+}
+
+.mistake-list li::before {
+ content: "✕";
+ position: absolute;
+ left: 0;
+ color: var(--accent);
+ font-size: 11px;
+ top: 4px;
+}
+
details {
margin-top: 14px;
border-top: 1px solid var(--rule);
@@ -995,7 +1098,7 @@ details summary {
min-width: 0;
display: flex;
flex-direction: column;
- gap: 4px;
+ gap: 5px;
}
.exercise-card-preview {
@@ -1008,6 +1111,18 @@ details summary {
white-space: nowrap;
}
+.exercise-card-title {
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.45;
+ color: var(--ink);
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: var(--serif-body);
+}
+
.difficulty-pill {
align-self: flex-start;
padding: 2px 8px;
@@ -1042,6 +1157,54 @@ details summary {
border-top: 1px solid var(--rule);
}
+.problem-statement {
+ margin: 0 0 20px;
+ padding: 18px 20px 16px;
+ background: var(--paper-page);
+ border: 1px solid var(--rule);
+ border-left: 3px solid var(--accent);
+ border-radius: 0 var(--radius) var(--radius) 0;
+ box-shadow: var(--shadow-page);
+}
+
+.problem-title {
+ margin: 0 0 10px;
+ font-family: var(--serif-display);
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--accent-deep);
+ opacity: 0.8;
+}
+
+.problem-body {
+ font-family: var(--serif-body);
+ font-size: 16px;
+ line-height: 1.85;
+ color: var(--ink);
+}
+
+.answer-label {
+ margin: 0 0 8px;
+ font-family: var(--serif-display);
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--ink-faint);
+}
+
+.exercise-num-label {
+ margin: 0 0 12px;
+ font-size: 11.5px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--accent);
+ font-family: var(--serif-display);
+ font-weight: 600;
+}
+
details summary:hover {
color: var(--accent-deep);
}
@@ -1112,7 +1275,10 @@ details ul {
margin: 0 0 10px;
}
-.chapter-header-kp-count {
+.chapter-header-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
font-size: 12px;
color: var(--ink-faint);
margin: 0;
@@ -1120,6 +1286,21 @@ details ul {
letter-spacing: 0.06em;
}
+.chapter-header-stats span {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.chapter-header-stats span::before {
+ content: "";
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--gold);
+ opacity: 0.8;
+}
+
/* ----------------------------------------------------------------
KP 导航:前后箭头
----------------------------------------------------------------- */
@@ -1193,6 +1374,13 @@ details ul {
margin: 0;
}
+.tutor-context {
+ margin: 5px 0 0;
+ color: var(--ink-soft);
+ font-size: 13.5px;
+ line-height: 1.45;
+}
+
.settings-toggle {
font-size: 12.5px;
padding: 5px 11px;
diff --git a/app/layout.tsx b/app/layout.tsx
index 96bdf9a..af09fbc 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import "katex/dist/katex.min.css";
+import "./fonts.css";
import "./globals.css";
export const metadata: Metadata = {
@@ -10,14 +11,6 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
-
-
-
-
diff --git a/components/course/ChapterHeader.tsx b/components/course/ChapterHeader.tsx
index 203d46d..e0688c4 100644
--- a/components/course/ChapterHeader.tsx
+++ b/components/course/ChapterHeader.tsx
@@ -1,3 +1,4 @@
+import { MathText } from "@/components/math/MathText";
import type { ChapterContent } from "@/lib/course/types";
const statusLabel: Record = {
@@ -7,6 +8,9 @@ const statusLabel: Record = {
};
export function ChapterHeader({ chapter }: { chapter: ChapterContent }) {
+ const diagramCount = new Set(chapter.knowledgePoints.flatMap((kp) => kp.diagramIds)).size;
+ const exerciseCount = new Set(chapter.knowledgePoints.flatMap((kp) => kp.exerciseIds)).size;
+
return (
@@ -14,10 +18,12 @@ export function ChapterHeader({ chapter }: { chapter: ChapterContent }) {
{statusLabel[chapter.status] ?? chapter.status}
{chapter.title}
- {chapter.overview && {chapter.overview}
}
-
- {chapter.knowledgePoints.length} 个知识点
-
+ {chapter.overview &&
}
+
+ {chapter.knowledgePoints.length} 个知识点
+ {diagramCount > 0 && {diagramCount} 张教学图示}
+ {exerciseCount > 0 && {exerciseCount} 道练习}
+
);
}
diff --git a/components/course/ExercisePanel.tsx b/components/course/ExercisePanel.tsx
index 0574698..e8ff9c1 100644
--- a/components/course/ExercisePanel.tsx
+++ b/components/course/ExercisePanel.tsx
@@ -33,13 +33,14 @@ function difficultyClass(d: string) {
return d === "basic" ? "basic" : d === "medium" ? "medium" : "advanced";
}
-function previewText(text: string, max = 72): string {
- const stripped = text
- .replace(/\$\$[\s\S]*?\$\$/g, "[式]")
- .replace(/\$[^$\n]+\$/g, "[式]")
+function cardLabel(exercise: Exercise): string {
+ if (exercise.title) return exercise.title;
+ const stripped = exercise.prompt
+ .replace(/\$\$[\s\S]*?\$\$/g, "")
+ .replace(/\$[^$\n]+\$/g, "")
.replace(/\n+/g, " ")
.trim();
- return stripped.length > max ? stripped.slice(0, max) + "…" : stripped;
+ return stripped.slice(0, 64) + (stripped.length > 64 ? "…" : "");
}
export function ExercisePanel({
@@ -66,6 +67,7 @@ export function ExercisePanel({
const [gradeResult, setGradeResult] = useState(null);
const [masteryState, setMasteryState] = useState("not_started");
const attachmentsRef = useRef([]);
+ const rubricRef = useRef(null);
const activeExercise = selectedIndex != null ? exercises[selectedIndex] : undefined;
const visibleHints = useMemo(() => {
@@ -77,6 +79,13 @@ export function ExercisePanel({
attachmentsRef.current = attachments;
}, [attachments]);
+ useEffect(() => {
+ setSelectedIndex(exercises.length > 0 ? 0 : null);
+ resetAnswerState();
+ // Reset when the learning context changes; resetAnswerState is a local helper.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [knowledgePoint.id, exercises.length]);
+
useEffect(() => {
const progress = loadCourseProgress(courseId, volumeId);
const record = getKnowledgePointProgress(progress, knowledgePoint.id);
@@ -193,7 +202,7 @@ export function ExercisePanel({
练习
{exercises.length > 0 ? `${exercises.length} 题` : "暂无练习"}
- {getMasteryLabel(masteryState)}
+ 当前掌握状态 · {getMasteryLabel(masteryState)}
{exercises.length === 0 ? (
@@ -212,21 +221,28 @@ export function ExercisePanel({
>
{index + 1}
+
{cardLabel(exercise)}
{difficultyLabel(exercise.difficulty)}
-
{previewText(exercise.prompt)}
))}
)}
- {activeExercise && (
+ {activeExercise && selectedIndex != null && (
-
-
-
+
第 {selectedIndex + 1} 题 · 共 {exercises.length} 题
+
+ {activeExercise.title && (
+
{activeExercise.title}
+ )}
+
+
+
+
+
解答
)}
-
{gradingError && {gradingError}
}
{gradeResult && (
@@ -281,9 +302,9 @@ export function ExercisePanel({
AI 批改结果
{gradeResult.score} 分
-
+
-
+
{gradeResult.missingKeyPoints.length > 0 && (
还缺这些要点
@@ -307,7 +328,7 @@ export function ExercisePanel({
))}
)}
-
+
评分要点
{activeExercise.gradingRubric.map((item) => (
diff --git a/components/course/LearningStepPanel.tsx b/components/course/LearningStepPanel.tsx
index 9b69ab1..548215b 100644
--- a/components/course/LearningStepPanel.tsx
+++ b/components/course/LearningStepPanel.tsx
@@ -61,7 +61,7 @@ export function LearningStepPanel({
{diagrams.length > 0 && (
-
+
{diagrams.map((diagram) => (
@@ -81,6 +81,18 @@ export function LearningStepPanel({
)}
+
+ {knowledgePoint.commonMistakes.length > 0 && (
+
+
+ {knowledgePoint.commonMistakes.map((mistake) => (
+ -
+
+
+ ))}
+
+
+ )}
);
}
diff --git a/components/course/TutorPanel.tsx b/components/course/TutorPanel.tsx
index 9bb5f73..96d0916 100644
--- a/components/course/TutorPanel.tsx
+++ b/components/course/TutorPanel.tsx
@@ -96,7 +96,10 @@ export function TutorPanel({ knowledgePoint }: { knowledgePoint: KnowledgePoint
return (