hydro-ai-helper 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/handlers/adminConfigHandler.js +30 -0
- package/dist/handlers/adminConfigHandler.js.map +1 -1
- package/dist/handlers/batchSummaryHandler.js +15 -8
- package/dist/handlers/batchSummaryHandler.js.map +1 -1
- package/dist/handlers/studentHandler.js +1 -1
- package/dist/handlers/studentHandler.js.map +1 -1
- package/dist/handlers/teachingSummaryHandler.js +38 -9
- package/dist/handlers/teachingSummaryHandler.js.map +1 -1
- package/dist/models/aiConfig.js +15 -2
- package/dist/models/aiConfig.js.map +1 -1
- package/dist/models/studentSummary.js +13 -0
- package/dist/models/studentSummary.js.map +1 -1
- package/dist/models/teachingSummary.js +2 -0
- package/dist/models/teachingSummary.js.map +1 -1
- package/dist/services/analyzers/errorClusterAnalyzer.js +1 -0
- package/dist/services/analyzers/errorClusterAnalyzer.js.map +1 -1
- package/dist/services/analyzers/findingConsolidator.js +172 -0
- package/dist/services/analyzers/findingConsolidator.js.map +1 -0
- package/dist/services/batchSummaryService.js +79 -38
- package/dist/services/batchSummaryService.js.map +1 -1
- package/dist/services/openaiClient.js +59 -28
- package/dist/services/openaiClient.js.map +1 -1
- package/dist/services/promptService.js +49 -24
- package/dist/services/promptService.js.map +1 -1
- package/dist/services/teachingAnalysisService.js +13 -2
- package/dist/services/teachingAnalysisService.js.map +1 -1
- package/dist/services/teachingSuggestionService.js +43 -44
- package/dist/services/teachingSuggestionService.js.map +1 -1
- package/frontend/admin/BudgetConfigForm.tsx +1 -1
- package/frontend/admin/ConfigPanel.tsx +53 -3
- package/frontend/admin/EndpointManager.tsx +1 -1
- package/frontend/admin/FeedbackForm.tsx +1 -1
- package/frontend/admin/JailbreakLogsViewer.tsx +1 -1
- package/frontend/admin/ScenarioModelSelector.tsx +238 -0
- package/frontend/admin/TelemetrySettings.tsx +1 -1
- package/frontend/admin/VersionBadge.tsx +1 -1
- package/frontend/admin/configTypes.ts +9 -0
- package/frontend/batchSummary/BatchSummaryPanel.tsx +29 -1
- package/frontend/batchSummary/StudentSummaryView.tsx +47 -14
- package/frontend/batchSummary/SummaryCard.tsx +1 -1
- package/frontend/batchSummary/useBatchSummary.ts +26 -8
- package/frontend/components/AIHelperDashboard.tsx +1 -1
- package/frontend/components/ErrorBoundary.tsx +1 -1
- package/frontend/components/ScoreboardTabContainer.tsx +1 -1
- package/frontend/generated/localeFallback.ts +1502 -0
- package/frontend/problem_detail.page.tsx +1 -1
- package/frontend/student/AIAssistantPanel.tsx +1 -1
- package/frontend/student/ChatInput.tsx +1 -1
- package/frontend/student/ChatMessageList.tsx +1 -1
- package/frontend/student/ThinkingBlock.tsx +1 -1
- package/frontend/student/hooks/useChatSession.ts +1 -1
- package/frontend/teacher/AnalyticsPage.tsx +1 -1
- package/frontend/teacher/ClassAnalyticsTable.tsx +1 -1
- package/frontend/teacher/ConversationDetail.tsx +1 -1
- package/frontend/teacher/ConversationDetailModal.tsx +1 -1
- package/frontend/teacher/ConversationList.tsx +1 -1
- package/frontend/teacher/CostDashboard.tsx +1 -1
- package/frontend/teacher/ExportDialog.tsx +1 -1
- package/frontend/teacher/MetricsPanel.tsx +1 -1
- package/frontend/teacher/ProblemAnalyticsTable.tsx +1 -1
- package/frontend/teacher/StudentAnalyticsTable.tsx +1 -1
- package/frontend/teacher/analyticsTypes.ts +1 -1
- package/frontend/teachingSummary/TeachingReviewPanel.tsx +1 -1
- package/frontend/teachingSummary/TeachingSummaryPanel.tsx +525 -175
- package/frontend/teachingSummary/useTeachingSummary.ts +10 -0
- package/frontend/utils/i18n.ts +32 -0
- package/frontend/utils/i18nFallbackCore.ts +30 -0
- package/locales/en.yaml +30 -6
- package/locales/zh.yaml +30 -6
- package/package.json +3 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TeachingSummaryPanel — teacher-facing UI for AI teaching summary generation.
|
|
3
3
|
* Injects into homework/contest scoreboard pages for whole-class analysis.
|
|
4
|
+
*
|
|
5
|
+
* 完成态按教师备课工作流组织为四段:
|
|
6
|
+
* ① 一句话诊断 → ② 下节课回顾清单(含个体干预)→ ③ 学生问题(重点卡片 + 其他观察)
|
|
7
|
+
* → ④ 课后强化训练(可一键复制下发)
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
7
|
-
import { i18n } from '
|
|
11
|
+
import { i18n } from '../utils/i18n';
|
|
8
12
|
import {
|
|
9
13
|
COLORS, SPACING, RADIUS, SHADOWS, TRANSITIONS,
|
|
10
14
|
getButtonStyle, cardStyle, markdownTheme, LAYOUT,
|
|
@@ -23,17 +27,26 @@ const I18N_FALLBACK: Record<string, string> = {
|
|
|
23
27
|
ai_helper_teaching_summary_focus_placeholder: '可选:输入教学重点(如"递归理解"或"复杂度优化")',
|
|
24
28
|
ai_helper_teaching_summary_snapshot_notice: '数据快照时间:',
|
|
25
29
|
ai_helper_teaching_summary_participated: '参与学生',
|
|
26
|
-
|
|
30
|
+
ai_helper_teaching_summary_primary_findings: '重点问题',
|
|
27
31
|
ai_helper_teaching_summary_high_priority: '高优先级',
|
|
28
32
|
ai_helper_teaching_summary_ai_users: 'AI 使用者',
|
|
33
|
+
ai_helper_teaching_summary_completed_all: '完成全部题目',
|
|
29
34
|
ai_helper_teaching_summary_low_data_warning: '参与学生不足 10 人,分析结果仅供参考。',
|
|
30
|
-
|
|
35
|
+
ai_helper_teaching_summary_section_diagnosis: '一句话诊断',
|
|
36
|
+
ai_helper_teaching_summary_section_review: '下节课回顾清单',
|
|
37
|
+
ai_helper_teaching_summary_section_problems: '学生问题',
|
|
38
|
+
ai_helper_teaching_summary_section_homework: '课后强化训练',
|
|
39
|
+
ai_helper_teaching_summary_section_full_report: 'AI 完整报告',
|
|
40
|
+
ai_helper_teaching_summary_other_observations: '其他观察',
|
|
31
41
|
ai_helper_teaching_summary_no_findings: '未发现明显问题',
|
|
32
42
|
ai_helper_teaching_summary_overall_suggestion: 'AI 综合建议',
|
|
33
|
-
ai_helper_teaching_summary_expand: '展开',
|
|
34
|
-
ai_helper_teaching_summary_collapse: '收起',
|
|
35
43
|
ai_helper_teaching_summary_affected: '涉及',
|
|
36
44
|
ai_helper_teaching_summary_students: '名学生',
|
|
45
|
+
ai_helper_teaching_summary_student_list: '涉及学生',
|
|
46
|
+
ai_helper_teaching_summary_copy_homework: '复制作业',
|
|
47
|
+
ai_helper_teaching_summary_copied: '已复制',
|
|
48
|
+
ai_helper_teaching_summary_copy_failed: '复制失败,请手动选择文本复制',
|
|
49
|
+
ai_helper_teaching_summary_homework_hint: '可直接复制下发("建议挖空点说明"为教师参考答案,请勿下发)',
|
|
37
50
|
ai_helper_teaching_summary_feedback_helpful: '有帮助',
|
|
38
51
|
ai_helper_teaching_summary_feedback_not_helpful: '没帮助',
|
|
39
52
|
ai_helper_teaching_summary_feedback_thanks: '感谢反馈!',
|
|
@@ -73,11 +86,6 @@ const METRIC_LABELS: Record<string, string> = {
|
|
|
73
86
|
passRate: '通过率',
|
|
74
87
|
attempted: '尝试人数',
|
|
75
88
|
accepted: '通过人数',
|
|
76
|
-
affectedCount: '受影响人数',
|
|
77
|
-
totalStudents: '总学生数',
|
|
78
|
-
percentage: '占比',
|
|
79
|
-
atRiskCount: '高危人数',
|
|
80
|
-
completedCount: '完成人数',
|
|
81
89
|
comprehensionPct: '理解类提问占比',
|
|
82
90
|
aiUserCount: 'AI 使用人数',
|
|
83
91
|
nonAiUserCount: '未使用 AI 人数',
|
|
@@ -90,10 +98,7 @@ const METRIC_LABELS: Record<string, string> = {
|
|
|
90
98
|
aiPassRate: 'AI 用户通过率',
|
|
91
99
|
nonAiPassRate: '非 AI 通过率',
|
|
92
100
|
diff: '差异',
|
|
93
|
-
|
|
94
|
-
stuck_silent: '沉默挣扎',
|
|
95
|
-
persistent_learner: '持续努力',
|
|
96
|
-
disengaged: '未参与',
|
|
101
|
+
sameSignatureCount: '同一错误位学生数',
|
|
97
102
|
aiGroupSize: 'AI组人数',
|
|
98
103
|
nonAiGroupSize: '非AI组人数',
|
|
99
104
|
aiACRate: 'AI组通过率',
|
|
@@ -102,12 +107,84 @@ const METRIC_LABELS: Record<string, string> = {
|
|
|
102
107
|
dominantClusterPct: '主要错误集群占比',
|
|
103
108
|
};
|
|
104
109
|
|
|
110
|
+
/** 这些指标与卡片标题/"涉及 N 名学生"徽标重复,展开时不再显示 */
|
|
111
|
+
const REDUNDANT_METRICS = new Set([
|
|
112
|
+
'affectedCount', 'totalStudents', 'percentage',
|
|
113
|
+
'atRiskCount', 'completedCount',
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
/** 百分比类指标,展示时补 % 号 */
|
|
117
|
+
const PERCENT_METRIC = /rate|pct|percentage/i;
|
|
118
|
+
|
|
105
119
|
const SEVERITY_COLORS = {
|
|
106
120
|
high: { bg: '#fef2f2', text: '#b91c1c', border: '#fecaca' },
|
|
107
121
|
medium: { bg: '#fffbeb', text: '#92400e', border: '#fde68a' },
|
|
108
122
|
low: { bg: '#f0fdf4', text: '#166534', border: '#bbf7d0' },
|
|
109
123
|
};
|
|
110
124
|
|
|
125
|
+
/** 学生名单默认展示上限,超出折叠为 +N */
|
|
126
|
+
const MAX_VISIBLE_NAMES = 24;
|
|
127
|
+
|
|
128
|
+
// ─── overallSuggestion 分段解析 ───────────────────────────────────────────────
|
|
129
|
+
//
|
|
130
|
+
// 主报告是一段 markdown,按 "### " 标题切块归类;识别失败的块进入 rest,
|
|
131
|
+
// 由"AI 完整报告"折叠区兜底(兼容旧版含 P0 报告/作业的长文档)。
|
|
132
|
+
|
|
133
|
+
export interface ParsedSuggestion {
|
|
134
|
+
diagnosis?: string;
|
|
135
|
+
reviewList?: string;
|
|
136
|
+
p1?: string;
|
|
137
|
+
homework?: string;
|
|
138
|
+
rest: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseSuggestionSections(md: string): ParsedSuggestion {
|
|
142
|
+
const result: ParsedSuggestion = { rest: [] };
|
|
143
|
+
if (!md || !md.trim()) return result;
|
|
144
|
+
|
|
145
|
+
const chunks = md.split(/(?=^###\s)/m).filter(c => c.trim());
|
|
146
|
+
for (const chunk of chunks) {
|
|
147
|
+
const firstLine = chunk.split('\n', 1)[0];
|
|
148
|
+
const body = chunk.replace(/^###[^\n]*\n?/, '').trim();
|
|
149
|
+
if (/一句话诊断/.test(firstLine) && !result.diagnosis) {
|
|
150
|
+
result.diagnosis = body;
|
|
151
|
+
} else if (/下节课回顾清单/.test(firstLine) && !result.reviewList) {
|
|
152
|
+
result.reviewList = body;
|
|
153
|
+
} else if (/个体干预/.test(firstLine) && !result.p1) {
|
|
154
|
+
result.p1 = body;
|
|
155
|
+
} else if (/课后巩固|挖空练习/.test(firstLine) && !result.homework) {
|
|
156
|
+
result.homework = chunk.trim();
|
|
157
|
+
} else {
|
|
158
|
+
result.rest.push(chunk.trim());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── 剪贴板 ───────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function copyText(text: string): Promise<boolean> {
|
|
167
|
+
try {
|
|
168
|
+
if (navigator.clipboard?.writeText) {
|
|
169
|
+
await navigator.clipboard.writeText(text);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
} catch { /* fall through to legacy path */ }
|
|
173
|
+
try {
|
|
174
|
+
const ta = document.createElement('textarea');
|
|
175
|
+
ta.value = text;
|
|
176
|
+
ta.style.position = 'fixed';
|
|
177
|
+
ta.style.opacity = '0';
|
|
178
|
+
document.body.appendChild(ta);
|
|
179
|
+
ta.select();
|
|
180
|
+
const ok = document.execCommand('copy');
|
|
181
|
+
document.body.removeChild(ta);
|
|
182
|
+
return ok;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
111
188
|
// ─── SkeletonBlock subcomponent ───────────────────────────────────────────────
|
|
112
189
|
|
|
113
190
|
const SkeletonBlock: React.FC<{ lines?: number }> = ({ lines = 8 }) => (
|
|
@@ -131,19 +208,86 @@ const SkeletonBlock: React.FC<{ lines?: number }> = ({ lines = 8 }) => (
|
|
|
131
208
|
</div>
|
|
132
209
|
);
|
|
133
210
|
|
|
211
|
+
// ─── Section shell ────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
interface SectionCardProps {
|
|
214
|
+
icon: string;
|
|
215
|
+
title: string;
|
|
216
|
+
accentColor?: string;
|
|
217
|
+
headerRight?: React.ReactNode;
|
|
218
|
+
children: React.ReactNode;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const SectionCard: React.FC<SectionCardProps> = ({
|
|
222
|
+
icon, title, accentColor = COLORS.border, headerRight, children,
|
|
223
|
+
}) => (
|
|
224
|
+
<div style={{
|
|
225
|
+
border: `1px solid ${COLORS.border}`,
|
|
226
|
+
borderLeft: `4px solid ${accentColor}`,
|
|
227
|
+
borderRadius: RADIUS.md,
|
|
228
|
+
backgroundColor: COLORS.bgCard,
|
|
229
|
+
boxShadow: SHADOWS.sm,
|
|
230
|
+
marginBottom: SPACING.lg,
|
|
231
|
+
overflow: 'hidden',
|
|
232
|
+
}}>
|
|
233
|
+
<div style={{
|
|
234
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
235
|
+
gap: SPACING.sm,
|
|
236
|
+
padding: `${SPACING.md} ${SPACING.base}`,
|
|
237
|
+
borderBottom: `1px solid ${COLORS.border}`,
|
|
238
|
+
backgroundColor: COLORS.bgPage,
|
|
239
|
+
}}>
|
|
240
|
+
<span style={{
|
|
241
|
+
fontWeight: 600, fontSize: '13px', color: COLORS.textSecondary,
|
|
242
|
+
letterSpacing: '0.05em',
|
|
243
|
+
}}>
|
|
244
|
+
{icon} {title}
|
|
245
|
+
</span>
|
|
246
|
+
{headerRight}
|
|
247
|
+
</div>
|
|
248
|
+
<div style={{ padding: `${SPACING.md} ${SPACING.base}` }}>
|
|
249
|
+
{children}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
|
|
134
254
|
// ─── FindingCard subcomponent ─────────────────────────────────────────────────
|
|
135
255
|
|
|
136
256
|
interface FindingCardProps {
|
|
137
257
|
finding: TeachingFinding;
|
|
138
258
|
deepDiveText?: string;
|
|
259
|
+
studentNames?: Record<string, string>;
|
|
139
260
|
}
|
|
140
261
|
|
|
141
|
-
const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText }) => {
|
|
262
|
+
const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText, studentNames }) => {
|
|
142
263
|
const [expanded, setExpanded] = useState(false);
|
|
264
|
+
const [showAllNames, setShowAllNames] = useState(false);
|
|
143
265
|
const colors = SEVERITY_COLORS[finding.severity] || SEVERITY_COLORS.low;
|
|
144
266
|
const dimensionLabel = DIMENSION_LABELS[finding.dimension] || finding.dimension;
|
|
145
267
|
const affectedCount = finding.evidence.affectedStudents.length;
|
|
146
268
|
|
|
269
|
+
const names = (finding.evidence.affectedStudents || [])
|
|
270
|
+
.map(uid => studentNames?.[String(uid)])
|
|
271
|
+
.filter(Boolean) as string[];
|
|
272
|
+
|
|
273
|
+
const extraMetrics = Object.entries(finding.evidence.metrics || {})
|
|
274
|
+
.filter(([key]) => !REDUNDANT_METRICS.has(key));
|
|
275
|
+
|
|
276
|
+
const codeSample = (finding.dimension === 'commonError' || finding.dimension === 'errorCluster')
|
|
277
|
+
? finding.evidence.samples?.code?.[0]
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
280
|
+
// 展开必须有实际内容;否则整行不可点,避免"展开什么也没有"
|
|
281
|
+
const hasDetails = Boolean(
|
|
282
|
+
(finding.supplements && finding.supplements.length > 0)
|
|
283
|
+
|| deepDiveText
|
|
284
|
+
|| codeSample
|
|
285
|
+
|| names.length > 0
|
|
286
|
+
|| extraMetrics.length > 0,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const visibleNames = showAllNames ? names : names.slice(0, MAX_VISIBLE_NAMES);
|
|
290
|
+
|
|
147
291
|
return (
|
|
148
292
|
<div style={{
|
|
149
293
|
backgroundColor: COLORS.bgCard,
|
|
@@ -155,11 +299,11 @@ const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText }) => {
|
|
|
155
299
|
transition: 'box-shadow 200ms ease',
|
|
156
300
|
}}>
|
|
157
301
|
<div
|
|
158
|
-
onClick={() => setExpanded(!expanded)}
|
|
302
|
+
onClick={hasDetails ? () => setExpanded(!expanded) : undefined}
|
|
159
303
|
style={{
|
|
160
304
|
display: 'flex', alignItems: 'center', gap: SPACING.sm,
|
|
161
305
|
padding: `${SPACING.md} ${SPACING.base}`,
|
|
162
|
-
cursor: 'pointer', userSelect: 'none',
|
|
306
|
+
cursor: hasDetails ? 'pointer' : 'default', userSelect: 'none',
|
|
163
307
|
}}
|
|
164
308
|
>
|
|
165
309
|
<span style={{
|
|
@@ -204,47 +348,43 @@ const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText }) => {
|
|
|
204
348
|
{t('ai_helper_teaching_summary_affected')} {affectedCount} {t('ai_helper_teaching_summary_students')}
|
|
205
349
|
</span>
|
|
206
350
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
351
|
+
{hasDetails && (
|
|
352
|
+
<span style={{
|
|
353
|
+
display: 'inline-block', width: '16px', height: '16px', flexShrink: 0,
|
|
354
|
+
textAlign: 'center', lineHeight: '16px', fontSize: '12px',
|
|
355
|
+
color: COLORS.textMuted,
|
|
356
|
+
transition: 'transform 200ms ease',
|
|
357
|
+
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
358
|
+
}}>▶</span>
|
|
359
|
+
)}
|
|
214
360
|
</div>
|
|
215
361
|
|
|
216
|
-
{expanded && (
|
|
362
|
+
{expanded && hasDetails && (
|
|
217
363
|
<div style={{
|
|
218
364
|
padding: `0 ${SPACING.base} ${SPACING.base}`,
|
|
219
365
|
borderTop: `1px solid ${COLORS.border}`,
|
|
220
366
|
paddingTop: SPACING.md,
|
|
221
367
|
}}>
|
|
222
|
-
{
|
|
368
|
+
{/* 关联洞察(合并进来的交叉关联/错误聚类补充) */}
|
|
369
|
+
{finding.supplements && finding.supplements.length > 0 && (
|
|
223
370
|
<div style={{
|
|
224
|
-
display: 'flex', flexWrap: 'wrap', gap: SPACING.sm,
|
|
225
371
|
marginBottom: SPACING.md,
|
|
226
372
|
padding: `${SPACING.sm} ${SPACING.md}`,
|
|
227
373
|
backgroundColor: COLORS.bgPage,
|
|
374
|
+
borderLeft: `3px solid ${COLORS.textMuted}`,
|
|
228
375
|
borderRadius: RADIUS.sm,
|
|
229
376
|
}}>
|
|
230
|
-
{
|
|
231
|
-
<
|
|
232
|
-
fontSize: '
|
|
377
|
+
{finding.supplements.map((s, i) => (
|
|
378
|
+
<div key={i} style={{
|
|
379
|
+
fontSize: '13px', color: COLORS.textSecondary, lineHeight: 1.6,
|
|
233
380
|
}}>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
</span>
|
|
381
|
+
↳ {s}
|
|
382
|
+
</div>
|
|
237
383
|
))}
|
|
238
384
|
</div>
|
|
239
385
|
)}
|
|
240
386
|
|
|
241
|
-
{
|
|
242
|
-
<div style={{ fontSize: '13px', color: COLORS.textSecondary, marginBottom: SPACING.md, lineHeight: 1.6 }}>
|
|
243
|
-
<strong>{DIMENSION_LABELS[finding.dimension] || finding.dimension}:</strong>
|
|
244
|
-
{finding.aiSuggestion}
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
|
|
387
|
+
{/* 深度诊断(根因 + 反例 + 课堂提问) */}
|
|
248
388
|
{deepDiveText && (
|
|
249
389
|
<div
|
|
250
390
|
className="markdown-body"
|
|
@@ -253,8 +393,8 @@ const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText }) => {
|
|
|
253
393
|
/>
|
|
254
394
|
)}
|
|
255
395
|
|
|
256
|
-
{
|
|
257
|
-
|
|
396
|
+
{/* 典型错误代码 */}
|
|
397
|
+
{codeSample && (
|
|
258
398
|
<div style={{ marginTop: SPACING.md }}>
|
|
259
399
|
<div style={{
|
|
260
400
|
fontSize: '12px', fontWeight: 600, color: COLORS.textSecondary,
|
|
@@ -269,10 +409,66 @@ const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText }) => {
|
|
|
269
409
|
color: '#e2e8f0', lineHeight: 1.6,
|
|
270
410
|
fontFamily: "'SFMono-Regular', 'Menlo', 'Consolas', monospace",
|
|
271
411
|
}}>
|
|
272
|
-
{
|
|
412
|
+
{codeSample}
|
|
273
413
|
</pre>
|
|
274
414
|
</div>
|
|
275
415
|
)}
|
|
416
|
+
|
|
417
|
+
{/* 涉及学生名单 — 教师可据此点名辅导 */}
|
|
418
|
+
{names.length > 0 && (
|
|
419
|
+
<div style={{ marginTop: SPACING.md }}>
|
|
420
|
+
<div style={{
|
|
421
|
+
fontSize: '12px', fontWeight: 600, color: COLORS.textSecondary,
|
|
422
|
+
marginBottom: SPACING.sm, letterSpacing: '0.02em',
|
|
423
|
+
}}>
|
|
424
|
+
{t('ai_helper_teaching_summary_student_list')}({names.length}):
|
|
425
|
+
</div>
|
|
426
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
|
427
|
+
{visibleNames.map((name, i) => (
|
|
428
|
+
<span key={i} style={{
|
|
429
|
+
fontSize: '12px', padding: '2px 8px',
|
|
430
|
+
backgroundColor: COLORS.bgPage,
|
|
431
|
+
border: `1px solid ${COLORS.border}`,
|
|
432
|
+
borderRadius: RADIUS.full,
|
|
433
|
+
color: COLORS.textSecondary,
|
|
434
|
+
}}>
|
|
435
|
+
{name}
|
|
436
|
+
</span>
|
|
437
|
+
))}
|
|
438
|
+
{names.length > MAX_VISIBLE_NAMES && !showAllNames && (
|
|
439
|
+
<span
|
|
440
|
+
onClick={() => setShowAllNames(true)}
|
|
441
|
+
style={{
|
|
442
|
+
fontSize: '12px', padding: '2px 8px',
|
|
443
|
+
borderRadius: RADIUS.full,
|
|
444
|
+
color: COLORS.primary, cursor: 'pointer',
|
|
445
|
+
border: `1px dashed ${COLORS.primary}`,
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
448
|
+
+{names.length - MAX_VISIBLE_NAMES}
|
|
449
|
+
</span>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{/* 其余非冗余指标 */}
|
|
456
|
+
{extraMetrics.length > 0 && (
|
|
457
|
+
<div style={{
|
|
458
|
+
display: 'flex', flexWrap: 'wrap', gap: SPACING.sm,
|
|
459
|
+
marginTop: SPACING.md,
|
|
460
|
+
}}>
|
|
461
|
+
{extraMetrics.map(([key, val]) => (
|
|
462
|
+
<span key={key} style={{ fontSize: '12px', color: COLORS.textMuted }}>
|
|
463
|
+
{METRIC_LABELS[key] || key}:{' '}
|
|
464
|
+
<strong style={{ color: COLORS.textSecondary }}>
|
|
465
|
+
{typeof val === 'number' ? (val % 1 === 0 ? val : val.toFixed(2)) : val}
|
|
466
|
+
{typeof val === 'number' && PERCENT_METRIC.test(key) ? '%' : ''}
|
|
467
|
+
</strong>
|
|
468
|
+
</span>
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
276
472
|
</div>
|
|
277
473
|
)}
|
|
278
474
|
</div>
|
|
@@ -281,29 +477,25 @@ const FindingCard: React.FC<FindingCardProps> = ({ finding, deepDiveText }) => {
|
|
|
281
477
|
|
|
282
478
|
// ─── Overview bar ─────────────────────────────────────────────────────────────
|
|
283
479
|
|
|
284
|
-
interface
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
480
|
+
interface OverviewItem {
|
|
481
|
+
label: string;
|
|
482
|
+
value: React.ReactNode;
|
|
483
|
+
highlight?: boolean;
|
|
484
|
+
positive?: boolean;
|
|
288
485
|
}
|
|
289
486
|
|
|
290
|
-
const OverviewBar: React.FC<
|
|
487
|
+
const OverviewBar: React.FC<{ items: OverviewItem[] }> = ({ items }) => (
|
|
291
488
|
<div style={{
|
|
292
489
|
display: 'flex', flexWrap: 'wrap', gap: SPACING.lg,
|
|
293
490
|
marginBottom: SPACING.base,
|
|
294
491
|
padding: `${SPACING.sm} 0`,
|
|
295
492
|
}}>
|
|
296
|
-
{
|
|
297
|
-
{ label: t('ai_helper_teaching_summary_participated'), value: stats.participatedStudents },
|
|
298
|
-
{ label: t('ai_helper_teaching_summary_findings'), value: findingsCount },
|
|
299
|
-
{ label: t('ai_helper_teaching_summary_high_priority'), value: highCount, highlight: highCount > 0 },
|
|
300
|
-
{ label: t('ai_helper_teaching_summary_ai_users'), value: stats.aiUserCount },
|
|
301
|
-
].map(({ label, value, highlight }) => (
|
|
493
|
+
{items.map(({ label, value, highlight, positive }) => (
|
|
302
494
|
<span key={label} style={{ display: 'inline-flex', alignItems: 'baseline', gap: '4px' }}>
|
|
303
495
|
<span style={{ fontSize: '12px', color: COLORS.textMuted }}>{label}</span>
|
|
304
496
|
<span style={{
|
|
305
497
|
fontSize: '18px', fontWeight: 700,
|
|
306
|
-
color: highlight ? COLORS.error : COLORS.textPrimary,
|
|
498
|
+
color: highlight ? COLORS.error : positive ? COLORS.hydroGreenDark : COLORS.textPrimary,
|
|
307
499
|
}}>
|
|
308
500
|
{value}
|
|
309
501
|
</span>
|
|
@@ -326,16 +518,20 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
326
518
|
|
|
327
519
|
const [teachingFocus, setTeachingFocus] = useState('');
|
|
328
520
|
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
|
|
329
|
-
const [
|
|
521
|
+
const [fullReportCollapsed, setFullReportCollapsed] = useState(true);
|
|
522
|
+
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>('idle');
|
|
330
523
|
|
|
331
524
|
useEffect(() => {
|
|
332
525
|
fetchSummary();
|
|
333
526
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
334
527
|
|
|
335
|
-
// Report
|
|
528
|
+
// Report primary problem count to parent for tab badge
|
|
336
529
|
useEffect(() => {
|
|
337
530
|
if (onStatsUpdate && summary?.findings) {
|
|
338
|
-
|
|
531
|
+
const primaryCount = summary.findings.filter(
|
|
532
|
+
f => !f.isSecondary && f.dimension !== 'progress',
|
|
533
|
+
).length;
|
|
534
|
+
onStatsUpdate(primaryCount);
|
|
339
535
|
}
|
|
340
536
|
}, [summary?.findings?.length]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
341
537
|
|
|
@@ -468,14 +664,98 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
468
664
|
// ── Completed state ───────────────────────────────────────────────────────
|
|
469
665
|
|
|
470
666
|
const findings = summary.findings || [];
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
667
|
+
const progressFinding = findings.find(f => f.dimension === 'progress');
|
|
668
|
+
const problemFindings = findings.filter(f => f.dimension !== 'progress');
|
|
669
|
+
|
|
670
|
+
// 后端已按严重度+影响面排序并标记次要发现;对旧文档做同样的客户端兜底限量
|
|
671
|
+
const flaggedPrimary = problemFindings.filter(f => !f.isSecondary);
|
|
672
|
+
const primaryFindings = flaggedPrimary.slice(0, 5);
|
|
673
|
+
const secondaryFindings = [
|
|
674
|
+
...flaggedPrimary.slice(5),
|
|
675
|
+
...problemFindings.filter(f => f.isSecondary),
|
|
676
|
+
];
|
|
677
|
+
|
|
678
|
+
const highCount = primaryFindings.filter(f => f.severity === 'high').length;
|
|
679
|
+
|
|
680
|
+
const parsed = parseSuggestionSections(summary.overallSuggestion || '');
|
|
681
|
+
const parsedOk = Boolean(parsed.diagnosis || parsed.reviewList);
|
|
682
|
+
const homeworkMd = (summary.homeworkText && summary.homeworkText.trim())
|
|
683
|
+
? summary.homeworkText
|
|
684
|
+
: parsed.homework;
|
|
474
685
|
|
|
475
686
|
const snapshotDate = summary.dataSnapshotAt
|
|
476
687
|
? new Date(summary.dataSnapshotAt).toLocaleString('zh-CN', { hour12: false })
|
|
477
688
|
: '';
|
|
478
689
|
|
|
690
|
+
const handleCopyHomework = async () => {
|
|
691
|
+
if (!homeworkMd) return;
|
|
692
|
+
const ok = await copyText(homeworkMd);
|
|
693
|
+
setCopyState(ok ? 'copied' : 'failed');
|
|
694
|
+
setTimeout(() => setCopyState('idle'), 2000);
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const overviewItems: OverviewItem[] = [
|
|
698
|
+
{
|
|
699
|
+
label: t('ai_helper_teaching_summary_participated'),
|
|
700
|
+
value: `${summary.stats.participatedStudents}/${summary.stats.totalStudents}`,
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
label: t('ai_helper_teaching_summary_primary_findings'),
|
|
704
|
+
value: primaryFindings.length,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
label: t('ai_helper_teaching_summary_high_priority'),
|
|
708
|
+
value: highCount,
|
|
709
|
+
highlight: highCount > 0,
|
|
710
|
+
},
|
|
711
|
+
...(progressFinding ? [{
|
|
712
|
+
label: t('ai_helper_teaching_summary_completed_all'),
|
|
713
|
+
value: `${progressFinding.evidence.affectedStudents.length}${
|
|
714
|
+
typeof progressFinding.evidence.metrics?.percentage === 'number'
|
|
715
|
+
? ` (${progressFinding.evidence.metrics.percentage}%)` : ''
|
|
716
|
+
}`,
|
|
717
|
+
positive: true,
|
|
718
|
+
}] : []),
|
|
719
|
+
{
|
|
720
|
+
label: t('ai_helper_teaching_summary_ai_users'),
|
|
721
|
+
value: summary.stats.aiUserCount,
|
|
722
|
+
},
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
const feedbackRow = (
|
|
726
|
+
<div style={{
|
|
727
|
+
display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
728
|
+
gap: SPACING.sm, padding: `${SPACING.sm} 0`,
|
|
729
|
+
}}>
|
|
730
|
+
{feedbackSubmitted ? (
|
|
731
|
+
<span style={{ fontSize: '12px', color: COLORS.successText }}>
|
|
732
|
+
{t('ai_helper_teaching_summary_feedback_thanks')}
|
|
733
|
+
</span>
|
|
734
|
+
) : (
|
|
735
|
+
<>
|
|
736
|
+
{(['up', 'down'] as const).map(rating => (
|
|
737
|
+
<button
|
|
738
|
+
key={rating}
|
|
739
|
+
onClick={() => handleFeedback(rating)}
|
|
740
|
+
style={{
|
|
741
|
+
fontSize: '12px', padding: '3px 8px',
|
|
742
|
+
border: `1px solid ${COLORS.border}`,
|
|
743
|
+
borderRadius: RADIUS.sm,
|
|
744
|
+
backgroundColor: 'transparent',
|
|
745
|
+
color: summary.feedback?.rating === rating
|
|
746
|
+
? (rating === 'up' ? COLORS.successText : COLORS.errorText)
|
|
747
|
+
: COLORS.textMuted,
|
|
748
|
+
cursor: 'pointer',
|
|
749
|
+
}}
|
|
750
|
+
>
|
|
751
|
+
{rating === 'up' ? t('ai_helper_teaching_summary_feedback_helpful') : t('ai_helper_teaching_summary_feedback_not_helpful')}
|
|
752
|
+
</button>
|
|
753
|
+
))}
|
|
754
|
+
</>
|
|
755
|
+
)}
|
|
756
|
+
</div>
|
|
757
|
+
);
|
|
758
|
+
|
|
479
759
|
return (
|
|
480
760
|
<div style={{ fontFamily: 'inherit', color: COLORS.textPrimary, maxWidth: LAYOUT.contentMaxWidth, margin: '0 auto', width: '100%' }}>
|
|
481
761
|
<style>{markdownTheme}</style>
|
|
@@ -519,11 +799,7 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
519
799
|
)}
|
|
520
800
|
|
|
521
801
|
{/* Overview bar */}
|
|
522
|
-
<OverviewBar
|
|
523
|
-
stats={summary.stats}
|
|
524
|
-
findingsCount={findings.length}
|
|
525
|
-
highCount={highFindings.length}
|
|
526
|
-
/>
|
|
802
|
+
<OverviewBar items={overviewItems} />
|
|
527
803
|
|
|
528
804
|
{/* Low-data warning */}
|
|
529
805
|
{summary.stats.participatedStudents < 10 && (
|
|
@@ -539,128 +815,202 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
539
815
|
</div>
|
|
540
816
|
)}
|
|
541
817
|
|
|
542
|
-
{/*
|
|
543
|
-
|
|
544
|
-
{
|
|
545
|
-
|
|
818
|
+
{/* ① 一句话诊断 — 30 秒抓住重点 */}
|
|
819
|
+
{parsed.diagnosis && (
|
|
820
|
+
<div style={{
|
|
821
|
+
padding: `${SPACING.base} ${SPACING.lg}`,
|
|
822
|
+
marginBottom: SPACING.lg,
|
|
823
|
+
backgroundColor: COLORS.bgCard,
|
|
824
|
+
border: `1px solid ${COLORS.border}`,
|
|
825
|
+
borderLeft: `4px solid ${COLORS.primary}`,
|
|
826
|
+
borderRadius: RADIUS.md,
|
|
827
|
+
boxShadow: SHADOWS.sm,
|
|
828
|
+
}}>
|
|
546
829
|
<div style={{
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
letterSpacing: '0.05em',
|
|
830
|
+
fontSize: '12px', fontWeight: 600, color: COLORS.textMuted,
|
|
831
|
+
letterSpacing: '0.05em', marginBottom: SPACING.sm,
|
|
550
832
|
}}>
|
|
551
|
-
{t('
|
|
833
|
+
📊 {t('ai_helper_teaching_summary_section_diagnosis')}
|
|
552
834
|
</div>
|
|
835
|
+
<div
|
|
836
|
+
className="markdown-body"
|
|
837
|
+
style={{ fontSize: '15px', lineHeight: 1.7 }}
|
|
838
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(parsed.diagnosis) }}
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
)}
|
|
553
842
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
843
|
+
{/* ② 下节课回顾清单 — 拿着就能上课,常驻展示 */}
|
|
844
|
+
{parsed.reviewList && (
|
|
845
|
+
<SectionCard
|
|
846
|
+
icon="📋"
|
|
847
|
+
title={t('ai_helper_teaching_summary_section_review')}
|
|
848
|
+
accentColor={COLORS.hydroGreen}
|
|
849
|
+
>
|
|
850
|
+
<div
|
|
851
|
+
className="markdown-body"
|
|
852
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(parsed.reviewList) }}
|
|
853
|
+
/>
|
|
854
|
+
{parsed.p1 && (
|
|
855
|
+
<div style={{ marginTop: SPACING.md, paddingTop: SPACING.md, borderTop: `1px dashed ${COLORS.border}` }}>
|
|
856
|
+
<div style={{
|
|
857
|
+
fontSize: '12px', fontWeight: 600, color: COLORS.textSecondary,
|
|
858
|
+
marginBottom: SPACING.sm, letterSpacing: '0.02em',
|
|
859
|
+
}}>
|
|
860
|
+
👥 个体干预建议
|
|
861
|
+
</div>
|
|
862
|
+
<div
|
|
863
|
+
className="markdown-body"
|
|
864
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(parsed.p1) }}
|
|
865
|
+
/>
|
|
557
866
|
</div>
|
|
558
|
-
) : (
|
|
559
|
-
<>
|
|
560
|
-
{highFindings.map(f => (
|
|
561
|
-
<FindingCard key={f.id} finding={f} deepDiveText={summary.deepDiveResults?.[f.id]} />
|
|
562
|
-
))}
|
|
563
|
-
{mediumFindings.map(f => (
|
|
564
|
-
<FindingCard key={f.id} finding={f} deepDiveText={summary.deepDiveResults?.[f.id]} />
|
|
565
|
-
))}
|
|
566
|
-
{lowFindings.map(f => (
|
|
567
|
-
<FindingCard key={f.id} finding={f} deepDiveText={summary.deepDiveResults?.[f.id]} />
|
|
568
|
-
))}
|
|
569
|
-
</>
|
|
570
867
|
)}
|
|
571
|
-
</
|
|
868
|
+
</SectionCard>
|
|
869
|
+
)}
|
|
572
870
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
871
|
+
{/* ③ 学生问题 — 去重后的重点卡片 + 其他观察一行带过 */}
|
|
872
|
+
<SectionCard
|
|
873
|
+
icon="🔍"
|
|
874
|
+
title={`${t('ai_helper_teaching_summary_section_problems')}${primaryFindings.length > 0 ? `(${primaryFindings.length})` : ''}`}
|
|
875
|
+
accentColor={highCount > 0 ? SEVERITY_COLORS.high.text : COLORS.border}
|
|
876
|
+
>
|
|
877
|
+
{primaryFindings.length === 0 ? (
|
|
878
|
+
<div style={{ color: COLORS.textMuted, fontSize: '13px', fontStyle: 'italic' }}>
|
|
879
|
+
{t('ai_helper_teaching_summary_no_findings')}
|
|
880
|
+
</div>
|
|
881
|
+
) : (
|
|
882
|
+
primaryFindings.map(f => (
|
|
883
|
+
<FindingCard
|
|
884
|
+
key={f.id}
|
|
885
|
+
finding={f}
|
|
886
|
+
deepDiveText={summary.deepDiveResults?.[f.id]}
|
|
887
|
+
studentNames={summary.studentNames}
|
|
888
|
+
/>
|
|
889
|
+
))
|
|
890
|
+
)}
|
|
891
|
+
|
|
892
|
+
{secondaryFindings.length > 0 && (
|
|
893
|
+
<div style={{
|
|
894
|
+
marginTop: SPACING.sm,
|
|
895
|
+
padding: `${SPACING.sm} ${SPACING.md}`,
|
|
896
|
+
backgroundColor: COLORS.bgPage,
|
|
897
|
+
borderRadius: RADIUS.sm,
|
|
898
|
+
fontSize: '12px', color: COLORS.textMuted, lineHeight: 1.8,
|
|
899
|
+
}}>
|
|
900
|
+
<strong>{t('ai_helper_teaching_summary_other_observations')}:</strong>
|
|
901
|
+
{secondaryFindings.map(f => f.title).join(' · ')}
|
|
902
|
+
</div>
|
|
903
|
+
)}
|
|
904
|
+
</SectionCard>
|
|
905
|
+
|
|
906
|
+
{/* ④ 课后强化训练 — 一键复制下发 */}
|
|
907
|
+
{homeworkMd && (
|
|
908
|
+
<SectionCard
|
|
909
|
+
icon="📝"
|
|
910
|
+
title={t('ai_helper_teaching_summary_section_homework')}
|
|
911
|
+
accentColor={COLORS.primary}
|
|
912
|
+
headerRight={(
|
|
913
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: SPACING.sm }}>
|
|
914
|
+
{copyState !== 'idle' && (
|
|
915
|
+
<span style={{
|
|
916
|
+
fontSize: '12px',
|
|
917
|
+
color: copyState === 'copied' ? COLORS.successText : COLORS.errorText,
|
|
918
|
+
}}>
|
|
919
|
+
{copyState === 'copied'
|
|
920
|
+
? t('ai_helper_teaching_summary_copied')
|
|
921
|
+
: t('ai_helper_teaching_summary_copy_failed')}
|
|
922
|
+
</span>
|
|
923
|
+
)}
|
|
924
|
+
<button
|
|
925
|
+
onClick={handleCopyHomework}
|
|
586
926
|
style={{
|
|
587
|
-
padding:
|
|
588
|
-
|
|
589
|
-
|
|
927
|
+
fontSize: '12px', padding: '3px 10px',
|
|
928
|
+
border: `1px solid ${COLORS.primary}`,
|
|
929
|
+
borderRadius: RADIUS.sm,
|
|
930
|
+
backgroundColor: 'transparent',
|
|
931
|
+
color: COLORS.primary,
|
|
590
932
|
cursor: 'pointer',
|
|
591
|
-
display: 'flex',
|
|
592
|
-
alignItems: 'center',
|
|
593
|
-
justifyContent: 'space-between',
|
|
594
|
-
userSelect: 'none' as const,
|
|
595
933
|
}}
|
|
596
934
|
>
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
935
|
+
📄 {t('ai_helper_teaching_summary_copy_homework')}
|
|
936
|
+
</button>
|
|
937
|
+
</span>
|
|
938
|
+
)}
|
|
939
|
+
>
|
|
940
|
+
<div style={{
|
|
941
|
+
fontSize: '12px', color: COLORS.textMuted, marginBottom: SPACING.md,
|
|
942
|
+
}}>
|
|
943
|
+
{t('ai_helper_teaching_summary_homework_hint')}
|
|
944
|
+
</div>
|
|
945
|
+
<div
|
|
946
|
+
className="markdown-body"
|
|
947
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(homeworkMd) }}
|
|
948
|
+
/>
|
|
949
|
+
</SectionCard>
|
|
950
|
+
)}
|
|
611
951
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)}
|
|
657
|
-
</div>
|
|
658
|
-
</>
|
|
659
|
-
)}
|
|
660
|
-
</div>
|
|
952
|
+
{/* 兜底:解析失败(或旧版文档)时完整展示 AI 报告 */}
|
|
953
|
+
{!parsedOk && summary.overallSuggestion && (
|
|
954
|
+
<SectionCard
|
|
955
|
+
icon="🤖"
|
|
956
|
+
title={t('ai_helper_teaching_summary_overall_suggestion')}
|
|
957
|
+
accentColor={COLORS.hydroGreen}
|
|
958
|
+
>
|
|
959
|
+
<div
|
|
960
|
+
className="markdown-body"
|
|
961
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(summary.overallSuggestion) }}
|
|
962
|
+
/>
|
|
963
|
+
</SectionCard>
|
|
964
|
+
)}
|
|
965
|
+
|
|
966
|
+
{/* 已解析主要板块后剩余的未识别章节(如旧版 P0 长报告),默认折叠 */}
|
|
967
|
+
{parsedOk && parsed.rest.length > 0 && (
|
|
968
|
+
<div style={{
|
|
969
|
+
border: `1px solid ${COLORS.border}`,
|
|
970
|
+
borderRadius: RADIUS.md,
|
|
971
|
+
backgroundColor: COLORS.bgCard,
|
|
972
|
+
marginBottom: SPACING.lg,
|
|
973
|
+
overflow: 'hidden',
|
|
974
|
+
}}>
|
|
975
|
+
<div
|
|
976
|
+
onClick={() => setFullReportCollapsed(!fullReportCollapsed)}
|
|
977
|
+
style={{
|
|
978
|
+
padding: `${SPACING.md} ${SPACING.base}`,
|
|
979
|
+
cursor: 'pointer', userSelect: 'none',
|
|
980
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
981
|
+
}}
|
|
982
|
+
>
|
|
983
|
+
<span style={{
|
|
984
|
+
fontWeight: 600, fontSize: '13px', color: COLORS.textMuted,
|
|
985
|
+
letterSpacing: '0.05em',
|
|
986
|
+
}}>
|
|
987
|
+
🗂 {t('ai_helper_teaching_summary_section_full_report')}
|
|
988
|
+
</span>
|
|
989
|
+
<span style={{
|
|
990
|
+
display: 'inline-block', width: '16px', height: '16px',
|
|
991
|
+
textAlign: 'center', lineHeight: '16px', fontSize: '12px',
|
|
992
|
+
color: COLORS.textMuted,
|
|
993
|
+
transition: `transform ${TRANSITIONS.fast}`,
|
|
994
|
+
transform: fullReportCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
|
995
|
+
}}>▶</span>
|
|
661
996
|
</div>
|
|
662
|
-
|
|
663
|
-
|
|
997
|
+
{!fullReportCollapsed && (
|
|
998
|
+
<div
|
|
999
|
+
className="markdown-body"
|
|
1000
|
+
style={{ padding: `0 ${SPACING.base} ${SPACING.base}`, borderTop: `1px solid ${COLORS.border}`, paddingTop: SPACING.md }}
|
|
1001
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(parsed.rest.join('\n\n')) }}
|
|
1002
|
+
/>
|
|
1003
|
+
)}
|
|
1004
|
+
</div>
|
|
1005
|
+
)}
|
|
1006
|
+
|
|
1007
|
+
{/* 生成中兜底:completed 但建议为空 */}
|
|
1008
|
+
{!summary.overallSuggestion && !homeworkMd && (
|
|
1009
|
+
<SkeletonBlock lines={6} />
|
|
1010
|
+
)}
|
|
1011
|
+
|
|
1012
|
+
{/* Feedback */}
|
|
1013
|
+
{feedbackRow}
|
|
664
1014
|
</div>
|
|
665
1015
|
);
|
|
666
1016
|
};
|