diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.html b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.html index f7c1a5d0092..02c58d4a888 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.html +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.html @@ -84,6 +84,18 @@ /> + } @else if (component?.type === 'Discussion') { + + + + + } } diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.ts index 7c8d98686f8..81d4190fd44 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.ts @@ -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, @@ -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; } } diff --git a/src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html b/src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html new file mode 100644 index 00000000000..f4bda35f709 --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html @@ -0,0 +1,24 @@ +@if (hasStudentResponses) { +
+
+ + @if (generatingSummary) { + + } +
+ @if (newSummaryAvailable) { + *New responses since last summary + } +
+ @if (summary) { + +
+ Summary generated {{ summaryDate | date: 'short' }} from + {{ getLatestPeriodComponentStates().length }} responses +
+ } +} @else { +
No student responses
+} diff --git a/src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.ts b/src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.ts new file mode 100644 index 00000000000..db65ccbc4ff --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.ts @@ -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 { + 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}`; + } +} diff --git a/src/assets/wise5/directives/teacher-summary-display/discussion-summary-display/discussion-summary-display.component.ts b/src/assets/wise5/directives/teacher-summary-display/discussion-summary-display/discussion-summary-display.component.ts new file mode 100644 index 00000000000..b701a0c131a --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/discussion-summary-display/discussion-summary-display.component.ts @@ -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: PostReply 1Reply 2. + 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.replies.map((reply) => `${reply}`).join('')}`, + '' + ); + } + + 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; + } +} diff --git a/src/messages.xlf b/src/messages.xlf index f0bae20cbf5..89c84a09e92 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -21846,6 +21846,66 @@ If this problem continues, let your teacher know and move on to the next activit 401 + + Generate Class Summary + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 5,7 + + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 5,7 + + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 5,7 + + + + *New responses since last summary + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 12,16 + + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 12,16 + + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 12,16 + + + + Summary generated from responses + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 18,22 + + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 18,22 + + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 18,22 + + + + No student responses + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 23,25 + + + src/assets/wise5/directives/teacher-summary-display/ai-summary-display/ai-summary-display.component.html + 23,25 + + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 23,25 + + Student Ideas Detected @@ -21930,34 +21990,6 @@ If this problem continues, let your teacher know and move on to the next activit 58,61 - - Generate Class Summary - - src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 5,7 - - - - *New responses since last summary - - src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 12,16 - - - - Summary generated from responses - - src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 18,22 - - - - No student responses - - src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 23,25 - - The student will see a graph of their individual data here.