From 060a9c44ed1a6f8531a3c38bc454ede3a837b7d1 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Thu, 22 Jan 2026 15:32:42 -0800 Subject: [PATCH 1/6] feat(OpenResponse): Summarize student responses in grading tool --- src/app/services/localStorageService.ts | 40 ++ .../component-summary.component.html | 12 + .../component-summary.component.ts | 7 + ...en-response-summary-display.component.html | 16 + ...response-summary-display.component.spec.ts | 377 ++++++++++++++++++ ...open-response-summary-display.component.ts | 95 +++++ src/messages.xlf | 28 ++ 7 files changed, 575 insertions(+) create mode 100644 src/app/services/localStorageService.ts create mode 100644 src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html create mode 100644 src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts create mode 100644 src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts diff --git a/src/app/services/localStorageService.ts b/src/app/services/localStorageService.ts new file mode 100644 index 00000000000..08f2cf8926e --- /dev/null +++ b/src/app/services/localStorageService.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LocalStorageService { + setItem(key: string, value: any): void { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (e) { + console.error('Error saving to local storage', e); + } + } + + getItem(key: string): any { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : null; + } catch (e) { + console.error('Error reading from local storage', e); + return null; + } + } + + removeItem(key: string): void { + try { + localStorage.removeItem(key); + } catch (e) { + console.error('Error removing from local storage', e); + } + } + + clear(): void { + try { + localStorage.clear(); + } catch (e) { + console.error('Error clearing local storage', e); + } + } +} 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 dffde8abbd6..f7c1a5d0092 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 @@ -72,6 +72,18 @@ /> + } @else if (component?.type === 'OpenResponse') { + + + + + } } 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 2fa2c1ff40b..7c8d98686f8 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 @@ -14,6 +14,8 @@ import { IdeasSummaryComponent } from '../../../directives/teacher-summary-displ import { MatchSummaryDisplayComponent } from '../../../directives/teacher-summary-display/match-summary-display/match-summary-display.component'; 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'; @Component({ imports: [ @@ -22,6 +24,7 @@ import { CRaterService } from '../../../services/cRaterService'; MatCardModule, MatchSummaryDisplayComponent, MilestoneReportButtonComponent, + OpenResponseSummaryDisplayComponent, PeerGroupButtonComponent, TeacherSummaryDisplayComponent ], @@ -48,6 +51,7 @@ export class ComponentSummaryComponent { private componentServiceLookupService: ComponentServiceLookupService, private cRaterService: CRaterService, private dataService: TeacherDataService, + private projectService: ProjectService, private summaryService: SummaryService ) {} @@ -79,6 +83,9 @@ export class ComponentSummaryComponent { (this.hasScoresSummary && this.hasScoreAnnotation) || this.hasIdeaRubricData || this.component?.type === 'Match'; + if (this.component?.type === 'OpenResponse') { + this.hasSummaryData = this.projectService.getProject().ai?.enabled; + } } private setSource(): void { diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html new file mode 100644 index 00000000000..349a77e7806 --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html @@ -0,0 +1,16 @@ +@if (hasStudentResponses) { + + @if (newSummaryAvailable) { + (New Student Summary Available) + } + @if (generatingSummary) { + + } @else if (summary) { + + ({{ getLatestPeriodComponentStates().length }} responses) + } +} @else { + No student responses +} diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts new file mode 100644 index 00000000000..8c990355f20 --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts @@ -0,0 +1,377 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { OpenResponseSummaryDisplayComponent } from './open-response-summary-display.component'; +import { MockComponent, MockProviders } from 'ng-mocks'; +import { AnnotationService } from '../../../services/annotationService'; +import { ConfigService } from '../../../services/configService'; +import { CRaterService } from '../../../services/cRaterService'; +import { ProjectService } from '../../../services/projectService'; +import { SummaryService } from '../../../components/summary/summaryService'; +import { AwsBedRockService } from '../../../../../app/chatbot/awsBedRock.service'; +import { TeacherDataService } from '../../../services/teacherDataService'; +import { LocalStorageService } from '../../../../../app/services/localStorageService'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { DataService } from '../../../../../app/services/data.service'; +import { MarkdownComponent, MarkdownService } from 'ngx-markdown'; + +describe('OpenResponseSummaryDisplayComponent', () => { + let component: OpenResponseSummaryDisplayComponent; + let fixture: ComponentFixture; + let awsBedRockService: AwsBedRockService; + let localStorageService: LocalStorageService; + let dataService: TeacherDataService; + let projectService: ProjectService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OpenResponseSummaryDisplayComponent, MockComponent(MarkdownComponent)], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: DataService, useExisting: TeacherDataService }, + MockProviders( + AnnotationService, + AwsBedRockService, + ConfigService, + CRaterService, + LocalStorageService, + MarkdownService, + ProjectService, + SummaryService, + TeacherDataService + ) + ] + }).compileComponents(); + + awsBedRockService = TestBed.inject(AwsBedRockService); + localStorageService = TestBed.inject(LocalStorageService); + dataService = TestBed.inject(TeacherDataService) as TeacherDataService; + projectService = TestBed.inject(ProjectService); + + spyOn(projectService, 'getComponent').and.returnValue({ + id: 'component1', + type: 'OpenResponse', + prompt: 'What is your opinion on climate change?' + } as any); + + fixture = TestBed.createComponent(OpenResponseSummaryDisplayComponent); + component = fixture.componentInstance; + component.nodeId = 'node1'; + component.componentId = 'component1'; + component.periodId = 1; + }); + + describe('ngOnInit', () => { + it('should call renderDisplay', () => { + spyOn(component as any, 'renderDisplay'); + component.ngOnInit(); + expect((component as any).renderDisplay).toHaveBeenCalled(); + }); + }); + + describe('renderDisplay', () => { + it('should set hasStudentResponses to false when no component states exist', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue([]); + component.ngOnInit(); + fixture.detectChanges(); + expect(component['hasStudentResponses']).toBe(false); + }); + + it('should set hasStudentResponses to true when component states exist', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + fixture.detectChanges(); + expect(component['hasStudentResponses']).toBe(true); + }); + + it('should load summary from localStorage if it exists', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + const savedSummary = 'This is a saved summary'; + spyOn(localStorageService, 'getItem').and.returnValues(savedSummary, 1000); + fixture.detectChanges(); + expect(component['summary']).toBe(savedSummary); + }); + + it('should set newSummaryAvailable to true when responses are newer than summary', () => { + const componentStates = getComponentStates(); + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(componentStates); + const oldTimestamp = 1000; + spyOn(localStorageService, 'getItem').and.returnValues('Old summary', oldTimestamp); + fixture.detectChanges(); + expect(component['newSummaryAvailable']).toBe(true); + }); + + it('should set newSummaryAvailable to false when summary is newer than responses', () => { + const componentStates = getComponentStates(); + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(componentStates); + const futureTimestamp = Date.now() + 100000; + spyOn(localStorageService, 'getItem').and.returnValues('Recent summary', futureTimestamp); + component.ngOnInit(); + fixture.detectChanges(); + expect(component['newSummaryAvailable']).toBe(false); + }); + }); + + describe('getLatestPeriodComponentStates', () => { + it('should filter component states by period ID', () => { + const componentStates = getComponentStates(); + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(componentStates); + component.periodId = 1; + const result = component['getLatestPeriodComponentStates'](); + expect(result.every((state) => state.periodId === 1)).toBe(true); + }); + + it('should return all component states when periodId is -1', () => { + const componentStates = [ + ...getComponentStates(), + { + id: 4, + componentId: 'component1', + nodeId: 'node1', + periodId: 2, + runId: 1, + serverSaveTime: 4000, + studentData: { response: 'Response from period 2' }, + workgroupId: 4 + } + ]; + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(componentStates); + component.periodId = -1; + const result = component['getLatestPeriodComponentStates'](); + expect(result.length).toBe(4); + }); + + it('should return only the latest state per workgroup', () => { + const componentStates = [ + ...getComponentStates(), + { + id: 4, + componentId: 'component1', + nodeId: 'node1', + periodId: 1, + runId: 1, + serverSaveTime: 5000, + studentData: { response: 'Updated response from workgroup 1' }, + workgroupId: 1 + } + ]; + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(componentStates); + component.periodId = 1; + const result = component['getLatestPeriodComponentStates'](); + const workgroup1States = result.filter((state) => state.workgroupId === 1); + expect(workgroup1States.length).toBe(1); + expect(workgroup1States[0].serverSaveTime).toBe(5000); + }); + }); + + describe('generateSummary', () => { + beforeEach(() => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should call awsBedRockService with correct system prompt', async () => { + const sendMessageSpy = spyOn(awsBedRockService, 'sendMessage').and.returnValue( + Promise.resolve('Generated summary') + ); + await component['generateSummary'](); + const messages = sendMessageSpy.calls.mostRecent().args[0]; + expect(messages[0].role).toBe('system'); + expect(messages[0].content).toContain('What is your opinion on climate change?'); + }); + + it('should call awsBedRockService with student responses', async () => { + const sendMessageSpy = spyOn(awsBedRockService, 'sendMessage').and.returnValue( + Promise.resolve('Generated summary') + ); + await component['generateSummary'](); + const messages = sendMessageSpy.calls.mostRecent().args[0]; + expect(messages[1].role).toBe('user'); + expect(messages[1].content).toContain(''); + expect(messages[1].content).toContain('Climate change is real'); + }); + + it('should save summary to localStorage', async () => { + const generatedSummary = 'This is a generated summary'; + spyOn(awsBedRockService, 'sendMessage').and.returnValue(Promise.resolve(generatedSummary)); + const setItemSpy = spyOn(localStorageService, 'setItem'); + await component['generateSummary'](); + expect(setItemSpy).toHaveBeenCalledWith( + 'openResponseSummary-1-node1-component1', + generatedSummary + ); + }); + + it('should save timestamp to localStorage', async () => { + spyOn(awsBedRockService, 'sendMessage').and.returnValue(Promise.resolve('Generated summary')); + const setItemSpy = spyOn(localStorageService, 'setItem'); + const beforeTime = new Date().getTime(); + await component['generateSummary'](); + const afterTime = new Date().getTime(); + const timestampCall = setItemSpy.calls + .all() + .find((call) => call.args[0].includes('timestamp')); + expect(timestampCall).toBeDefined(); + expect(timestampCall.args[1]).toBeGreaterThanOrEqual(beforeTime); + expect(timestampCall.args[1]).toBeLessThanOrEqual(afterTime); + }); + + it('should set generatingSummary to false after completion', async () => { + spyOn(awsBedRockService, 'sendMessage').and.returnValue(Promise.resolve('Generated summary')); + await component['generateSummary'](); + expect(component['generatingSummary']).toBe(false); + }); + + it('should set newSummaryAvailable to false after generation', async () => { + component['newSummaryAvailable'] = true; + spyOn(awsBedRockService, 'sendMessage').and.returnValue(Promise.resolve('Generated summary')); + await component['generateSummary'](); + expect(component['newSummaryAvailable']).toBe(false); + }); + + it('should update summary property', async () => { + const generatedSummary = 'This is a generated summary'; + spyOn(awsBedRockService, 'sendMessage').and.returnValue(Promise.resolve(generatedSummary)); + await component['generateSummary'](); + expect(component['summary']).toBe(generatedSummary); + }); + }); + + describe('getStudentResponses', () => { + it('should format student responses with XML tags', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + const responses = component['getStudentResponses'](); + expect(responses).toContain('Climate change is real'); + expect(responses).toContain('We need to act now'); + expect(responses).toContain('Renewable energy is the future'); + }); + + it('should concatenate all responses', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + const responses = component['getStudentResponses'](); + const responseCount = (responses.match(//g) || []).length; + expect(responseCount).toBe(3); + }); + }); + + describe('template rendering', () => { + it('should display "No Student Responses" when hasStudentResponses is false', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue([]); + component.ngOnInit(); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.textContent).toContain('No Student Responses'); + }); + + it('should display generate button when hasStudentResponses is true', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + fixture.detectChanges(); + const button = fixture.nativeElement.querySelector('button'); + expect(button).toBeTruthy(); + expect(button.textContent).toContain('Generate Student Summary'); + }); + + it('should disable generate button when generatingSummary is true', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + component['generatingSummary'] = true; + fixture.detectChanges(); + const button = fixture.nativeElement.querySelector('button'); + expect(button.disabled).toBe(true); + }); + + it('should display "New Student Summary Available" when newSummaryAvailable is true', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + const oldTimestamp = 1000; + spyOn(localStorageService, 'getItem') + .withArgs('openResponseSummary-1-node1-component1') + .and.returnValue('Old summary') + .withArgs('openResponseSummary-timestamp-1-node1-component1') + .and.returnValue(oldTimestamp); + component.ngOnInit(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('New Student Summary Available'); + }); + + it('should display spinner when generatingSummary is true', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + component.ngOnInit(); + component['generatingSummary'] = true; + fixture.detectChanges(); + const spinner = fixture.nativeElement.querySelector('mat-spinner'); + expect(spinner).toBeTruthy(); + }); + + it('should display summary when summary exists', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + const savedSummary = 'This is a saved summary'; + spyOn(localStorageService, 'getItem') + .withArgs('openResponseSummary-1-node1-component1') + .and.returnValue(savedSummary) + .withArgs('openResponseSummary-timestamp-1-node1-component1') + .and.returnValue(Date.now() + 100000); + component.ngOnInit(); + fixture.detectChanges(); + const markdown = fixture.nativeElement.querySelector('markdown'); + expect(markdown).toBeTruthy(); + }); + + it('should display response count when summary exists', () => { + spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); + const savedSummary = 'This is a saved summary'; + spyOn(localStorageService, 'getItem') + .withArgs('openResponseSummary-1-node1-component1') + .and.returnValue(savedSummary) + .withArgs('openResponseSummary-timestamp-1-node1-component1') + .and.returnValue(Date.now() + 100000); + component.ngOnInit(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('3 responses'); + }); + }); +}); + +function getComponentStates(): any[] { + return [ + { + id: 1, + componentId: 'component1', + nodeId: 'node1', + periodId: 1, + runId: 1, + serverSaveTime: 1000, + studentData: { + response: 'Climate change is real' + }, + workgroupId: 1 + }, + { + id: 2, + componentId: 'component1', + nodeId: 'node1', + periodId: 1, + runId: 1, + serverSaveTime: 2000, + studentData: { + response: 'We need to act now' + }, + workgroupId: 2 + }, + { + id: 3, + componentId: 'component1', + nodeId: 'node1', + periodId: 1, + runId: 1, + serverSaveTime: 3000, + studentData: { + response: 'Renewable energy is the future' + }, + workgroupId: 3 + } + ]; +} diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts new file mode 100644 index 00000000000..19c089a9e8c --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts @@ -0,0 +1,95 @@ +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'; + +@Component({ + imports: [MarkdownComponent, MatButton, MatIcon, MatProgressSpinner], + selector: 'open-response-summary', + templateUrl: './open-response-summary-display.component.html' +}) +export class OpenResponseSummaryDisplayComponent 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( + `openResponseSummary-${this.periodId}-${this.nodeId}-${this.componentId}` + ) || ''; + this.summaryTimestamp = + this.localStorageService.getItem( + `openResponseSummary-timestamp-${this.periodId}-${this.nodeId}-${this.componentId}` + ) || 0; + const lastResponseTime = latestPeriodComponentStates.reduce((max, state) => { + return 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) + .reduceRight( + (soFar, currentState) => + soFar.find((state) => state.workgroupId === currentState.workgroupId) + ? soFar + : soFar.concat(currentState), + [] + ); + } + + protected async generateSummary(): Promise { + this.generatingSummary = true; + const prompt = this.projectService.getComponent(this.nodeId, this.componentId).prompt; + const systemPrompt = `You are a teacher who is summarizing student responses to the following question: "${prompt}". + Each student response is in the format: Response. + In the same language as the question, provide a summary of the responses in 100 words or less.`; + const messages = [ + new ChatMessage('system', systemPrompt, this.nodeId), + new ChatMessage('user', this.getStudentResponses(), this.nodeId) + ]; + this.summary = await this.awsBedRockService.sendMessage(messages); + this.localStorageService.setItem( + `openResponseSummary-${this.periodId}-${this.nodeId}-${this.componentId}`, + this.summary + ); + this.localStorageService.setItem( + `openResponseSummary-timestamp-${this.periodId}-${this.nodeId}-${this.componentId}`, + new Date().getTime() + ); + this.generatingSummary = false; + this.newSummaryAvailable = false; + } + + private getStudentResponses(): string { + return this.getLatestPeriodComponentStates().reduce( + (soFar, state) => `${soFar}${state.studentData.response}`, + '' + ); + } +} diff --git a/src/messages.xlf b/src/messages.xlf index 7ec3d98f169..3a357644cee 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -21930,6 +21930,34 @@ If this problem continues, let your teacher know and move on to the next activit 58,61 + + Generate Student Summary + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 3,5 + + + + New Student Summary Available + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 6,10 + + + + ( responses) + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 12,14 + + + + No student responses + + src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html + 15,17 + + The student will see a graph of their individual data here. From 0ff9af03bb8edddd40251fdf91fdb6ee28c36fd4 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Thu, 22 Jan 2026 15:56:22 -0800 Subject: [PATCH 2/6] fix unit tests --- .../component-summary/component-summary.component.spec.ts | 2 ++ .../open-response-summary-display.component.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.spec.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.spec.ts index 3efa31d2f3a..7d2349d79a5 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.spec.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/component-summary/component-summary.component.spec.ts @@ -12,6 +12,7 @@ import { CRaterService } from '../../../services/cRaterService'; import { TeacherDataService } from '../../../services/teacherDataService'; import { SummaryService } from '../../../components/summary/summaryService'; import { PeerGroupButtonComponent } from '../peer-group-button/peer-group-button.component'; +import { ProjectService } from '../../../services/projectService'; let component: ComponentSummaryComponent; let fixture: ComponentFixture; @@ -31,6 +32,7 @@ describe('ComponentSummaryComponent', () => { AnnotationService, ComponentServiceLookupService, CRaterService, + ProjectService, SummaryService ), MockProvider(TeacherDataService, { diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts index 8c990355f20..83ed070f76e 100644 --- a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts @@ -258,12 +258,12 @@ describe('OpenResponseSummaryDisplayComponent', () => { }); describe('template rendering', () => { - it('should display "No Student Responses" when hasStudentResponses is false', () => { + it('should display "No student responses" when hasStudentResponses is false', () => { spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue([]); component.ngOnInit(); fixture.detectChanges(); const compiled = fixture.nativeElement; - expect(compiled.textContent).toContain('No Student Responses'); + expect(compiled.textContent).toContain('No student responses'); }); it('should display generate button when hasStudentResponses is true', () => { From 53bb0a6a90fb75c58b6140ec3f23dd65d74dcf3b Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Tue, 27 Jan 2026 09:32:50 -0800 Subject: [PATCH 3/6] Update styles and layout and show date and time of latest summary --- ...en-response-summary-display.component.html | 30 ++++++++++++------- ...response-summary-display.component.spec.ts | 6 ++-- ...open-response-summary-display.component.ts | 5 +++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html index 349a77e7806..51c54f13f34 100644 --- a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html @@ -1,16 +1,24 @@ @if (hasStudentResponses) { - - @if (newSummaryAvailable) { - (New Student Summary Available) - } - @if (generatingSummary) { - - } @else if (summary) { +
+
+ + @if (generatingSummary) { + + } +
+ @if (newSummaryAvailable) { + New responses since last summary + } +
+ @if (summary) { - ({{ getLatestPeriodComponentStates().length }} responses) +
+ Summary generated {{ summaryDate | date: 'short' }} from + {{ getLatestPeriodComponentStates().length }} responses +
} } @else { - No student responses +
No student responses
} diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts index 83ed070f76e..65290c15839 100644 --- a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.spec.ts @@ -272,7 +272,7 @@ describe('OpenResponseSummaryDisplayComponent', () => { fixture.detectChanges(); const button = fixture.nativeElement.querySelector('button'); expect(button).toBeTruthy(); - expect(button.textContent).toContain('Generate Student Summary'); + expect(button.textContent).toContain('Generate Class Summary'); }); it('should disable generate button when generatingSummary is true', () => { @@ -284,7 +284,7 @@ describe('OpenResponseSummaryDisplayComponent', () => { expect(button.disabled).toBe(true); }); - it('should display "New Student Summary Available" when newSummaryAvailable is true', () => { + it('should display "New responses since last summary" when newSummaryAvailable is true', () => { spyOn(dataService, 'getComponentStatesByComponentId').and.returnValue(getComponentStates()); const oldTimestamp = 1000; spyOn(localStorageService, 'getItem') @@ -294,7 +294,7 @@ describe('OpenResponseSummaryDisplayComponent', () => { .and.returnValue(oldTimestamp); component.ngOnInit(); fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('New Student Summary Available'); + expect(fixture.nativeElement.textContent).toContain('New responses since last summary'); }); it('should display spinner when generatingSummary is true', () => { diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts index 19c089a9e8c..84c334adc63 100644 --- a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.ts @@ -1,3 +1,4 @@ +import { DatePipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component'; import { MatButton } from '@angular/material/button'; @@ -10,7 +11,7 @@ import { LocalStorageService } from '../../../../../app/services/localStorageSer import { MarkdownComponent } from 'ngx-markdown'; @Component({ - imports: [MarkdownComponent, MatButton, MatIcon, MatProgressSpinner], + imports: [DatePipe, MarkdownComponent, MatButton, MatIcon, MatProgressSpinner], selector: 'open-response-summary', templateUrl: './open-response-summary-display.component.html' }) @@ -21,6 +22,7 @@ export class OpenResponseSummaryDisplayComponent extends TeacherSummaryDisplayCo private localStorageService: LocalStorageService = inject(LocalStorageService); protected newSummaryAvailable: boolean = false; protected summary: string; + protected summaryDate: Date; private summaryTimestamp: number; ngOnInit(): void { @@ -42,6 +44,7 @@ export class OpenResponseSummaryDisplayComponent extends TeacherSummaryDisplayCo this.localStorageService.getItem( `openResponseSummary-timestamp-${this.periodId}-${this.nodeId}-${this.componentId}` ) || 0; + this.summaryDate = new Date(this.summaryTimestamp); const lastResponseTime = latestPeriodComponentStates.reduce((max, state) => { return Math.max(max, state.serverSaveTime); }, 0); From 6f1d10195471a946485267bf3886b8273a5b0f37 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 27 Jan 2026 17:37:42 +0000 Subject: [PATCH 4/6] Updated messages --- src/messages.xlf | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/messages.xlf b/src/messages.xlf index 3a357644cee..d1757193940 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -21930,32 +21930,32 @@ If this problem continues, let your teacher know and move on to the next activit 58,61
- - Generate Student Summary + + Generate Class Summary src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 3,5 + 5,7 - - New Student Summary Available + + New responses since last summary src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 6,10 + 12,16 - - ( responses) + + Summary generated from responses src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 12,14 + 18,22 No student responses src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html - 15,17 + 23,25 From ae2cb7a588d7a58b784c34d377a6f9d0d35732cf Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 27 Jan 2026 11:51:06 -0800 Subject: [PATCH 5/6] Show newSummaryAvailable text next to generate summary button --- .../open-response-summary-display.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html index 51c54f13f34..f4bda35f709 100644 --- a/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html +++ b/src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html @@ -1,5 +1,5 @@ @if (hasStudentResponses) { -
+
@if (newSummaryAvailable) { - New responses since last summary + *New responses since last summary }
@if (summary) { From ba5e7667a7e006a6249d0ff04f845b96887847ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 27 Jan 2026 19:55:54 +0000 Subject: [PATCH 6/6] Updated messages --- src/messages.xlf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/messages.xlf b/src/messages.xlf index d1757193940..f0bae20cbf5 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -21937,8 +21937,8 @@ If this problem continues, let your teacher know and move on to the next activit 5,7 - - New responses since last summary + + *New responses since last summary src/assets/wise5/directives/teacher-summary-display/open-response-summary-display/open-response-summary-display.component.html 12,16