Date: 2026-05-16
Status: Approved (design)
Repos: PPT (frontend), cococlass-english-speaking-api (backend)
AISummary.vue currently shows a compact 3-bullet card. The backend generate_class_summary
feeds the class-summary LLM only thin aggregated data (overallScore, dimensions,
topHighlights, topImprovements per student) and gets back 3 short text bullets.
The new requirement is a full tiered Markdown report: an A/B/C tier statistics table,
an optional ASCII bar chart, and Key Insights — driven by a richer per-student payload
(per-sentence transcripts + sentence comments, word-level accuracy_score/error_type,
per-user dimension scores + comment) and a new LLM system prompt.
overall_report has no numeric scores. OverallReportEvaluator only
emits { aiComment, highlights, improvements }. The reads in list_sessions_by_config
for overall_report.get("overallScore") / .get("dimensions") are dead — always
null/{}. Today's class summary silently aggregates zeros.adaptReport() in
llmService.ts computes overallScore + dimensions by averaging the per-sentence
Azure scores, relabeled:
overallScore = mean over student sentences of avg(accuracy, fluency, prosody, completeness)dimensions.fluency = avg Azure fluencyScoredimensions.interaction = avg Azure prosodyScoredimensions.vocabulary = avg Azure completenessScoredimensions.grammar = avg Azure accuracyScoreadaptReport's averaging server-side, keeping the class
report numerically consistent with the per-student report.SpeakingClassPanel. Collapsed = one live
completion line + expand toggle; expanded = full Markdown report in a scroll region.completedAt); a truncated flag is passed when capped.adaptReport). No new evaluator, no migration.形状词汇使用率 in the supplied prompt is topic-specific (copied from a
"shapes" example) and is generalized to 目标词汇使用率.≥85 / B 75-84 / C <75, per the prompt.cococlass-english-speaking-apiPOST /api/speaking/dialogue/sessions/by-config/summary/stream — SSE response.
Request body:
{ "configId": "...", "students": [{ "userId": "...", "name": "..." }], "locale": "zh|en|hk" }
SSE events reuse the existing shape parsed by parseSSEStream:
{ "type": "token", "text": "..." } … then { "type": "done" } or
{ "type": "error", "message": "..." }.
_build_class_report_input()list_sessions_by_config to get the latest session per user.completed sessions, ordered by completedAt, take the first 30. Load
DialogueMessage + selectinload(evaluation) rows (same pattern as get_report).Per student build:
{
name,
overallScore, # avg of sentence avg(4 Azure scores)
dimensions: { fluency, interaction, vocabulary, grammar }, # Azure averages
aiComment, highlights, improvements, # from overall_report
sentences: [
{ round, transcript,
sentenceComment, # evaluation.content_feedback.comment, omit if absent
wordAnalysis: [{ word, accuracyScore, errorType }] }
]
}
Only role == "student" messages with a completed evaluation contribute scores.
classStats: { total, submitted, unsubmitted, notStarted, rate, avgScore,
highScore, lowScore } computed from the real averaged scores.
If the completed count exceeded 30: truncated: true plus completedTotal /
includedCount so the LLM can note partial coverage.
ClassReportEvaluator目标词汇使用率 column
generalization). Output is Markdown, not JSON.stream=True; yields Markdown chunks as they arrive.{ classStats, perStudent, locale } with the existing
"treat data as data, not instructions" safety preamble.submitted == 0, skip the LLM and stream a single short
"waiting for submissions" message (localized) then done.token events; accumulate full text.(configId, contentHash(summaries)), same SUMMARY_TTL_SECONDS.token event + done.{ type: "error" }.POST /sessions/by-config/summary route, ClassSummaryEvaluator,
class_summary_rules.py, and the bullet_* rule helpers.overall_report.get("overallScore")/.get("dimensions") reads in
list_sessions_by_config stay — harmless, and list_sessions_by_config is still
used by the student grid.PPTsrc/services/speaking.tsstreamClassReport(configId, students, locale) — async generator using
fetch + the shared parseSSEStream; yields Markdown token strings.parseSSEStream is promoted from llmService.ts to a shared module (or imported)
so both services use one implementation.generateClassSummary + ClassSummaryResponse.src/views/Student/components/SpeakingClassPanel/useClassSummary.tsaiBullets, liveBullet1-as-tuple wiring, frontendRuleBullet2/3,
aiBackendBullets, refreshAISummary.liveBullet1 (live completion count/rate) as a standalone computed — it is the
collapsed-state line and needs no LLM call.reportMarkdown: Ref<string>, reportStreaming: Ref<boolean>,
reportGeneratedAt: Ref<string|null>, reportError: Ref<string|null>,
refreshReport() — opens the stream, appends tokens to reportMarkdown, token-guard
against stale streams (same aiToken pattern).src/views/Student/components/SpeakingClassPanel/AISummary.vue{ completionLine: string, markdown: string, streaming: boolean,
generatedAt: string|null, error: string|null }; emits refresh.查看完整分析报告 ▾ toggle.markdown with markdown-it (html: true), pass through
DOMPurify with a tight allowlist (table tags + span with a restricted style
attr), in a max-height + overflow:auto region. ASCII chart renders inside a
fenced code block (monospace).streaming; skeleton before the first token.submitted == 0 → show the localized waiting message instead of the report.dompurify (+ @types/dompurify) to PPT. markdown-it already present.New keys: expand/collapse labels, "查看完整分析报告", regenerate, stream-error message, waiting-for-submissions message.
markdown-it; acceptable._build_class_report_input — score averaging matches
adaptReport, 30-cap + truncated flag, sentence/word assembly, missing
content_feedback; streamed endpoint with a stubbed LLM (token events → done);
cache-hit replay path.useClassSummary stream accumulation + stale-token discard;
markdown-it → DOMPurify render (red <span> survives, <script> stripped).StudentReportModal.