Skip to content

Commit d99fdcc

Browse files
feat(Discussion): Summarize student responses in grading tool (#2262)
Co-authored-by: Jonathan Lim-Breitbart <breity10@gmail.com>
1 parent 1e1682f commit d99fdcc

6 files changed

Lines changed: 226 additions & 29 deletions

File tree

src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@
8484
/>
8585
</mat-card-content>
8686
</mat-card>
87+
} @else if (component?.type === 'Discussion') {
88+
<mat-card appearance="outlined" class="w-full">
89+
<mat-card-content>
90+
<discussion-summary-display
91+
[nodeId]="node.id"
92+
[componentId]="component.id"
93+
[periodId]="periodId"
94+
[source]="source"
95+
[doRender]="true"
96+
/>
97+
</mat-card-content>
98+
</mat-card>
8799
}
88100
</div>
89101
}

src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { MatCardModule } from '@angular/material/card';
1616
import { CRaterService } from '../../../services/cRaterService';
1717
import { OpenResponseSummaryDisplayComponent } from '../../../directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component';
1818
import { ProjectService } from '../../../services/projectService';
19+
import { DiscussionSummaryDisplayComponent } from '../../../directives/teacher-summary-display/discussion-summary-display/discussion-summary-display.component';
1920

2021
@Component({
2122
imports: [
2223
ComponentCompletionComponent,
24+
DiscussionSummaryDisplayComponent,
2325
IdeasSummaryComponent,
2426
MatCardModule,
2527
MatchSummaryDisplayComponent,
@@ -83,7 +85,7 @@ export class ComponentSummaryComponent {
8385
(this.hasScoresSummary && this.hasScoreAnnotation) ||
8486
this.hasIdeaRubricData ||
8587
this.component?.type === 'Match';
86-
if (this.component?.type === 'OpenResponse') {
88+
if (this.component?.type === 'OpenResponse' || this.component?.type === 'Discussion') {
8789
this.hasSummaryData = this.projectService.getProject().ai?.enabled;
8890
}
8991
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@if (hasStudentResponses) {
2+
<div class="flex gap-2 flex-wrap items-center mat-caption">
3+
<div class="flex items-center gap-2">
4+
<button mat-button (click)="generateSummary()" [disabled]="generatingSummary" color="primary">
5+
<span i18n>Generate Class Summary</span><mat-icon>auto_awesome</mat-icon>
6+
</button>
7+
@if (generatingSummary) {
8+
<mat-spinner diameter="20" />
9+
}
10+
</div>
11+
@if (newSummaryAvailable) {
12+
<span class="info" i18n>*New responses since last summary</span>
13+
}
14+
</div>
15+
@if (summary) {
16+
<markdown [data]="summary" />
17+
<div class="mat-caption text-secondary" i18n>
18+
Summary generated {{ summaryDate | date: 'short' }} from
19+
{{ getLatestPeriodComponentStates().length }} responses
20+
</div>
21+
}
22+
} @else {
23+
<div i18n>No student responses</div>
24+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Component, inject } from '@angular/core';
2+
import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component';
3+
import { MatButton } from '@angular/material/button';
4+
import { MatIcon } from '@angular/material/icon';
5+
import { AwsBedRockService } from '../../../../../app/chatbot/awsBedRock.service';
6+
import { ChatMessage } from '../../../../../app/chatbot/chat';
7+
import { TeacherDataService } from '../../../services/teacherDataService';
8+
import { MatProgressSpinner } from '@angular/material/progress-spinner';
9+
import { LocalStorageService } from '../../../../../app/services/localStorageService';
10+
import { MarkdownComponent } from 'ngx-markdown';
11+
import { DatePipe } from '@angular/common';
12+
13+
@Component({
14+
imports: [DatePipe, MarkdownComponent, MatButton, MatIcon, MatProgressSpinner],
15+
templateUrl: './ai-summary-display.component.html'
16+
})
17+
export abstract class AiSummaryDisplayComponent extends TeacherSummaryDisplayComponent {
18+
protected awsBedRockService: AwsBedRockService = inject(AwsBedRockService);
19+
protected generatingSummary: boolean = false;
20+
protected hasStudentResponses: boolean = false;
21+
private localStorageService: LocalStorageService = inject(LocalStorageService);
22+
protected newSummaryAvailable: boolean = false;
23+
protected summary: string;
24+
private summaryTimestamp: number;
25+
26+
ngOnInit(): void {
27+
this.renderDisplay();
28+
}
29+
30+
protected renderDisplay(): void {
31+
super.renderDisplay();
32+
const latestPeriodComponentStates = this.getLatestPeriodComponentStates();
33+
this.hasStudentResponses = latestPeriodComponentStates.length > 0;
34+
if (!this.hasStudentResponses) {
35+
return;
36+
}
37+
this.summary = this.localStorageService.getItem(this.getSummaryKey()) || '';
38+
this.summaryTimestamp = this.localStorageService.getItem(this.getSummaryTimestampKey()) || 0;
39+
const lastResponseTime = latestPeriodComponentStates.reduce(
40+
(max, state) => Math.max(max, state.serverSaveTime),
41+
0
42+
);
43+
this.newSummaryAvailable =
44+
this.summaryTimestamp > 0 && lastResponseTime > this.summaryTimestamp;
45+
}
46+
47+
protected getLatestPeriodComponentStates(): any[] {
48+
return (this.dataService as TeacherDataService)
49+
.getComponentStatesByComponentId(this.componentId)
50+
.filter((state) => state.periodId === this.periodId || this.periodId === -1)
51+
.sort((a, b) => a.serverSaveTime - b.serverSaveTime);
52+
}
53+
54+
protected async generateSummary(): Promise<void> {
55+
this.generatingSummary = true;
56+
const prompt = this.projectService.getComponent(this.nodeId, this.componentId).prompt;
57+
this.summary = await this.awsBedRockService.sendMessage([
58+
new ChatMessage('system', this.getSystemPrompt(prompt), this.nodeId),
59+
new ChatMessage('user', this.getStudentResponses(), this.nodeId)
60+
]);
61+
this.localStorageService.setItem(this.getSummaryKey(), this.summary);
62+
this.localStorageService.setItem(this.getSummaryTimestampKey(), new Date().getTime());
63+
this.generatingSummary = false;
64+
this.newSummaryAvailable = false;
65+
}
66+
67+
protected abstract getStudentResponses(): string;
68+
69+
protected abstract getSystemPrompt(prompt: string): string;
70+
71+
private getSummaryKey(): string {
72+
return `component-summary-${this.periodId}-${this.nodeId}-${this.componentId}`;
73+
}
74+
75+
private getSummaryTimestampKey(): string {
76+
return `component-summary-timestamp-${this.periodId}-${this.nodeId}-${this.componentId}`;
77+
}
78+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Component } from '@angular/core';
2+
import { MatButton } from '@angular/material/button';
3+
import { MatIcon } from '@angular/material/icon';
4+
import { MatProgressSpinner } from '@angular/material/progress-spinner';
5+
import { MarkdownComponent } from 'ngx-markdown';
6+
import { AiSummaryDisplayComponent } from '../ai-summary-display/ai-summary-display.component';
7+
import { DatePipe } from '@angular/common';
8+
9+
interface Thread {
10+
id: number;
11+
post: string;
12+
replies: string[];
13+
}
14+
15+
@Component({
16+
imports: [DatePipe, MarkdownComponent, MatButton, MatIcon, MatProgressSpinner],
17+
selector: 'discussion-summary-display',
18+
templateUrl: '../ai-summary-display/ai-summary-display.component.html'
19+
})
20+
export class DiscussionSummaryDisplayComponent extends AiSummaryDisplayComponent {
21+
protected getSystemPrompt(prompt: string): string {
22+
return `You are a teacher who is summarizing students' discussion threads, which include posts and replies to the following question: "${prompt}".
23+
Each thread is in the format: <thread><post>Post</post><replies><reply>Reply 1</reply><reply>Reply 2</reply></replies></thread>.
24+
In the same language as the question, provide a summary of the threads in 100 words or less.`;
25+
}
26+
27+
protected getStudentResponses(): string {
28+
return this.getDiscussionThreads().reduce(
29+
(soFar, thread) =>
30+
`${soFar}<thread><post>${thread.post}</post><replies>${thread.replies.map((reply) => `<reply>${reply}</reply>`).join('')}</replies></thread>`,
31+
''
32+
);
33+
}
34+
35+
private getDiscussionThreads(): Thread[] {
36+
const states = this.getLatestPeriodComponentStates();
37+
const threads = states
38+
.filter((state) => state.studentData.componentStateIdReplyingTo == null)
39+
.map((post) => ({ id: post.id, post: post.studentData.response, replies: [] }));
40+
states
41+
.filter((state) => state.studentData.componentStateIdReplyingTo != null)
42+
.forEach((reply) => {
43+
threads
44+
.find((t) => t.id === reply.studentData.componentStateIdReplyingTo)
45+
?.replies.push(reply.studentData.response);
46+
});
47+
return threads;
48+
}
49+
}

src/messages.xlf

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21846,6 +21846,66 @@ If this problem continues, let your teacher know and move on to the next activit
2184621846
<context context-type="linenumber">401</context>
2184721847
</context-group>
2184821848
</trans-unit>
21849+
<trans-unit id="3646439300244649070" datatype="html">
21850+
<source>Generate Class Summary</source>
21851+
<context-group purpose="location">
21852+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21853+
<context context-type="linenumber">5,7</context>
21854+
</context-group>
21855+
<context-group purpose="location">
21856+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21857+
<context context-type="linenumber">5,7</context>
21858+
</context-group>
21859+
<context-group purpose="location">
21860+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21861+
<context context-type="linenumber">5,7</context>
21862+
</context-group>
21863+
</trans-unit>
21864+
<trans-unit id="2647230157856881985" datatype="html">
21865+
<source>*New responses since last summary</source>
21866+
<context-group purpose="location">
21867+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21868+
<context context-type="linenumber">12,16</context>
21869+
</context-group>
21870+
<context-group purpose="location">
21871+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21872+
<context context-type="linenumber">12,16</context>
21873+
</context-group>
21874+
<context-group purpose="location">
21875+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21876+
<context context-type="linenumber">12,16</context>
21877+
</context-group>
21878+
</trans-unit>
21879+
<trans-unit id="2777541710949293848" datatype="html">
21880+
<source> Summary generated <x id="INTERPOLATION" equiv-text="{{ summaryDate | date: &apos;short&apos; }}"/> from <x id="INTERPOLATION_1" equiv-text="{{ getLatestPeriodComponentStates().length }}"/> responses </source>
21881+
<context-group purpose="location">
21882+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21883+
<context context-type="linenumber">18,22</context>
21884+
</context-group>
21885+
<context-group purpose="location">
21886+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21887+
<context context-type="linenumber">18,22</context>
21888+
</context-group>
21889+
<context-group purpose="location">
21890+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21891+
<context context-type="linenumber">18,22</context>
21892+
</context-group>
21893+
</trans-unit>
21894+
<trans-unit id="3421658563032484911" datatype="html">
21895+
<source>No student responses</source>
21896+
<context-group purpose="location">
21897+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21898+
<context context-type="linenumber">23,25</context>
21899+
</context-group>
21900+
<context-group purpose="location">
21901+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
21902+
<context context-type="linenumber">23,25</context>
21903+
</context-group>
21904+
<context-group purpose="location">
21905+
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21906+
<context context-type="linenumber">23,25</context>
21907+
</context-group>
21908+
</trans-unit>
2184921909
<trans-unit id="1563518905064973119" datatype="html">
2185021910
<source>Student Ideas Detected</source>
2185121911
<context-group purpose="location">
@@ -21930,34 +21990,6 @@ If this problem continues, let your teacher know and move on to the next activit
2193021990
<context context-type="linenumber">58,61</context>
2193121991
</context-group>
2193221992
</trans-unit>
21933-
<trans-unit id="3646439300244649070" datatype="html">
21934-
<source>Generate Class Summary</source>
21935-
<context-group purpose="location">
21936-
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21937-
<context context-type="linenumber">5,7</context>
21938-
</context-group>
21939-
</trans-unit>
21940-
<trans-unit id="2647230157856881985" datatype="html">
21941-
<source>*New responses since last summary</source>
21942-
<context-group purpose="location">
21943-
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21944-
<context context-type="linenumber">12,16</context>
21945-
</context-group>
21946-
</trans-unit>
21947-
<trans-unit id="2777541710949293848" datatype="html">
21948-
<source> Summary generated <x id="INTERPOLATION" equiv-text="{{ summaryDate | date: &apos;short&apos; }}"/> from <x id="INTERPOLATION_1" equiv-text="{{ getLatestPeriodComponentStates().length }}"/> responses </source>
21949-
<context-group purpose="location">
21950-
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21951-
<context context-type="linenumber">18,22</context>
21952-
</context-group>
21953-
</trans-unit>
21954-
<trans-unit id="3421658563032484911" datatype="html">
21955-
<source>No student responses</source>
21956-
<context-group purpose="location">
21957-
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
21958-
<context context-type="linenumber">23,25</context>
21959-
</context-group>
21960-
</trans-unit>
2196121993
<trans-unit id="866753876041645935" datatype="html">
2196221994
<source>The student will see a graph of their individual data here.</source>
2196321995
<context-group purpose="location">

0 commit comments

Comments
 (0)