From 8cd20aad0db9d89630158b20dba194f3ab1de20b Mon Sep 17 00:00:00 2001 From: TakalaWang Date: Fri, 27 Mar 2026 02:38:14 +0800 Subject: [PATCH 1/3] fix: stance spectrum classification and enhanced grading UI (#119, #116) - Add stance_category to Stage 1/2 AI classifiers so stanceSnapshot correctly produces pro-strong/weak and con-strong/weak instead of always returning "neutral" - Remove stanceSnapshot from Stage 3/4 handlers (no stance changes) - Enhance teacher grading modal with conversation history, AI summary, and assessment dimension scores Closes #119 Closes #116 Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mentora/messages/en.json | 14 ++ apps/mentora/messages/zh-tw.json | 14 ++ .../course/mentor/CourseSubmissions.svelte | 233 +++++++++++++++--- .../src/builder/stage1-asking-stance.ts | 22 ++ .../src/builder/stage2-case-challenge.ts | 22 ++ .../orchestrator/handlers/asking-stance.ts | 5 +- .../orchestrator/handlers/case-challenge.ts | 17 +- .../src/orchestrator/handlers/closure.ts | 2 - .../handlers/principle-reasoning.ts | 4 - 9 files changed, 289 insertions(+), 44 deletions(-) diff --git a/apps/mentora/messages/en.json b/apps/mentora/messages/en.json index 6dbc50e5..63668243 100644 --- a/apps/mentora/messages/en.json +++ b/apps/mentora/messages/en.json @@ -515,6 +515,20 @@ "mentor_submissions_status_graded": "Graded", "mentor_submissions_type_dialogue": "Dialogue", "mentor_submissions_type_questionnaire": "Questionnaire", + "mentor_submissions_summary": "Summary", + "mentor_submissions_ai_assessment": "AI Assessment", + "mentor_submissions_overall": "Overall", + "mentor_submissions_conversation": "Conversation", + "mentor_submissions_no_conversation": "No conversation available", + "mentor_submissions_no_assessment": "No AI assessment available", + "mentor_submissions_no_summary": "No summary available", + "mentor_submissions_dimension_argumentQuality": "Argument Quality", + "mentor_submissions_dimension_criticalThinking": "Critical Thinking", + "mentor_submissions_dimension_principleExtraction": "Principle Extraction", + "mentor_submissions_dimension_openness": "Openness", + "mentor_submissions_dimension_coherence": "Coherence", + "mentor_submissions_student_label": "Student", + "mentor_submissions_ai_label": "AI", "announcements": "Announcements", "announcements_title": "Announcements", "announcements_today": "Today", diff --git a/apps/mentora/messages/zh-tw.json b/apps/mentora/messages/zh-tw.json index fcdc2a60..23719e1c 100644 --- a/apps/mentora/messages/zh-tw.json +++ b/apps/mentora/messages/zh-tw.json @@ -515,6 +515,20 @@ "mentor_submissions_status_graded": "已評分", "mentor_submissions_type_dialogue": "對話", "mentor_submissions_type_questionnaire": "問卷", + "mentor_submissions_summary": "對話總結", + "mentor_submissions_ai_assessment": "AI 評估", + "mentor_submissions_overall": "總分", + "mentor_submissions_conversation": "對話紀錄", + "mentor_submissions_no_conversation": "尚無對話紀錄", + "mentor_submissions_no_assessment": "尚無 AI 評估", + "mentor_submissions_no_summary": "尚無總結", + "mentor_submissions_dimension_argumentQuality": "論點品質", + "mentor_submissions_dimension_criticalThinking": "批判思考", + "mentor_submissions_dimension_principleExtraction": "原則提取", + "mentor_submissions_dimension_openness": "開放性", + "mentor_submissions_dimension_coherence": "連貫性", + "mentor_submissions_student_label": "學生", + "mentor_submissions_ai_label": "AI", "announcements": "公告", "announcements_title": "公告", "announcements_today": "今天", diff --git a/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte b/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte index b2aa549c..7322ce3f 100644 --- a/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte +++ b/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte @@ -9,6 +9,8 @@ Questionnaire, QuestionnaireResponse, } from "$lib/api"; + import type { AssessmentResult, Turn } from "mentora-firebase"; + import type { DialogueStateDisplay } from "mentora-firebase"; import { formatMentoraDateTime } from "$lib/features/datetime/format"; import { onMount } from "svelte"; import { SvelteMap } from "svelte/reactivity"; @@ -69,6 +71,10 @@ let gradingSaving = $state(false); let gradingSuccess = $state(null); let gradingError = $state(null); + let gradingLoading = $state(false); + let gradingAssessment = $state(null); + let gradingDialogueState = $state(null); + let gradingConversationTurns = $state([]); // Response viewer modal state let viewResponseOpen = $state(false); @@ -230,7 +236,36 @@ gradingNotes = sub?.notes ?? ""; gradingSuccess = null; gradingError = null; + gradingAssessment = sub?.assessment ?? null; + gradingDialogueState = null; + gradingConversationTurns = []; + gradingLoading = true; gradingOpen = true; + + // Fetch conversation data in background + void loadGradingDetails(row.userId); + } + + async function loadGradingDetails(userId: string) { + try { + const convRes = await api.conversations.getForAssignment( + selectedItemId, + userId, + ); + if (convRes.success) { + gradingConversationTurns = convRes.data.turns; + const stateRes = await api.conversations.getDialogueState( + convRes.data.id, + ); + if (stateRes.success) { + gradingDialogueState = stateRes.data; + } + } + } catch { + // Non-critical: grading still works without conversation details + } finally { + gradingLoading = false; + } } async function saveGrade() { @@ -296,6 +331,18 @@ } } + function dimensionLabel(key: string): string { + const labels: Record string> = { + argumentQuality: m.mentor_submissions_dimension_argumentQuality, + criticalThinking: m.mentor_submissions_dimension_criticalThinking, + principleExtraction: + m.mentor_submissions_dimension_principleExtraction, + openness: m.mentor_submissions_dimension_openness, + coherence: m.mentor_submissions_dimension_coherence, + }; + return labels[key]?.() ?? key; + } + function statusLabel(state: string): string { switch (state) { case "in_progress": @@ -490,7 +537,11 @@ {/if} - +
@@ -499,36 +550,158 @@
{gradingStudentName}
-
- - -
+ {#if gradingLoading} +
+ + {m.loading()} +
+ {:else} + +
+

+ {m.mentor_submissions_summary()} +

+ {#if gradingDialogueState?.summary} +

+ {gradingDialogueState.summary} +

+ {:else} +

+ {m.mentor_submissions_no_summary()} +

+ {/if} +
-
- - + +
+

+ {m.mentor_submissions_ai_assessment()} +

+ {#if gradingAssessment} +
+ {#each Object.entries(gradingAssessment.dimensions) as [key, dim]} +
+
+ {dimensionLabel(key)} +
+
+ {#each Array(5) as _, i} +
+ {/each} + {dim.score}/5 +
+

+ {dim.feedback} +

+
+ {/each} +
+
+ {m.mentor_submissions_overall()}: + {gradingAssessment.overallScore}/5 + {#if gradingAssessment.overallFeedback} + — {gradingAssessment.overallFeedback} + {/if} +
+ {:else} +

+ {m.mentor_submissions_no_assessment()} +

+ {/if} +
+ + +
+

+ {m.mentor_submissions_conversation()} +

+ {#if gradingConversationTurns.length > 0} +
+ {#each gradingConversationTurns as turn, i} + {@const isStudent = i % 2 === 0} +
+
+
+ {isStudent + ? m.mentor_submissions_student_label() + : m.mentor_submissions_ai_label()} +
+ {turn.text} +
+
+ {/each} +
+ {:else} +

+ {m.mentor_submissions_no_conversation()} +

+ {/if} +
+ {/if} + + +
+
+
+ + +
+ +
+ + +
+
{#if gradingError} diff --git a/packages/mentora-ai/src/builder/stage1-asking-stance.ts b/packages/mentora-ai/src/builder/stage1-asking-stance.ts index e032f178..9bbe0cad 100644 --- a/packages/mentora-ai/src/builder/stage1-asking-stance.ts +++ b/packages/mentora-ai/src/builder/stage1-asking-stance.ts @@ -23,6 +23,21 @@ export const AskingStanceClassifierSchema = z.object({ .object({ stance: z.string().optional().describe("The user's stated stance"), reasoning: z.string().optional().describe("The user's reasoning"), + stance_category: z + .enum([ + "pro-strong", + "pro-weak", + "con-strong", + "con-weak", + "neutral", + ]) + .optional() + .describe( + "Classify the user's stance relative to the topic question. " + + "'pro' = agrees with the proposition, 'con' = disagrees. " + + "'strong' = firm/definitive, 'weak' = tentative/partial. " + + "'neutral' = genuinely undecided or balanced.", + ), }) .describe("選填:若有提取到關鍵資訊放在這裡"), }); @@ -71,6 +86,13 @@ Rules: 1. If the input is too short, vague, irrelevant, or does not answer the specific question -> Output "TR_CLARIFY". 2. If the user expresses a clear stance (even if simple) -> Output "TR_V1_ESTABLISHED". +When outputting "TR_V1_ESTABLISHED", you MUST also classify the stance: +- "pro-strong": The user clearly and firmly agrees with the proposition (e.g., "Yes, absolutely", "I strongly support this") +- "pro-weak": The user leans toward agreeing but is tentative (e.g., "I think so", "Probably yes") +- "con-strong": The user clearly and firmly disagrees (e.g., "No, absolutely not", "I'm strongly against this") +- "con-weak": The user leans toward disagreeing but is tentative (e.g., "I don't think so", "Probably not") +- "neutral": The user is genuinely undecided or presents a balanced view + Analyze the user input below: Question: ${input.currentQuestion} User Input: ${input.userInput} diff --git a/packages/mentora-ai/src/builder/stage2-case-challenge.ts b/packages/mentora-ai/src/builder/stage2-case-challenge.ts index 387d1653..026b2a1e 100644 --- a/packages/mentora-ai/src/builder/stage2-case-challenge.ts +++ b/packages/mentora-ai/src/builder/stage2-case-challenge.ts @@ -31,6 +31,21 @@ export const CaseChallengeClassifierSchema = z.object({ .string() .optional() .describe("New or updated reasoning if applicable"), + stance_category: z + .enum([ + "pro-strong", + "pro-weak", + "con-strong", + "con-weak", + "neutral", + ]) + .optional() + .describe( + "Classify the user's CURRENT effective stance after this response. " + + "'pro' = agrees with the proposition, 'con' = disagrees. " + + "'strong' = firm/definitive, 'weak' = tentative/partial. " + + "'neutral' = genuinely undecided or balanced.", + ), }) .describe("選填:若有提取到關鍵資訊放在這裡"), }); @@ -90,6 +105,13 @@ Rules: 2. **TR_SCAFFOLD**: The user's answer contradicts their \`previous_stance\`, shows hesitation ("Maybe I was wrong"), or admits the counter-example is valid, implying a need to update their stance. 3. **TR_CASE_COMPLETED**: The user defends their stance logically, OR successfully integrates the case into their existing view without contradiction. +For ALL intents (not just TR_SCAFFOLD), you MUST classify the user's current effective stance in extracted_data.stance_category: +- "pro-strong": Firmly agrees with the proposition +- "pro-weak": Tentatively agrees +- "con-strong": Firmly disagrees +- "con-weak": Tentatively disagrees +- "neutral": Genuinely undecided or balanced + Context: Previous Stance: ${previousStance} Current Case Challenge: ${currentCase} diff --git a/packages/mentora-ai/src/orchestrator/handlers/asking-stance.ts b/packages/mentora-ai/src/orchestrator/handlers/asking-stance.ts index b9809fbc..11e525ca 100644 --- a/packages/mentora-ai/src/orchestrator/handlers/asking-stance.ts +++ b/packages/mentora-ai/src/orchestrator/handlers/asking-stance.ts @@ -129,7 +129,10 @@ export class AskingStanceHandler implements StageHandler { newState, ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "neutral" }, + stanceSnapshot: { + stance: + classification.extracted_data?.stance_category || "neutral", + }, }; } } diff --git a/packages/mentora-ai/src/orchestrator/handlers/case-challenge.ts b/packages/mentora-ai/src/orchestrator/handlers/case-challenge.ts index 2d4b1cf0..b715f94e 100644 --- a/packages/mentora-ai/src/orchestrator/handlers/case-challenge.ts +++ b/packages/mentora-ai/src/orchestrator/handlers/case-challenge.ts @@ -61,7 +61,7 @@ export class CaseChallengeHandler implements StageHandler { return this.handleScaffold(context, classification); case "TR_CASE_COMPLETED": - return this.handleCaseCompleted(context); + return this.handleCaseCompleted(context, classification); } } @@ -133,16 +133,15 @@ export class CaseChallengeHandler implements StageHandler { ); } - const stanceSnapshot = classification.extracted_data?.stance - ? { stance: "neutral" as const } - : { stance: "undetermined" as const }; - return { message, newState, ended: false, usage: executor.getTokenUsage(), - stanceSnapshot, + stanceSnapshot: { + stance: + classification.extracted_data?.stance_category || "neutral", + }, }; } @@ -151,6 +150,7 @@ export class CaseChallengeHandler implements StageHandler { */ private async handleCaseCompleted( context: StageContext, + classification: CaseChallengeClassifier, ): Promise { const { executor, state } = context; @@ -180,7 +180,10 @@ export class CaseChallengeHandler implements StageHandler { }, ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "neutral" }, + stanceSnapshot: { + stance: + classification.extracted_data?.stance_category || "neutral", + }, }; } } diff --git a/packages/mentora-ai/src/orchestrator/handlers/closure.ts b/packages/mentora-ai/src/orchestrator/handlers/closure.ts index 7220b16b..dc17aebe 100644 --- a/packages/mentora-ai/src/orchestrator/handlers/closure.ts +++ b/packages/mentora-ai/src/orchestrator/handlers/closure.ts @@ -101,7 +101,6 @@ export class ClosureHandler implements StageHandler { }, ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "neutral" }, }; } @@ -157,7 +156,6 @@ export class ClosureHandler implements StageHandler { usage: executor.getTokenUsage(), assessment, assessmentError, - stanceSnapshot: { stance: "neutral" }, }; } } diff --git a/packages/mentora-ai/src/orchestrator/handlers/principle-reasoning.ts b/packages/mentora-ai/src/orchestrator/handlers/principle-reasoning.ts index 37d08dd3..4594a0aa 100644 --- a/packages/mentora-ai/src/orchestrator/handlers/principle-reasoning.ts +++ b/packages/mentora-ai/src/orchestrator/handlers/principle-reasoning.ts @@ -95,7 +95,6 @@ export class PrincipleReasoningHandler implements StageHandler { newState: transitionTo(state, DialogueStage.PRINCIPLE_REASONING), ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "undetermined" }, }; } @@ -127,7 +126,6 @@ export class PrincipleReasoningHandler implements StageHandler { newState: transitionTo(state, DialogueStage.PRINCIPLE_REASONING), ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "neutral" }, }; } @@ -179,7 +177,6 @@ export class PrincipleReasoningHandler implements StageHandler { }, ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "neutral" }, }; } @@ -234,7 +231,6 @@ export class PrincipleReasoningHandler implements StageHandler { }, ended: false, usage: executor.getTokenUsage(), - stanceSnapshot: { stance: "neutral" }, }; } } From 50606a41b9c94bf5a527fbe9a23ca5565346b6a2 Mon Sep 17 00:00:00 2001 From: TakalaWang Date: Fri, 27 Mar 2026 02:44:07 +0800 Subject: [PATCH 2/3] fix: resolve lint errors in grading modal each blocks Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/components/course/mentor/CourseSubmissions.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte b/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte index 7322ce3f..95845454 100644 --- a/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte +++ b/apps/mentora/src/lib/components/course/mentor/CourseSubmissions.svelte @@ -579,7 +579,7 @@ {#if gradingAssessment}
- {#each Object.entries(gradingAssessment.dimensions) as [key, dim]} + {#each Object.entries(gradingAssessment.dimensions) as [key, dim] (key)}
- {#each Array(5) as _, i} + {#each [0, 1, 2, 3, 4] as dot (dot)}
{#if gradingConversationTurns.length > 0}
- {#each gradingConversationTurns as turn, i} + {#each gradingConversationTurns as turn, i (turn.id)} {@const isStudent = i % 2 === 0}