Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@
/>
</mat-card-content>
</mat-card>
} @else if (component?.type === 'Discussion') {
<mat-card appearance="outlined" class="w-full">
<mat-card-content>
<discussion-summary-display
[nodeId]="node.id"
[componentId]="component.id"
[periodId]="periodId"
[source]="source"
[doRender]="true"
/>
</mat-card-content>
</mat-card>
}
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import { MatCardModule } from '@angular/material/card';
import { CRaterService } from '../../../services/cRaterService';
import { OpenResponseSummaryDisplayComponent } from '../../../directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component';
import { ProjectService } from '../../../services/projectService';
import { DiscussionSummaryDisplayComponent } from '../../../directives/teacher-summary-display/discussion-summary-display/discussion-summary-display.component';

@Component({
imports: [
ComponentCompletionComponent,
DiscussionSummaryDisplayComponent,
IdeasSummaryComponent,
MatCardModule,
MatchSummaryDisplayComponent,
Expand Down Expand Up @@ -83,7 +85,7 @@ export class ComponentSummaryComponent {
(this.hasScoresSummary && this.hasScoreAnnotation) ||
this.hasIdeaRubricData ||
this.component?.type === 'Match';
if (this.component?.type === 'OpenResponse') {
if (this.component?.type === 'OpenResponse' || this.component?.type === 'Discussion') {
this.hasSummaryData = this.projectService.getProject().ai?.enabled;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@if (hasStudentResponses) {
<div class="flex gap-2 flex-wrap items-center mat-caption">
<div class="flex items-center gap-2">
<button mat-button (click)="generateSummary()" [disabled]="generatingSummary" color="primary">
<span i18n>Generate Class Summary</span><mat-icon>auto_awesome</mat-icon>
</button>
@if (generatingSummary) {
<mat-spinner diameter="20" />
}
</div>
@if (newSummaryAvailable) {
<span class="info" i18n>*New responses since last summary</span>
}
</div>
@if (summary) {
<markdown [data]="summary" />
<div class="mat-caption text-secondary" i18n>
Summary generated {{ summaryDate | date: 'short' }} from
{{ getLatestPeriodComponentStates().length }} responses
</div>
}
} @else {
<div i18n>No student responses</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Component, inject } from '@angular/core';
import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component';
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { AwsBedRockService } from '../../../../../app/chatbot/awsBedRock.service';
import { ChatMessage } from '../../../../../app/chatbot/chat';
import { TeacherDataService } from '../../../services/teacherDataService';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { LocalStorageService } from '../../../../../app/services/localStorageService';
import { MarkdownComponent } from 'ngx-markdown';
import { DatePipe } from '@angular/common';

@Component({
imports: [DatePipe, MarkdownComponent, MatButton, MatIcon, MatProgressSpinner],
templateUrl: './ai-summary-display.component.html'
})
export abstract class AiSummaryDisplayComponent extends TeacherSummaryDisplayComponent {
protected awsBedRockService: AwsBedRockService = inject(AwsBedRockService);
protected generatingSummary: boolean = false;
protected hasStudentResponses: boolean = false;
private localStorageService: LocalStorageService = inject(LocalStorageService);
protected newSummaryAvailable: boolean = false;
protected summary: string;
private summaryTimestamp: number;

ngOnInit(): void {
this.renderDisplay();
}

protected renderDisplay(): void {
super.renderDisplay();
const latestPeriodComponentStates = this.getLatestPeriodComponentStates();
this.hasStudentResponses = latestPeriodComponentStates.length > 0;
if (!this.hasStudentResponses) {
return;
}
this.summary = this.localStorageService.getItem(this.getSummaryKey()) || '';
this.summaryTimestamp = this.localStorageService.getItem(this.getSummaryTimestampKey()) || 0;
const lastResponseTime = latestPeriodComponentStates.reduce(
(max, state) => Math.max(max, state.serverSaveTime),
0
);
this.newSummaryAvailable =
this.summaryTimestamp > 0 && lastResponseTime > this.summaryTimestamp;
}

protected getLatestPeriodComponentStates(): any[] {
return (this.dataService as TeacherDataService)
.getComponentStatesByComponentId(this.componentId)
.filter((state) => state.periodId === this.periodId || this.periodId === -1)
.sort((a, b) => a.serverSaveTime - b.serverSaveTime);
}

protected async generateSummary(): Promise<void> {
this.generatingSummary = true;
const prompt = this.projectService.getComponent(this.nodeId, this.componentId).prompt;
this.summary = await this.awsBedRockService.sendMessage([
new ChatMessage('system', this.getSystemPrompt(prompt), this.nodeId),
new ChatMessage('user', this.getStudentResponses(), this.nodeId)
]);
this.localStorageService.setItem(this.getSummaryKey(), this.summary);
this.localStorageService.setItem(this.getSummaryTimestampKey(), new Date().getTime());
this.generatingSummary = false;
this.newSummaryAvailable = false;
}

protected abstract getStudentResponses(): string;

protected abstract getSystemPrompt(prompt: string): string;

private getSummaryKey(): string {
return `component-summary-${this.periodId}-${this.nodeId}-${this.componentId}`;
}

private getSummaryTimestampKey(): string {
return `component-summary-timestamp-${this.periodId}-${this.nodeId}-${this.componentId}`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Component } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MarkdownComponent } from 'ngx-markdown';
import { AiSummaryDisplayComponent } from '../ai-summary-display/ai-summary-display.component';
import { DatePipe } from '@angular/common';

interface Thread {
id: number;
post: string;
replies: string[];
}

@Component({
imports: [DatePipe, MarkdownComponent, MatButton, MatIcon, MatProgressSpinner],
selector: 'discussion-summary-display',
templateUrl: '../ai-summary-display/ai-summary-display.component.html'
})
export class DiscussionSummaryDisplayComponent extends AiSummaryDisplayComponent {
protected getSystemPrompt(prompt: string): string {
return `You are a teacher who is summarizing students' discussion threads, which include posts and replies to the following question: "${prompt}".
Each thread is in the format: <thread><post>Post</post><replies><reply>Reply 1</reply><reply>Reply 2</reply></replies></thread>.
In the same language as the question, provide a summary of the threads in 100 words or less.`;
}

protected getStudentResponses(): string {
return this.getDiscussionThreads().reduce(
(soFar, thread) =>
`${soFar}<thread><post>${thread.post}</post><replies>${thread.replies.map((reply) => `<reply>${reply}</reply>`).join('')}</replies></thread>`,
''
);
}

private getDiscussionThreads(): Thread[] {
const states = this.getLatestPeriodComponentStates();
const threads = states
.filter((state) => state.studentData.componentStateIdReplyingTo == null)
.map((post) => ({ id: post.id, post: post.studentData.response, replies: [] }));
states
.filter((state) => state.studentData.componentStateIdReplyingTo != null)
.forEach((reply) => {
threads
.find((t) => t.id === reply.studentData.componentStateIdReplyingTo)
?.replies.push(reply.studentData.response);
});
return threads;
}
}
88 changes: 60 additions & 28 deletions src/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21846,6 +21846,66 @@ If this problem continues, let your teacher know and move on to the next activit
<context context-type="linenumber">401</context>
</context-group>
</trans-unit>
<trans-unit id="3646439300244649070" datatype="html">
<source>Generate Class Summary</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">5,7</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">5,7</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">5,7</context>
</context-group>
</trans-unit>
<trans-unit id="2647230157856881985" datatype="html">
<source>*New responses since last summary</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">12,16</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">12,16</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">12,16</context>
</context-group>
</trans-unit>
<trans-unit id="2777541710949293848" datatype="html">
<source> Summary generated <x id="INTERPOLATION" equiv-text="{{ summaryDate | date: &apos;short&apos; }}"/> from <x id="INTERPOLATION_1" equiv-text="{{ getLatestPeriodComponentStates().length }}"/> responses </source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">18,22</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">18,22</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">18,22</context>
</context-group>
</trans-unit>
<trans-unit id="3421658563032484911" datatype="html">
<source>No student responses</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">23,25</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html</context>
<context context-type="linenumber">23,25</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">23,25</context>
</context-group>
</trans-unit>
<trans-unit id="1563518905064973119" datatype="html">
<source>Student Ideas Detected</source>
<context-group purpose="location">
Expand Down Expand Up @@ -21930,34 +21990,6 @@ If this problem continues, let your teacher know and move on to the next activit
<context context-type="linenumber">58,61</context>
</context-group>
</trans-unit>
<trans-unit id="3646439300244649070" datatype="html">
<source>Generate Class Summary</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">5,7</context>
</context-group>
</trans-unit>
<trans-unit id="2647230157856881985" datatype="html">
<source>*New responses since last summary</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">12,16</context>
</context-group>
</trans-unit>
<trans-unit id="2777541710949293848" datatype="html">
<source> Summary generated <x id="INTERPOLATION" equiv-text="{{ summaryDate | date: &apos;short&apos; }}"/> from <x id="INTERPOLATION_1" equiv-text="{{ getLatestPeriodComponentStates().length }}"/> responses </source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">18,22</context>
</context-group>
</trans-unit>
<trans-unit id="3421658563032484911" datatype="html">
<source>No student responses</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html</context>
<context context-type="linenumber">23,25</context>
</context-group>
</trans-unit>
<trans-unit id="866753876041645935" datatype="html">
<source>The student will see a graph of their individual data here.</source>
<context-group purpose="location">
Expand Down