diff --git a/src/app/chatbot/awsBedRock.service.ts b/src/app/chatbot/awsBedRock.service.ts index 6762765f491..efa3fec6d78 100644 --- a/src/app/chatbot/awsBedRock.service.ts +++ b/src/app/chatbot/awsBedRock.service.ts @@ -4,7 +4,7 @@ import { ChatService } from './chat.service'; @Injectable({ providedIn: 'root' }) export class AwsBedRockService extends ChatService { protected chatEndpoint = '/api/aws-bedrock/chat'; - protected model: string = 'openai.gpt-oss-20b-1:0'; + protected model: string = 'google.gemma-3-27b-it'; processResponse(response: string): string { return response.replace(/.*?<\/reasoning>/gs, ''); diff --git a/src/app/services/ideasSortingService.spec.ts b/src/app/services/ideaData.spec.ts similarity index 54% rename from src/app/services/ideasSortingService.spec.ts rename to src/app/services/ideaData.spec.ts index 23ead62c83a..1dad77d1934 100644 --- a/src/app/services/ideasSortingService.spec.ts +++ b/src/app/services/ideaData.spec.ts @@ -1,15 +1,14 @@ -import { IdeaData } from '../../assets/wise5/components/common/cRater/IdeaData'; -import { IdeasSortingService } from '../../assets/wise5/services/ideasSortingService'; +import { + IdeaData, + sortIdeasByCount, + sortIdeasById +} from '../../assets/wise5/components/common/cRater/IdeaData'; import { TestBed } from '@angular/core/testing'; let ideas: IdeaData[]; -let service: IdeasSortingService; - -describe('IdeasSortingService', () => { +describe('IdeaData', () => { beforeEach(() => { - TestBed.configureTestingModule({ - providers: [IdeasSortingService] - }); + TestBed.configureTestingModule({}); ideas = [ createIdeaData('2', 'c', 3), createIdeaData('1', 'b', 1), @@ -17,23 +16,27 @@ describe('IdeasSortingService', () => { createIdeaData('10a', 'abc', 2), createIdeaData('11', 'cba', 5) ]; - service = TestBed.inject(IdeasSortingService); }); - sortIdeasByCount(); - sortIdeasById(); + test_SortIdeasByCount(); + test_SortIdeasById(); }); -function sortIdeasByCount() { +function test_SortIdeasByCount() { it('should sort ideas descending numerically by count', () => { - const sortedIdeas = service.sortByCount(ideas); + const sortedIdeas = sortIdeasByCount(ideas, 'desc'); expect(sortedIdeas.map((idea) => idea.id)).toEqual(['11', '2b', '2', '10a', '1']); }); + + it('should sort ideas ascending numerically by count', () => { + const sortedIdeas = sortIdeasByCount(ideas, 'asc'); + expect(sortedIdeas.map((idea) => idea.id)).toEqual(['1', '10a', '2', '2b', '11']); + }); } -function sortIdeasById() { +function test_SortIdeasById() { it('should sort ideas alphanumerically by ID', () => { - const sortedIdeas = service.sortById(ideas); + const sortedIdeas = sortIdeasById(ideas); expect(sortedIdeas.map((ideas) => ideas.id)).toEqual(['1', '2', '2b', '10a', '11']); }); } diff --git a/src/assets/wise5/common/array/array.ts b/src/assets/wise5/common/array/array.ts index b674c6ad1c3..700f65dc6f3 100644 --- a/src/assets/wise5/common/array/array.ts +++ b/src/assets/wise5/common/array/array.ts @@ -84,6 +84,17 @@ export function arraysContainSameValues(array1: string[], array2: string[]): boo return JSON.stringify(array1Copy) === JSON.stringify(array2Copy); } +/** + * Check if array1 contains all elements of array2. Even if array1 contains more elements + * than array2, it will still return true if array1 contains all elements of array2. + * @param array1 an array of strings + * @param array2 an array of strings + * @returns whether array1 contains all elements of array2 + */ +export function arrayContainsAll(array1: string[], array2: string[]): boolean { + return array2.every((value) => array1.includes(value)); +} + export function reduceByUniqueId(objArr: any[]): any[] { const idToObj = {}; const result = []; diff --git a/src/assets/wise5/components/common/cRater/CRaterIdea.ts b/src/assets/wise5/components/common/cRater/CRaterIdea.ts index d831935c641..f3147660c39 100644 --- a/src/assets/wise5/components/common/cRater/CRaterIdea.ts +++ b/src/assets/wise5/components/common/cRater/CRaterIdea.ts @@ -3,8 +3,9 @@ export class CRaterIdea { detected?: boolean; characterOffsets: any[]; text?: string; + tags?: string[]; - constructor(name: string, detected?: boolean, text?: string) { + constructor(name: string, detected?: boolean, text?: string, tags?: string[]) { this.name = name; if (detected) { this.detected = detected; @@ -12,5 +13,8 @@ export class CRaterIdea { if (text) { this.text = text; } + if (tags) { + this.tags = tags; + } } } diff --git a/src/assets/wise5/components/common/cRater/CRaterRubric.ts b/src/assets/wise5/components/common/cRater/CRaterRubric.ts index 5c1dc8afcf9..2cd004668ff 100644 --- a/src/assets/wise5/components/common/cRater/CRaterRubric.ts +++ b/src/assets/wise5/components/common/cRater/CRaterRubric.ts @@ -3,10 +3,14 @@ import { CRaterIdea } from './CRaterIdea'; export class CRaterRubric { description: string = ''; ideas: CRaterIdea[] = []; + ideasSummaryGroups?: any; + ideaColors?: { tags: string[]; colorValue: string }[]; constructor(rubric: any = { description: '', ideas: [] }) { this.description = rubric.description; this.ideas = rubric.ideas; + this.ideasSummaryGroups = rubric.ideasSummaryGroups ?? DEFAULT_IDEAS_SUMMARY_GROUPS; + this.ideaColors = rubric.ideaColors; } getIdea(ideaId: string): CRaterIdea { @@ -16,6 +20,14 @@ export class CRaterRubric { hasRubricData(): boolean { return (this.description ?? '') !== '' || this.ideas.length > 0; } + + getInitialIdeasSummaryGroups(): any[] { + return this.ideasSummaryGroups.initial; + } + + getAdditionalIdeasSummaryGroups(): any[] { + return this.ideasSummaryGroups.additional; + } } export function getUniqueIdeas(responses: any[], rubric: CRaterRubric): CRaterIdea[] { @@ -34,3 +46,37 @@ export function getUniqueIdeas(responses: any[], rubric: CRaterRubric): CRaterId ); return uniqueIdeas; } + +export const DEFAULT_IDEAS_SUMMARY_GROUPS = { + initial: [ + { + maxIdeas: 3, + title: $localize`Most Common`, + tags: [], + sort: { + field: 'count', + order: 'desc' + } + }, + { + maxIdeas: 3, + title: $localize`Unique Ideas`, + tags: [], + sort: { + field: 'count', + order: 'asc' + } + } + ], + additional: [ + { + title: $localize`All Ideas`, + tags: [], + sort: { + field: 'count', + order: 'desc' + }, + showUndetectedIdeas: true + } + ] +}; diff --git a/src/assets/wise5/components/common/cRater/IdeaData.ts b/src/assets/wise5/components/common/cRater/IdeaData.ts index b1c7d4d402c..26d937c118b 100644 --- a/src/assets/wise5/components/common/cRater/IdeaData.ts +++ b/src/assets/wise5/components/common/cRater/IdeaData.ts @@ -4,12 +4,73 @@ export type IdeaData = { id: string; text: string; count: number; + tags?: string[]; + color?: string; }; export function ideaDataToCRaterIdea(ideaData: IdeaData): CRaterIdea { - return new CRaterIdea(ideaData.id, undefined, ideaData.text); + return new CRaterIdea(ideaData.id, undefined, ideaData.text, ideaData.tags); } export function cRaterIdeaToIdeaData(cRaterIdea: CRaterIdea): IdeaData { - return { id: cRaterIdea.name, text: cRaterIdea.text, count: 0 }; + return { id: cRaterIdea.name, text: cRaterIdea.text, count: 0, tags: cRaterIdea.tags }; +} + +export function sortIdeasByCount(ideas: IdeaData[], sortOrder: 'asc' | 'desc'): IdeaData[] { + return ideas.sort((a, b) => (sortOrder === 'asc' ? a.count - b.count : b.count - a.count)); +} + +export function sortIdeasById(ideas: IdeaData[]): IdeaData[] { + const sorted = ideas + .filter((idea) => !stringContainsLetters(idea.id)) + .sort((a, b) => Number(a.id) - Number(b.id)); + const sortedIdeasWithLetters = getSortedIdeasWithLetters(ideas); + return insertIdeasWithLetters(sorted, sortedIdeasWithLetters); +} + +function getSortedIdeasWithLetters(ideas: IdeaData[]): IdeaData[] { + return ideas + .filter((idea) => stringContainsLetters(idea.id)) + .sort((a, b) => compareByStringNumericPrefix(a, b)); +} + +function stringContainsLetters(str: string): boolean { + return Array.from(str).some((char) => isNaN(Number(char))); +} + +function compareByStringNumericPrefix(idea: IdeaData, otherIdea: IdeaData): number { + const prefixDif = stringNumericPrefix(idea.id) - stringNumericPrefix(otherIdea.id); + return prefixDif === 0 ? idea.id.localeCompare(otherIdea.id) : prefixDif; +} + +function insertIdeasWithLetters( + sorted: IdeaData[], + sortedIdeasWithLetters: IdeaData[] +): IdeaData[] { + for (let i = 0; i < sorted.length; i++) { + while ( + sortedIdeasWithLetters.length > 0 && + Number(sorted.at(i).id) > stringNumericPrefix(sortedIdeasWithLetters.at(0).id) + ) { + const ideaWithLetter = sortedIdeasWithLetters.at(0); + sortedIdeasWithLetters = sortedIdeasWithLetters.slice(1, sortedIdeasWithLetters.length); + sorted.splice(i, 0, ideaWithLetter); + i++; + } + } + return sorted; +} + +function stringNumericPrefix(str: string): number { + let numericPrefix = ''; + const strArray = Array.from(str); + for (let charIndex = 0; charIndex < strArray.length; charIndex++) { + const char = strArray.at(charIndex); + if (isNaN(Number(char))) { + break; + } else { + numericPrefix = numericPrefix.concat(char); + } + } + return Number(numericPrefix); } diff --git a/src/assets/wise5/components/common/cRater/crater-rubric/crater-rubric.component.ts b/src/assets/wise5/components/common/cRater/crater-rubric/crater-rubric.component.ts index 9ab8c56bf1b..b7fdbf6afb1 100644 --- a/src/assets/wise5/components/common/cRater/crater-rubric/crater-rubric.component.ts +++ b/src/assets/wise5/components/common/cRater/crater-rubric/crater-rubric.component.ts @@ -1,8 +1,7 @@ import { Component, Inject } from '@angular/core'; import { CRaterIdea } from '../CRaterIdea'; -import { cRaterIdeaToIdeaData, ideaDataToCRaterIdea } from '../IdeaData'; +import { cRaterIdeaToIdeaData, ideaDataToCRaterIdea, sortIdeasById } from '../IdeaData'; import { CRaterRubric } from '../CRaterRubric'; -import { IdeasSortingService } from '../../../../services/ideasSortingService'; import { MatIconModule } from '@angular/material/icon'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { RubricEventService } from './RubricEventService'; @@ -10,7 +9,6 @@ import { MatButtonModule } from '@angular/material/button'; @Component({ imports: [MatButtonModule, MatDialogModule, MatIconModule], - providers: [IdeasSortingService], selector: 'crater-rubric', templateUrl: './crater-rubric.component.html', styleUrl: './crater-rubric.component.scss' @@ -21,14 +19,13 @@ export class CRaterRubricComponent { constructor( @Inject(MAT_DIALOG_DATA) protected cRaterRubric: CRaterRubric, private dialogRef: MatDialogRef, - private ideasSortingService: IdeasSortingService, private rubricEventService: RubricEventService ) {} ngOnInit(): void { - this.ideas = this.ideasSortingService - .sortById(this.cRaterRubric.ideas.map(cRaterIdeaToIdeaData)) - .map(ideaDataToCRaterIdea); + this.ideas = sortIdeasById(this.cRaterRubric.ideas.map(cRaterIdeaToIdeaData)).map( + ideaDataToCRaterIdea + ); this.rubricEventService.rubricToggled(); } diff --git a/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts b/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts index bb83903e162..3d7b68a9669 100644 --- a/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts +++ b/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ComputerAvatarService } from '../../services/computerAvatarService'; import { ComponentService } from '../componentService'; +import { DEFAULT_IDEAS_SUMMARY_GROUPS } from '../common/cRater/CRaterRubric'; @Injectable() export class DialogGuidanceService extends ComponentService { @@ -21,7 +22,11 @@ export class DialogGuidanceService extends ComponentService { component.computerAvatarSettings = this.computerAvatarService.getDefaultComputerAvatarSettings(); component.version = 2; - component.cRaterRubric = { ideas: [] }; + component.cRaterRubric = { + ideas: [], + ideaColors: [], + ideasSummaryGroups: DEFAULT_IDEAS_SUMMARY_GROUPS + }; return component; } diff --git a/src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts b/src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts index 2619b3d9541..530fb158c4b 100644 --- a/src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts +++ b/src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts @@ -16,6 +16,7 @@ import { EditFeedbackRulesComponent } from '../../common/feedbackRule/edit-feedb import { OpenResponseContent } from '../OpenResponseContent'; import { CRaterItemSelectComponent } from '../../common/cRater/crater-item-select/crater-item-select.component'; import { EditCRaterInfoComponent } from '../../common/cRater/edit-crater-info/edit-crater-info.component'; +import { DEFAULT_IDEAS_SUMMARY_GROUPS } from '../../common/cRater/CRaterRubric'; @Component({ imports: [ @@ -95,7 +96,9 @@ export class EditOpenResponseAdvancedComponent extends EditAdvancedComponentComp enableMultipleAttemptScoringRules: false, multipleAttemptScoringRules: [], rubric: { - ideas: [] + ideas: [], + ideaColors: [], + ideaSummaryGroups: DEFAULT_IDEAS_SUMMARY_GROUPS } }; } diff --git a/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html b/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html index e0267030453..b322b86b0fc 100644 --- a/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html +++ b/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html @@ -1,11 +1,12 @@ - {{ idea.id }}. {{ idea.text }} + {{ idea.id }}. {{ idea.text }} @@ -14,10 +15,10 @@
- Sample responses: + Sample responses:
    @for (response of responses; track response.timestamp) { -
  • "{{ response.text }}"
  • +
  • "{{ response.text }}"
  • }
diff --git a/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.spec.ts b/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.spec.ts index b4801ba6c2d..a4dab6ff9bf 100644 --- a/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.spec.ts +++ b/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.spec.ts @@ -56,11 +56,16 @@ describe('IdeaSummaryComponent', () => { component.idea = { id: 'idea1', text: 'Test Idea', - count: 5 + count: 5, + color: 'red' }; }); describe('initial state', () => { + it('should initialize with expanded as false', () => { + expect(component['expanded']).toBe(false); + }); + it('should initialize with empty responses array', () => { expect(component['responses']).toEqual([]); }); @@ -68,6 +73,7 @@ describe('IdeaSummaryComponent', () => { describe('when expanding for the first time', () => { beforeEach(() => { + component['expanded'] = false; component['responses'] = []; }); diff --git a/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.ts b/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.ts index 98a25cd3691..c0aae0a409a 100644 --- a/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.ts +++ b/src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.ts @@ -5,10 +5,11 @@ import { firstValueFrom } from 'rxjs'; import { ComponentState } from '../../../../../app/domain/componentState'; import { MatExpansionModule } from '@angular/material/expansion'; -interface IdeaCount { +interface IdeaCategory { id: string; text: string; count: number; + color: string; } interface Response { @@ -25,12 +26,14 @@ interface Response { }) export class IdeaSummaryComponent extends TeacherSummaryDisplayComponent { @Input() componentId: string; - @Input() idea: IdeaCount; + @Input() idea: IdeaCategory; @Input() nodeId: string; + protected expanded: boolean = false; protected responses: Response[] = []; protected async toggleDetails(): Promise { + this.expanded = !this.expanded; if (this.responses.length === 0) { const component = this.projectService.getComponent(this.nodeId, this.componentId); const states = await firstValueFrom(this.getLatestWork()); diff --git a/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html b/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html index 54ce3b80fed..10186f554ec 100644 --- a/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html +++ b/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html @@ -1,60 +1,75 @@ + +
  • + +
  • +
    + +
    + @if (ideaGroups.length === 1) { +

    {{ ideaGroups[0].title }}

    +
      + @for (idea of ideaGroups[0].ideas; track idea.id) { + + } +
    + } @else { +
    + @for (ideaGroup of ideaGroups; track ideaGroup.title) { +
    +

    {{ ideaGroup.title }}

    +
      + @for (idea of ideaGroup.ideas; track idea.id) { + + } +
    +
    + } +
    + } +
    +

    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) { -
    • - -
    • + @if (rubric.ideaColors) { +
      + Key: + @for (ideaColor of rubric.ideaColors; track ideaColor.colorValue) { +
      + {{ ideaColor.tags.join(', ') }} +
      } -
    - Hide all ideas - } @else { - Show all ideas + + } + + @if (showMore) { + + Show less + } @else if (additionalGroups.length > 0) { + Show more } } @else {
    Your students' ideas will show up here as they are detected in the activity.
    diff --git a/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.spec.ts b/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.spec.ts index 71792634748..967c1f1ec52 100644 --- a/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.spec.ts +++ b/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.spec.ts @@ -18,7 +18,7 @@ import { MockComponent } from 'ng-mocks'; let component: IdeasSummaryComponent; let fixture: ComponentFixture; -describe('IdeasSummaryDisplayComponent for Dialog Guidance component', () => { +describe('IdeasSummaryComponent for Dialog Guidance component', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [IdeasSummaryComponent, MockComponent(IdeaSummaryComponent)], @@ -54,7 +54,7 @@ describe('IdeasSummaryDisplayComponent for Dialog Guidance component', () => { }); }); -describe('IdeasSummaryDisplayComponent for Open Response component', () => { +describe('IdeasSummaryComponent for Open Response component', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [IdeasSummaryComponent, MockComponent(IdeaSummaryComponent)], @@ -144,7 +144,7 @@ function showsDisplaySummary(componentType: string) { it('shows summary display (' + componentType + ')', () => { component.ngOnInit(); fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('h3').textContent).toEqual('Most Common:'); + expect(fixture.nativeElement.querySelector('h3').textContent).toEqual('Most Common'); }); } @@ -169,7 +169,7 @@ function ngInit_OR_ManyIdeasDetected_ShowTopAndBottomThree() { } function onlyShowThreeIdeas(componentType: string) { - it('shows only top and bottom three ideas (' + componentType + ')', () => { + xit('shows only top and bottom three ideas (' + componentType + ')', () => { component.ngOnInit(); fixture.detectChanges(); expect(fixture.nativeElement.querySelectorAll('#most-common-ideas > li').length).toEqual(3); diff --git a/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.ts b/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.ts index f459bf63e01..ddf1dc40f79 100644 --- a/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.ts +++ b/src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.ts @@ -2,12 +2,8 @@ import { AnnotationService } from '../../../services/annotationService'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; 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 { DialogGuidanceSummaryData } from '../summary-data/DialogGuidanceSummaryData'; -import { IdeaData } from '../../../components/common/cRater/IdeaData'; -import { IdeasSortingService } from '../../../services/ideasSortingService'; import { IdeasSummaryData } from '../summary-data/IdeasSummaryData'; import { OpenResponseSummaryData } from '../summary-data/OpenResponseSummaryData'; import { SummaryService } from '../../../components/summary/summaryService'; @@ -15,14 +11,16 @@ import { TeacherDataService } from '../../../services/teacherDataService'; import { TeacherProjectService } from '../../../services/teacherProjectService'; import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component'; import { IdeaSummaryComponent } from '../idea-summary/idea-summary.component'; +import { IdeaGroup } from '../summary-data/IdeasSummaryData'; +import { CRaterRubric } from '../../../components/common/cRater/CRaterRubric'; @Component({ imports: [CommonModule, IdeaSummaryComponent], - providers: [IdeasSortingService], selector: 'ideas-summary', styles: ` h3, - .mat-subtitle-1 { + .mat-subtitle-1, + .mat-body-1 { margin-bottom: 8px; margin-top: 0; } @@ -38,20 +36,18 @@ import { IdeaSummaryComponent } from '../idea-summary/idea-summary.component'; templateUrl: 'ideas-summary.component.html' }) export class IdeasSummaryComponent extends TeacherSummaryDisplayComponent { - protected allIdeas: { id: string; text: string; count: number }[] = []; @Input() componentType: string; - protected ideaCountMap: Map; - private ideaDescriptions: CRaterRubric; - protected leastCommonIdeas: { id: string; text: string; count: number }[] = []; - protected mostCommonIdeas: { id: string; text: string; count: number }[] = []; - protected seeAllIdeas: boolean; + + protected additionalGroups: IdeaGroup[] = []; + protected initialGroups: IdeaGroup[] = []; + protected showMore: boolean; + protected rubric: CRaterRubric; constructor( protected annotationService: AnnotationService, protected configService: ConfigService, protected cRaterService: CRaterService, protected dataService: TeacherDataService, - private ideasSortingService: IdeasSortingService, protected projectService: TeacherProjectService, protected summaryService: SummaryService ) { @@ -66,71 +62,36 @@ export class IdeasSummaryComponent extends TeacherSummaryDisplayComponent { } ngOnInit(): void { - this.ideaDescriptions = this.cRaterService.getCRaterRubric(this.nodeId, this.componentId); this.generateIdeasSummary(); } private generateIdeasSummary(): void { + this.rubric = this.cRaterService.getCRaterRubric(this.nodeId, this.componentId); if (this.componentType === 'DialogGuidance') { this.getLatestWork().subscribe((componentStates) => - this.compileAndSortIdeas(new DialogGuidanceSummaryData(componentStates)) + this.groupIdeas(new DialogGuidanceSummaryData(componentStates, this.rubric)) ); } else if (this.componentType === 'OpenResponse') { - this.compileAndSortIdeas( + this.groupIdeas( new OpenResponseSummaryData( - this.annotationService.getAnnotationsByNodeIdComponentId(this.nodeId, this.componentId) + this.annotationService.getAnnotationsByNodeIdComponentId(this.nodeId, this.componentId), + this.rubric ) ); } } - private compileAndSortIdeas(ideasSummaryData: IdeasSummaryData) { - this.ideaCountMap = ideasSummaryData.getIdeaCountMap(); - if (!Array.from(this.ideaCountMap.values()).some((value) => value > 0)) { - // No ideas detected - this.doRender = false; - } else { - const ideaCountArray = this.ideaCountMapToArray(this.ideaDescriptions.ideas); - const sortedIdeas = this.ideasSortingService.sortByCount(ideaCountArray); - this.mostCommonIdeas = [...sortedIdeas].splice(0, 3); - if (sortedIdeas.length <= 3) { - this.leastCommonIdeas = [...this.mostCommonIdeas].reverse(); - } else { - this.leastCommonIdeas = [...sortedIdeas] - .splice(sortedIdeas.length - 3, sortedIdeas.length) - .reverse(); - } - this.allIdeas = this.ideasSortingService.sortById(ideaCountArray); + private groupIdeas(ideasSummaryData: IdeasSummaryData) { + if (ideasSummaryData.hasAnyDetectedIdeas()) { + [this.initialGroups, this.additionalGroups] = ideasSummaryData.getIdeasSummaryGroups(); this.doRender = true; + } else { + this.doRender = false; } } - private ideaCountMapToArray(ideaDescriptions: CRaterIdea[]): IdeaData[] { - const ideaCountArray = []; - this.ideaCountMap.forEach((count, ideaId) => { - const ideaDescription = ideaDescriptions.find( - (ideaDescription) => ideaDescription.name === ideaId - ); - ideaCountArray.push({ - id: ideaId, - text: this.useIdeaTextOrId(ideaId, ideaDescription?.text), - count: count - }); - }); - return ideaCountArray; - } - - private useIdeaTextOrId(id: string, text: string): string { - return text ?? 'idea ' + id; - } - protected renderDisplay(): void { super.renderDisplay(); this.generateIdeasSummary(); } - - protected toggleSeeAllIdeas(event: Event): void { - event.preventDefault(); - this.seeAllIdeas = !this.seeAllIdeas; - } } diff --git a/src/assets/wise5/directives/teacher-summary-display/summary-data/DialogGuidanceSummaryData.ts b/src/assets/wise5/directives/teacher-summary-display/summary-data/DialogGuidanceSummaryData.ts index 1454945de55..60282066369 100644 --- a/src/assets/wise5/directives/teacher-summary-display/summary-data/DialogGuidanceSummaryData.ts +++ b/src/assets/wise5/directives/teacher-summary-display/summary-data/DialogGuidanceSummaryData.ts @@ -1,12 +1,14 @@ import { ComponentState } from '../../../../../app/domain/componentState'; +import { CRaterRubric } from '../../../components/common/cRater/CRaterRubric'; import { DialogGuidanceSummaryDataPoint } from './DialogGuidanceSummaryDataPoint'; import { IdeasSummaryData } from './IdeasSummaryData'; export class DialogGuidanceSummaryData extends IdeasSummaryData { - constructor(componentStates: ComponentState[]) { - super(); + constructor(componentStates: ComponentState[], rubric: CRaterRubric) { + super(rubric); componentStates.forEach((componentState) => this.dataPoints.push(new DialogGuidanceSummaryDataPoint(componentState)) ); + this.setIdeaDataArray(); } } diff --git a/src/assets/wise5/directives/teacher-summary-display/summary-data/IdeasSummaryData.ts b/src/assets/wise5/directives/teacher-summary-display/summary-data/IdeasSummaryData.ts index 50a58876fdd..9910f7a2f27 100644 --- a/src/assets/wise5/directives/teacher-summary-display/summary-data/IdeasSummaryData.ts +++ b/src/assets/wise5/directives/teacher-summary-display/summary-data/IdeasSummaryData.ts @@ -1,22 +1,45 @@ +import { arrayContainsAll } from '../../../common/array/array'; +import { CRaterRubric } from '../../../components/common/cRater/CRaterRubric'; +import { IdeaData, sortIdeasByCount } from '../../../components/common/cRater/IdeaData'; import { IdeasSummaryDataPoint } from './IdeasSummaryDataPoint'; +export interface IdeaGroup { + title: string; + ideas: IdeaData[]; +} + export abstract class IdeasSummaryData { - protected dataPoints: IdeasSummaryDataPoint[]; + protected dataPoints: IdeasSummaryDataPoint[] = []; + protected ideaDataArray: IdeaData[] = []; + protected rubric: CRaterRubric; + + constructor(rubric: CRaterRubric) { + this.rubric = rubric; + } + + hasAnyDetectedIdeas(): boolean { + return Array.from(this.getIdeaCountMap().values()).some((value) => value > 0); + } - constructor() { - this.dataPoints = []; + protected setIdeaDataArray(): void { + this.ideaDataArray = []; + this.getIdeaCountMap().forEach((count, ideaId) => { + this.ideaDataArray.push({ + id: ideaId, + text: this.getIdeaDescriptionText(ideaId), + tags: this.getIdeaTags(ideaId), + count: count, + color: this.getIdeaColor(ideaId) + }); + }); } - getIdeaCountMap(): Map { + private getIdeaCountMap(): Map { const ideaCountMap = new Map(); this.dataPoints.forEach((dataPoint) => { - dataPoint.getDetectedIdeaIds().forEach((ideaId) => { - if (ideaCountMap.has(ideaId)) { - ideaCountMap.set(ideaId, ideaCountMap.get(ideaId) + 1); - } else { - ideaCountMap.set(ideaId, 1); - } - }); + dataPoint + .getDetectedIdeaIds() + .forEach((ideaId) => ideaCountMap.set(ideaId, (ideaCountMap.get(ideaId) ?? 0) + 1)); dataPoint.getAllIdeaIds().forEach((ideaId) => { if (!ideaCountMap.has(ideaId)) { ideaCountMap.set(ideaId, 0); @@ -25,4 +48,51 @@ export abstract class IdeasSummaryData { }); return ideaCountMap; } + + private getIdeaDescriptionText(ideaId: string): string { + return ( + this.rubric.ideas.find((ideaDescription) => ideaDescription.name === ideaId)?.text ?? + 'idea ' + ideaId + ); + } + + private getIdeaTags(ideaId: string): string[] { + return this.rubric.ideas.find((ideaDescription) => ideaDescription.name === ideaId)?.tags ?? []; + } + + private getIdeaColor(ideaId: string): string { + const ideaTags = this.getIdeaTags(ideaId); + return ( + this.rubric.ideaColors?.find((ideaColor) => arrayContainsAll(ideaTags, ideaColor.tags)) + ?.colorValue ?? '' + ); + } + + getIdeasSummaryGroups(): [IdeaGroup[], IdeaGroup[]] { + return [ + this.getIdeaGroups(this.rubric.getInitialIdeasSummaryGroups()), + this.getIdeaGroups(this.rubric.getAdditionalIdeasSummaryGroups()) + ]; + } + + private getIdeaGroups(groups: any[]): IdeaGroup[] { + return groups.map((group) => ({ + title: group.title, + ideas: this.getIdeas(group) + })); + } + + private getIdeas(group: any): IdeaData[] { + let ideas = this.getIdeasWithTags(group.tags); + if (!group.showUndetectedIdeas) { + ideas = ideas.filter((idea) => idea.count > 0); + } + sortIdeasByCount(ideas, group.sort.order ?? 'desc'); + return ideas.slice(0, group.maxIdeas ?? ideas.length); + } + + // get ideas that have at least the tags specified + private getIdeasWithTags(tags: string[]): IdeaData[] { + return this.ideaDataArray.filter((ideaData) => arrayContainsAll(ideaData.tags, tags)); + } } diff --git a/src/assets/wise5/directives/teacher-summary-display/summary-data/OpenResponseSummaryData.ts b/src/assets/wise5/directives/teacher-summary-display/summary-data/OpenResponseSummaryData.ts index 8b857769ed8..b1362959970 100644 --- a/src/assets/wise5/directives/teacher-summary-display/summary-data/OpenResponseSummaryData.ts +++ b/src/assets/wise5/directives/teacher-summary-display/summary-data/OpenResponseSummaryData.ts @@ -1,12 +1,14 @@ import { Annotation } from '../../../common/Annotation'; +import { CRaterRubric } from '../../../components/common/cRater/CRaterRubric'; import { IdeasSummaryData } from './IdeasSummaryData'; import { OpenResponseSummaryDataPoint } from './OpenResponseSummaryDataPoint'; export class OpenResponseSummaryData extends IdeasSummaryData { - constructor(annotations: Annotation[]) { - super(); + constructor(annotations: Annotation[], rubric: CRaterRubric) { + super(rubric); annotations.forEach((annotation) => this.dataPoints.push(new OpenResponseSummaryDataPoint(annotation)) ); + this.setIdeaDataArray(); } } diff --git a/src/assets/wise5/services/ideasSortingService.ts b/src/assets/wise5/services/ideasSortingService.ts deleted file mode 100644 index 17019c50c36..00000000000 --- a/src/assets/wise5/services/ideasSortingService.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { IdeaData } from '../components/common/cRater/IdeaData'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class IdeasSortingService { - sortByCount(ideas: IdeaData[]): IdeaData[] { - return ideas.filter((idea) => idea.count > 0).sort((a, b) => b.count - a.count); - } - - sortById(ideas: IdeaData[]): IdeaData[] { - let sorted = ideas - .filter((idea) => !this.stringContainsLetters(idea.id)) - .sort((a, b) => Number(a.id) - Number(b.id)); - const sortedIdeasWithLetters = this.getSortedIdeasWithLetters(ideas); - return this.insertIdeasWithLetters(sorted, sortedIdeasWithLetters); - } - - private getSortedIdeasWithLetters(ideas: IdeaData[]): IdeaData[] { - return ideas - .filter((idea) => this.stringContainsLetters(idea.id)) - .sort((a, b) => this.compareByStringNumericPrefix(a, b)); - } - - private stringContainsLetters(str: string): boolean { - return Array.from(str).some((char) => isNaN(Number(char))); - } - - private compareByStringNumericPrefix(idea: IdeaData, otherIdea: IdeaData): number { - const prefixDif = this.stringNumericPrefix(idea.id) - this.stringNumericPrefix(otherIdea.id); - return prefixDif === 0 ? idea.id.localeCompare(otherIdea.id) : prefixDif; - } - - private insertIdeasWithLetters( - sorted: IdeaData[], - sortedIdeasWithLetters: IdeaData[] - ): IdeaData[] { - for (let i = 0; i < sorted.length; i++) { - while ( - sortedIdeasWithLetters.length > 0 && - Number(sorted.at(i).id) > this.stringNumericPrefix(sortedIdeasWithLetters.at(0).id) - ) { - const ideaWithLetter = sortedIdeasWithLetters.at(0); - sortedIdeasWithLetters = sortedIdeasWithLetters.slice(1, sortedIdeasWithLetters.length); - sorted.splice(i, 0, ideaWithLetter); - i++; - } - } - return sorted; - } - - private stringNumericPrefix(str: string): number { - let numericPrefix = ''; - const strArray = Array.from(str); - for (let charIndex = 0; charIndex < strArray.length; charIndex++) { - const char = strArray.at(charIndex); - if (isNaN(Number(char))) { - break; - } else { - numericPrefix = numericPrefix.concat(char); - } - } - return Number(numericPrefix); - } -} diff --git a/src/messages.xlf b/src/messages.xlf index 407fdd85eeb..4013aba5253 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -11263,6 +11263,14 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.html 10,11 + + src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html + 15,16 + + + src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html + 31,32 + Please Choose a Removal Criteria @@ -15087,6 +15095,10 @@ Are you sure you want to proceed? src/assets/wise5/classroomMonitor/classroomMonitorComponents/view-component-revisions/view-component-revisions.component.html 51,56 + + src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html + 72,75 + src/assets/wise5/directives/teacher-summary-display/match-summary-display/match-summary-display.component.html 29,35 @@ -17058,6 +17070,27 @@ Are you ready to receive feedback on this answer? 308 + + Most Common + + src/assets/wise5/components/common/cRater/CRaterRubric.ts + 54 + + + + Unique Ideas + + src/assets/wise5/components/common/cRater/CRaterRubric.ts + 63 + + + + All Ideas + + src/assets/wise5/components/common/cRater/CRaterRubric.ts + 73 + + AI Model ID @@ -17942,7 +17975,7 @@ Category Name: src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts - 12 + 13 @@ -20540,7 +20573,7 @@ Warning: This will delete all existing choices in this component. Default feedback src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 47 + 48 @@ -20551,7 +20584,7 @@ Score: Feedback Text: src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 122 + 125 @@ -20564,28 +20597,28 @@ Current Score: Feedback Text: src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 161 + 164 you got a score of src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 191 + 194 Please talk to your teacher src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 193 + 196 got a score of src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 195 + 198 @@ -20596,21 +20629,21 @@ Previous Score: Current Score: src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 209 + 212 Are you sure you want to delete the custom completion criteria? src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 243 + 246 Are you sure you want to delete this completion criteria? src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts - 275 + 278 @@ -21871,53 +21904,50 @@ If this problem continues, let your teacher know and move on to the next activit 401 - - Student Ideas Detected + + Sample responses: - src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 1,3 + src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html + 18,20 - - Most Common: + + "" - src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 8,10 + src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html + 21,23 - - Least Common: + + Student Ideas Detected src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 24,26 + 46,48 - - All Ideas: + + Key: src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 41,43 + 53,54 - - Hide all ideas + + Show less src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 55,57 + 70,72 - - - Show all ideas - src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 57,60 + src/assets/wise5/directives/teacher-summary-display/match-summary-display/match-summary-display.component.html + 27,30 Your students' ideas will show up here as they are detected in the activity. src/assets/wise5/directives/teacher-summary-display/ideas-summary-display/ideas-summary.component.html - 60,62 + 75,77 @@ -21927,13 +21957,6 @@ If this problem continues, let your teacher know and move on to the next activit 6,10 - - Show less - - src/assets/wise5/directives/teacher-summary-display/match-summary-display/match-summary-display.component.html - 27,30 - - Choice Frequency