diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.html b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.html index 906d9eac220..5f200e63117 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.html +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.html @@ -34,6 +34,7 @@

} @if (component.hasResponsesSummary && component.type === 'MultipleChoice') { } @if (component.hasScoresSummary && component.hasScoreAnnotation) { [doRender]="true" /> } + @if (component.hasResponsesSummary && component.type === 'DialogGuidance') { + + } } diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.scss b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.scss index 983bc02da49..26d22af08da 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.scss +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.scss @@ -38,3 +38,8 @@ width: auto; } } + +.summary-display { + display: block; + margin: 16px 0; +} diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.ts index 2abf08445e9..8ea03190e0c 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/shared/node-info/node-info.component.ts @@ -1,34 +1,36 @@ -import { Component, Input } from '@angular/core'; -import { SummaryService } from '../../../../components/summary/summaryService'; import { AnnotationService } from '../../../../services/annotationService'; +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { ComponentFactory } from '../../../../common/ComponentFactory'; import { ComponentServiceLookupService } from '../../../../services/componentServiceLookupService'; import { ComponentTypeService } from '../../../../services/componentTypeService'; -import { TeacherDataService } from '../../../../services/teacherDataService'; -import { TeacherProjectService } from '../../../../services/teacherProjectService'; -import { ComponentFactory } from '../../../../common/ComponentFactory'; +import { DialogGuidanceTeacherSummaryDisplayComponent } from '../../../../directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component'; +import { FlexLayoutModule } from '@angular/flex-layout'; import { isMatchingPeriods } from '../../../../common/period/period'; -import { Node } from '../../../../common/Node'; import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; import { MatDividerModule } from '@angular/material/divider'; -import { FlexLayoutModule } from '@angular/flex-layout'; +import { MatIconModule } from '@angular/material/icon'; +import { Node } from '../../../../common/Node'; import { PreviewComponentComponent } from '../../../../authoringTool/components/preview-component/preview-component.component'; +import { SummaryService } from '../../../../components/summary/summaryService'; +import { TeacherDataService } from '../../../../services/teacherDataService'; +import { TeacherProjectService } from '../../../../services/teacherProjectService'; import { TeacherSummaryDisplayComponent } from '../../../../directives/teacher-summary-display/teacher-summary-display.component'; -import { CommonModule } from '@angular/common'; @Component({ - imports: [ - CommonModule, - MatCardModule, - MatIconModule, - MatDividerModule, - FlexLayoutModule, - PreviewComponentComponent, - TeacherSummaryDisplayComponent - ], - selector: 'node-info', - styleUrl: 'node-info.component.scss', - templateUrl: 'node-info.component.html' + imports: [ + DialogGuidanceTeacherSummaryDisplayComponent, + CommonModule, + MatCardModule, + MatIconModule, + MatDividerModule, + FlexLayoutModule, + PreviewComponentComponent, + TeacherSummaryDisplayComponent + ], + selector: 'node-info', + styleUrl: 'node-info.component.scss', + templateUrl: 'node-info.component.html' }) export class NodeInfoComponent { protected node: Node; diff --git a/src/assets/wise5/components/common/cRater/CRaterRubric.ts b/src/assets/wise5/components/common/cRater/CRaterRubric.ts index 59b118a166e..a93eea422b8 100644 --- a/src/assets/wise5/components/common/cRater/CRaterRubric.ts +++ b/src/assets/wise5/components/common/cRater/CRaterRubric.ts @@ -10,6 +10,10 @@ export class CRaterRubric { getIdea(ideaId: string): CRaterIdea { return this.ideas.find((idea) => idea.name === ideaId); } + + getIdeas(): CRaterIdea[] { + return this.ideas; + } } export function getUniqueIdeas(responses: any[], rubric: CRaterRubric): CRaterIdea[] { diff --git a/src/assets/wise5/components/summary/summaryService.ts b/src/assets/wise5/components/summary/summaryService.ts index 8951287bc03..61d6139170b 100644 --- a/src/assets/wise5/components/summary/summaryService.ts +++ b/src/assets/wise5/components/summary/summaryService.ts @@ -1,8 +1,8 @@ 'use strict'; import { ComponentService } from '../componentService'; -import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @Injectable() @@ -26,7 +26,7 @@ export class SummaryService extends ComponentService { 'OpenResponse', 'Table' ]; - this.componentsWithResponsesSummary = ['MultipleChoice', 'Table']; + this.componentsWithResponsesSummary = ['DialogGuidance', 'MultipleChoice', 'Table']; } getComponentTypeLabel(): string { diff --git a/src/assets/wise5/directives/summary-display/summary-display.component.html b/src/assets/wise5/directives/summary-display/summary-display.component.html index d3af9269cd3..5cfd857cb7c 100644 --- a/src/assets/wise5/directives/summary-display/summary-display.component.html +++ b/src/assets/wise5/directives/summary-display/summary-display.component.html @@ -1,4 +1,4 @@ - + @if (hasWarning) {

{{ warningMessage }}

diff --git a/src/assets/wise5/directives/summary-display/summary-display.component.scss b/src/assets/wise5/directives/summary-display/summary-display.component.scss index be612a5974a..ec03009306b 100644 --- a/src/assets/wise5/directives/summary-display/summary-display.component.scss +++ b/src/assets/wise5/directives/summary-display/summary-display.component.scss @@ -1,7 +1,3 @@ -.summary-card { - margin: 16px 8px 8px; -} - .highcharts-chart { display: block; height: 400px; diff --git a/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html b/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html new file mode 100644 index 00000000000..c24dcac29d4 --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html @@ -0,0 +1,65 @@ + +
+ {{ idea.id }}. {{ idea.text }} (person{{ idea.count }}) +
+
+ + + +

Student Ideas Detected

+ @if (hasWarning) { +

{{ warningMessage }}

+ } + @if (doRender) { +
+
+

Most Common:

+
    + @for (idea of mostCommonIdeas; track idea.id) { +
  • + +
  • + } +
+
+
+

Least Common:

+
    + @for (idea of leastCommonIdeas; track idea.id) { +
  • + +
  • + } +
+
+
+ @if (seeAllIdeas) { +

All Ideas:

+
    + @for (idea of allIdeas; track idea.id) { +
  • + +
  • + } +
+ Hide all ideas + } @else { + Show all ideas + } + } @else { +
+ Your students' ideas will show up here as they are detected in the dialog. +
+ } +
+
diff --git a/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.spec.ts b/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.spec.ts new file mode 100644 index 00000000000..76446da3640 --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.spec.ts @@ -0,0 +1,135 @@ +import { AnnotationService } from '../../services/annotationService'; +import { ComponentFixture } from '@angular/core/testing'; +import { ComponentState } from '../../../../app/domain/componentState'; +import { ConfigService } from '../../services/configService'; +import { CRaterIdea } from '../../components/common/cRater/CRaterIdea'; +import { CRaterRubric } from '../../components/common/cRater/CRaterRubric'; +import { CRaterService } from '../../services/cRaterService'; +import { DialogGuidanceTeacherSummaryDisplayComponent } from './dialog-guidance-teacher-summary-display.component'; +import { MockProviders } from 'ng-mocks'; +import { Observable, of } from 'rxjs'; +import { SummaryService } from '../../components/summary/summaryService'; +import { TeacherDataService } from '../../services/teacherDataService'; +import { TeacherProjectService } from '../../services/teacherProjectService'; +import { TestBed } from '@angular/core/testing'; + +let component: DialogGuidanceTeacherSummaryDisplayComponent; +let fixture: ComponentFixture; +describe('DialogGuidanceTeacherSummaryDisplayComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DialogGuidanceTeacherSummaryDisplayComponent], + providers: [ + MockProviders( + AnnotationService, + ConfigService, + CRaterService, + TeacherDataService, + TeacherProjectService, + SummaryService + ) + ] + }).compileComponents(); + + fixture = TestBed.createComponent(DialogGuidanceTeacherSummaryDisplayComponent); + component = fixture.componentInstance; + component.doRender = true; + // Set up component? + }); + beforeEach(() => { + spyOn(TestBed.inject(ConfigService), 'isPreview').and.returnValue(false); + spyOn(TestBed.inject(ConfigService), 'isAuthoring').and.returnValue(false); + spyOn(TestBed.inject(ConfigService), 'isStudentRun').and.returnValue(false); + spyOn(TestBed.inject(ConfigService), 'getNumberOfWorkgroupsInPeriod').and.returnValue(1); + }); + ngOnInit(); +}); + +function ngOnInit() { + describe('ngOnChanges()', () => { + ngInit_NoIdeasDetected_ShowMessage(); + ngInit_IdeasDetected_ShowSummary(); + ngInit_ManyIdeasDetected_ShowTopAndBottomThree(); + }); +} + +function ngInit_NoIdeasDetected_ShowMessage() { + describe('no ideas detected', () => { + beforeEach(() => { + spyOn(TestBed.inject(CRaterService), 'getCRaterRubric').and.returnValue( + generateMockRubric(3, 0) + ); + spyOn(TestBed.inject(SummaryService), 'getLatestClassmateStudentWork').and.returnValue( + generateMockStudentWork(0) + ); + }); + it('shows message to teacher', () => { + component.ngOnInit(); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.notice').textContent).toContain( + "Your students' ideas will show up here as they are detected in the dialog." + ); + }); + }); +} + +function ngInit_IdeasDetected_ShowSummary() { + describe('ideas detected', () => { + beforeEach(() => { + spyOn(TestBed.inject(CRaterService), 'getCRaterRubric').and.returnValue( + generateMockRubric(3, 1) + ); + spyOn(TestBed.inject(SummaryService), 'getLatestClassmateStudentWork').and.returnValue( + generateMockStudentWork(1) + ); + }); + it('shows summary display', () => { + component.ngOnInit(); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('h3').textContent).toEqual('Most Common:'); + }); + }); +} + +function ngInit_ManyIdeasDetected_ShowTopAndBottomThree() { + describe('more than 3 ideas detected', () => { + beforeEach(() => { + spyOn(TestBed.inject(CRaterService), 'getCRaterRubric').and.returnValue( + generateMockRubric(4, 4) + ); + spyOn(TestBed.inject(SummaryService), 'getLatestClassmateStudentWork').and.returnValue( + generateMockStudentWork(4) + ); + }); + it('shows only top and bottom three ideas', () => { + component.ngOnInit(); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelectorAll('#most-common-ideas > li').length).toEqual(3); + expect(fixture.nativeElement.querySelectorAll('#least-common-ideas > li').length).toEqual(3); + }); + }); +} + +function generateMockRubric(numIdeas: number, numDetected: number): CRaterRubric { + const ideas = []; + for (let i = 0; i < numIdeas; i++) { + const idea = new CRaterIdea('idea ' + (i + 1), numDetected > 0 ? true : false); + ideas.push(idea); + numDetected--; + } + return new CRaterRubric({ ideas: ideas }); +} + +function generateMockStudentWork(numIdeasDetected: number): Observable { + const ideas = []; + for (let i = 0; i < numIdeasDetected; i++) { + ideas.push({ name: 'idea ' + (i + 1), detected: true }); + } + return of([ + new ComponentState({ + workgroupId: 1, + studentData: { responses: [{ ideas: ideas }] } + }) + ]); +} diff --git a/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.ts b/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.ts new file mode 100644 index 00000000000..c0307108509 --- /dev/null +++ b/src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.ts @@ -0,0 +1,121 @@ +import { AnnotationService } from '../../services/annotationService'; +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { ComponentState } from '../../../../app/domain/componentState'; +import { ConfigService } from '../../services/configService'; +import { CRaterIdea } from '../../components/common/cRater/CRaterIdea'; +import { CRaterRubric } from '../../components/common/cRater/CRaterRubric'; +import { CRaterService } from '../../services/cRaterService'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { SummaryService } from '../../components/summary/summaryService'; +import { TeacherDataService } from '../../services/teacherDataService'; +import { TeacherProjectService } from '../../services/teacherProjectService'; +import { TeacherSummaryDisplayComponent } from './teacher-summary-display.component'; + +@Component({ + imports: [CommonModule, MatCardModule, MatIconModule], + selector: 'dialog-guidance-teacher-summary-display', + styles: ` + h3 { + margin-bottom: 8px; + } + + .idea { + @apply px-2 py-1 rounded-md bg-gray-100 my-1 text-sm; + } + + .mat-icon { + vertical-align: middle; + } + `, + templateUrl: 'dialog-guidance-teacher-summary-display.component.html' +}) +export class DialogGuidanceTeacherSummaryDisplayComponent extends TeacherSummaryDisplayComponent { + protected allIdeas: { id: string; text: string; count: number }[] = []; + protected ideaCountMap: Map> = new Map>(); + protected leastCommonIdeas: { id: string; text: string; count: number }[] = []; + protected mostCommonIdeas: { id: string; text: string; count: number }[] = []; + private rubric: CRaterRubric; + protected seeAllIdeas: boolean; + + constructor( + protected annotationService: AnnotationService, + protected configService: ConfigService, + private cRaterService: CRaterService, + protected dataService: TeacherDataService, + protected projectService: TeacherProjectService, + protected summaryService: SummaryService + ) { + super(annotationService, configService, dataService, projectService, summaryService); + } + + ngOnInit(): void { + this.rubric = this.cRaterService.getCRaterRubric(this.nodeId, this.componentId); + this.getLatestWork().subscribe((componentStates) => { + this.extractIdeas(componentStates); + this.allIdeas = this.getAllIdeas(); + if (!this.allIdeas.some((idea) => this.ideaCountMap.get(idea.id)?.size > 0)) { + this.doRender = false; + } else { + const sortedIdeas = this.sortIdeas(); + this.mostCommonIdeas = [...sortedIdeas].splice(0, 3); + this.leastCommonIdeas = [...sortedIdeas] + .splice(sortedIdeas.length - 3, sortedIdeas.length) + .reverse(); + } + }); + } + + private getAllIdeas(): { id: string; text: string; count: number }[] { + return this.rubric.getIdeas().map((idea) => ({ + id: idea.name, + text: this.useIdeaTextOrId(idea.name, idea.text), + count: this.ideaCountMap.get(idea.name)?.size ?? 0 + })); + } + + private useIdeaTextOrId(id: string, text: string): string { + return text ?? 'idea ' + id; + } + + private extractIdeas(componentStates: ComponentState[]): void { + componentStates.forEach((componentState) => + this.getDetectedIdeas(componentState).forEach((idea) => { + if (this.ideaCountMap.has(idea.name)) { + this.ideaCountMap.get(idea.name).add(componentState.workgroupId); + } else { + this.ideaCountMap.set(idea.name, new Set([componentState.workgroupId])); + } + }) + ); + } + + private getDetectedIdeas(componentState: ComponentState): CRaterIdea[] { + return componentState.studentData.responses.flatMap( + (response) => + response.ideas + ?.filter((idea) => idea.detected) + .map((idea) => new CRaterIdea(idea.name, idea.detected)) ?? [] + ); + } + + private sortIdeas(): { id: string; text: string; count: number }[] { + return [...this.ideaCountMap.entries()] + .sort((a, b) => b[1].size - a[1].size) + .map((mapIterator) => ({ + id: mapIterator[0], + text: this.getIdeaText(mapIterator[0]), + count: mapIterator[1].size + })); + } + + private getIdeaText(id: string): string { + return this.useIdeaTextOrId(id, this.rubric.getIdea(id).text); + } + + protected toggleSeeAllIdeas(event: Event): void { + event.preventDefault(); + this.seeAllIdeas = !this.seeAllIdeas; + } +} diff --git a/src/messages.xlf b/src/messages.xlf index 7bee4632296..b235ff3c415 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -21290,6 +21290,55 @@ If this problem continues, let your teacher know and move on to the next activit 424 + + Student Ideas Detected + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 10,12 + + + + Most Common: + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 17,19 + + + + Least Common: + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 30,32 + + + + All Ideas: + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 44,46 + + + + Hide all ideas + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 55,57 + + + + Show all ideas + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 57,61 + + + + Your students' ideas will show up here as they are detected in the dialog. + + src/assets/wise5/directives/teacher-summary-display/dialog-guidance-teacher-summary-display.component.html + 61,66 + + The student will see a graph of their individual data here.