Skip to content

Commit a1254dc

Browse files
feat(IdeaSummary): Template to organize display of ideas (#2270)
Add support to configure how the idea summary is displayed in the Teacher Tools. Co-authored-by: Jonathan Lim-Breitbart <breity10@gmail.com>
1 parent 01cd0cb commit a1254dc

20 files changed

Lines changed: 415 additions & 266 deletions

File tree

src/app/chatbot/awsBedRock.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ChatService } from './chat.service';
44
@Injectable({ providedIn: 'root' })
55
export class AwsBedRockService extends ChatService {
66
protected chatEndpoint = '/api/aws-bedrock/chat';
7-
protected model: string = 'openai.gpt-oss-20b-1:0';
7+
protected model: string = 'google.gemma-3-27b-it';
88

99
processResponse(response: string): string {
1010
return response.replace(/<reasoning>.*?<\/reasoning>/gs, '');

src/app/services/ideasSortingService.spec.ts renamed to src/app/services/ideaData.spec.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,42 @@
1-
import { IdeaData } from '../../assets/wise5/components/common/cRater/IdeaData';
2-
import { IdeasSortingService } from '../../assets/wise5/services/ideasSortingService';
1+
import {
2+
IdeaData,
3+
sortIdeasByCount,
4+
sortIdeasById
5+
} from '../../assets/wise5/components/common/cRater/IdeaData';
36
import { TestBed } from '@angular/core/testing';
47

58
let ideas: IdeaData[];
6-
let service: IdeasSortingService;
7-
8-
describe('IdeasSortingService', () => {
9+
describe('IdeaData', () => {
910
beforeEach(() => {
10-
TestBed.configureTestingModule({
11-
providers: [IdeasSortingService]
12-
});
11+
TestBed.configureTestingModule({});
1312
ideas = [
1413
createIdeaData('2', 'c', 3),
1514
createIdeaData('1', 'b', 1),
1615
createIdeaData('2b', 'a', 4),
1716
createIdeaData('10a', 'abc', 2),
1817
createIdeaData('11', 'cba', 5)
1918
];
20-
service = TestBed.inject(IdeasSortingService);
2119
});
2220

23-
sortIdeasByCount();
24-
sortIdeasById();
21+
test_SortIdeasByCount();
22+
test_SortIdeasById();
2523
});
2624

27-
function sortIdeasByCount() {
25+
function test_SortIdeasByCount() {
2826
it('should sort ideas descending numerically by count', () => {
29-
const sortedIdeas = service.sortByCount(ideas);
27+
const sortedIdeas = sortIdeasByCount(ideas, 'desc');
3028
expect(sortedIdeas.map((idea) => idea.id)).toEqual(['11', '2b', '2', '10a', '1']);
3129
});
30+
31+
it('should sort ideas ascending numerically by count', () => {
32+
const sortedIdeas = sortIdeasByCount(ideas, 'asc');
33+
expect(sortedIdeas.map((idea) => idea.id)).toEqual(['1', '10a', '2', '2b', '11']);
34+
});
3235
}
3336

34-
function sortIdeasById() {
37+
function test_SortIdeasById() {
3538
it('should sort ideas alphanumerically by ID', () => {
36-
const sortedIdeas = service.sortById(ideas);
39+
const sortedIdeas = sortIdeasById(ideas);
3740
expect(sortedIdeas.map((ideas) => ideas.id)).toEqual(['1', '2', '2b', '10a', '11']);
3841
});
3942
}

src/assets/wise5/common/array/array.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ export function arraysContainSameValues(array1: string[], array2: string[]): boo
8484
return JSON.stringify(array1Copy) === JSON.stringify(array2Copy);
8585
}
8686

87+
/**
88+
* Check if array1 contains all elements of array2. Even if array1 contains more elements
89+
* than array2, it will still return true if array1 contains all elements of array2.
90+
* @param array1 an array of strings
91+
* @param array2 an array of strings
92+
* @returns whether array1 contains all elements of array2
93+
*/
94+
export function arrayContainsAll(array1: string[], array2: string[]): boolean {
95+
return array2.every((value) => array1.includes(value));
96+
}
97+
8798
export function reduceByUniqueId(objArr: any[]): any[] {
8899
const idToObj = {};
89100
const result = [];

src/assets/wise5/components/common/cRater/CRaterIdea.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ export class CRaterIdea {
33
detected?: boolean;
44
characterOffsets: any[];
55
text?: string;
6+
tags?: string[];
67

7-
constructor(name: string, detected?: boolean, text?: string) {
8+
constructor(name: string, detected?: boolean, text?: string, tags?: string[]) {
89
this.name = name;
910
if (detected) {
1011
this.detected = detected;
1112
}
1213
if (text) {
1314
this.text = text;
1415
}
16+
if (tags) {
17+
this.tags = tags;
18+
}
1519
}
1620
}

src/assets/wise5/components/common/cRater/CRaterRubric.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { CRaterIdea } from './CRaterIdea';
33
export class CRaterRubric {
44
description: string = '';
55
ideas: CRaterIdea[] = [];
6+
ideasSummaryGroups?: any;
7+
ideaColors?: { tags: string[]; colorValue: string }[];
68

79
constructor(rubric: any = { description: '', ideas: [] }) {
810
this.description = rubric.description;
911
this.ideas = rubric.ideas;
12+
this.ideasSummaryGroups = rubric.ideasSummaryGroups ?? DEFAULT_IDEAS_SUMMARY_GROUPS;
13+
this.ideaColors = rubric.ideaColors;
1014
}
1115

1216
getIdea(ideaId: string): CRaterIdea {
@@ -16,6 +20,14 @@ export class CRaterRubric {
1620
hasRubricData(): boolean {
1721
return (this.description ?? '') !== '' || this.ideas.length > 0;
1822
}
23+
24+
getInitialIdeasSummaryGroups(): any[] {
25+
return this.ideasSummaryGroups.initial;
26+
}
27+
28+
getAdditionalIdeasSummaryGroups(): any[] {
29+
return this.ideasSummaryGroups.additional;
30+
}
1931
}
2032

2133
export function getUniqueIdeas(responses: any[], rubric: CRaterRubric): CRaterIdea[] {
@@ -34,3 +46,37 @@ export function getUniqueIdeas(responses: any[], rubric: CRaterRubric): CRaterId
3446
);
3547
return uniqueIdeas;
3648
}
49+
50+
export const DEFAULT_IDEAS_SUMMARY_GROUPS = {
51+
initial: [
52+
{
53+
maxIdeas: 3,
54+
title: $localize`Most Common`,
55+
tags: [],
56+
sort: {
57+
field: 'count',
58+
order: 'desc'
59+
}
60+
},
61+
{
62+
maxIdeas: 3,
63+
title: $localize`Unique Ideas`,
64+
tags: [],
65+
sort: {
66+
field: 'count',
67+
order: 'asc'
68+
}
69+
}
70+
],
71+
additional: [
72+
{
73+
title: $localize`All Ideas`,
74+
tags: [],
75+
sort: {
76+
field: 'count',
77+
order: 'desc'
78+
},
79+
showUndetectedIdeas: true
80+
}
81+
]
82+
};

src/assets/wise5/components/common/cRater/IdeaData.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,73 @@ export type IdeaData = {
44
id: string;
55
text: string;
66
count: number;
7+
tags?: string[];
8+
color?: string;
79
};
810

911
export function ideaDataToCRaterIdea(ideaData: IdeaData): CRaterIdea {
10-
return new CRaterIdea(ideaData.id, undefined, ideaData.text);
12+
return new CRaterIdea(ideaData.id, undefined, ideaData.text, ideaData.tags);
1113
}
1214

1315
export function cRaterIdeaToIdeaData(cRaterIdea: CRaterIdea): IdeaData {
14-
return { id: cRaterIdea.name, text: cRaterIdea.text, count: 0 };
16+
return { id: cRaterIdea.name, text: cRaterIdea.text, count: 0, tags: cRaterIdea.tags };
17+
}
18+
19+
export function sortIdeasByCount(ideas: IdeaData[], sortOrder: 'asc' | 'desc'): IdeaData[] {
20+
return ideas.sort((a, b) => (sortOrder === 'asc' ? a.count - b.count : b.count - a.count));
21+
}
22+
23+
export function sortIdeasById(ideas: IdeaData[]): IdeaData[] {
24+
const sorted = ideas
25+
.filter((idea) => !stringContainsLetters(idea.id))
26+
.sort((a, b) => Number(a.id) - Number(b.id));
27+
const sortedIdeasWithLetters = getSortedIdeasWithLetters(ideas);
28+
return insertIdeasWithLetters(sorted, sortedIdeasWithLetters);
29+
}
30+
31+
function getSortedIdeasWithLetters(ideas: IdeaData[]): IdeaData[] {
32+
return ideas
33+
.filter((idea) => stringContainsLetters(idea.id))
34+
.sort((a, b) => compareByStringNumericPrefix(a, b));
35+
}
36+
37+
function stringContainsLetters(str: string): boolean {
38+
return Array.from(str).some((char) => isNaN(Number(char)));
39+
}
40+
41+
function compareByStringNumericPrefix(idea: IdeaData, otherIdea: IdeaData): number {
42+
const prefixDif = stringNumericPrefix(idea.id) - stringNumericPrefix(otherIdea.id);
43+
return prefixDif === 0 ? idea.id.localeCompare(otherIdea.id) : prefixDif;
44+
}
45+
46+
function insertIdeasWithLetters(
47+
sorted: IdeaData[],
48+
sortedIdeasWithLetters: IdeaData[]
49+
): IdeaData[] {
50+
for (let i = 0; i < sorted.length; i++) {
51+
while (
52+
sortedIdeasWithLetters.length > 0 &&
53+
Number(sorted.at(i).id) > stringNumericPrefix(sortedIdeasWithLetters.at(0).id)
54+
) {
55+
const ideaWithLetter = sortedIdeasWithLetters.at(0);
56+
sortedIdeasWithLetters = sortedIdeasWithLetters.slice(1, sortedIdeasWithLetters.length);
57+
sorted.splice(i, 0, ideaWithLetter);
58+
i++;
59+
}
60+
}
61+
return sorted;
62+
}
63+
64+
function stringNumericPrefix(str: string): number {
65+
let numericPrefix = '';
66+
const strArray = Array.from(str);
67+
for (let charIndex = 0; charIndex < strArray.length; charIndex++) {
68+
const char = strArray.at(charIndex);
69+
if (isNaN(Number(char))) {
70+
break;
71+
} else {
72+
numericPrefix = numericPrefix.concat(char);
73+
}
74+
}
75+
return Number(numericPrefix);
1576
}

src/assets/wise5/components/common/cRater/crater-rubric/crater-rubric.component.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { Component, Inject } from '@angular/core';
22
import { CRaterIdea } from '../CRaterIdea';
3-
import { cRaterIdeaToIdeaData, ideaDataToCRaterIdea } from '../IdeaData';
3+
import { cRaterIdeaToIdeaData, ideaDataToCRaterIdea, sortIdeasById } from '../IdeaData';
44
import { CRaterRubric } from '../CRaterRubric';
5-
import { IdeasSortingService } from '../../../../services/ideasSortingService';
65
import { MatIconModule } from '@angular/material/icon';
76
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
87
import { RubricEventService } from './RubricEventService';
98
import { MatButtonModule } from '@angular/material/button';
109

1110
@Component({
1211
imports: [MatButtonModule, MatDialogModule, MatIconModule],
13-
providers: [IdeasSortingService],
1412
selector: 'crater-rubric',
1513
templateUrl: './crater-rubric.component.html',
1614
styleUrl: './crater-rubric.component.scss'
@@ -21,14 +19,13 @@ export class CRaterRubricComponent {
2119
constructor(
2220
@Inject(MAT_DIALOG_DATA) protected cRaterRubric: CRaterRubric,
2321
private dialogRef: MatDialogRef<CRaterRubricComponent>,
24-
private ideasSortingService: IdeasSortingService,
2522
private rubricEventService: RubricEventService
2623
) {}
2724

2825
ngOnInit(): void {
29-
this.ideas = this.ideasSortingService
30-
.sortById(this.cRaterRubric.ideas.map(cRaterIdeaToIdeaData))
31-
.map(ideaDataToCRaterIdea);
26+
this.ideas = sortIdeasById(this.cRaterRubric.ideas.map(cRaterIdeaToIdeaData)).map(
27+
ideaDataToCRaterIdea
28+
);
3229
this.rubricEventService.rubricToggled();
3330
}
3431

src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable } from '@angular/core';
22
import { ComputerAvatarService } from '../../services/computerAvatarService';
33
import { ComponentService } from '../componentService';
4+
import { DEFAULT_IDEAS_SUMMARY_GROUPS } from '../common/cRater/CRaterRubric';
45

56
@Injectable()
67
export class DialogGuidanceService extends ComponentService {
@@ -21,7 +22,11 @@ export class DialogGuidanceService extends ComponentService {
2122
component.computerAvatarSettings =
2223
this.computerAvatarService.getDefaultComputerAvatarSettings();
2324
component.version = 2;
24-
component.cRaterRubric = { ideas: [] };
25+
component.cRaterRubric = {
26+
ideas: [],
27+
ideaColors: [],
28+
ideasSummaryGroups: DEFAULT_IDEAS_SUMMARY_GROUPS
29+
};
2530
return component;
2631
}
2732

src/assets/wise5/components/openResponse/edit-open-response-advanced/edit-open-response-advanced.component.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { EditFeedbackRulesComponent } from '../../common/feedbackRule/edit-feedb
1616
import { OpenResponseContent } from '../OpenResponseContent';
1717
import { CRaterItemSelectComponent } from '../../common/cRater/crater-item-select/crater-item-select.component';
1818
import { EditCRaterInfoComponent } from '../../common/cRater/edit-crater-info/edit-crater-info.component';
19+
import { DEFAULT_IDEAS_SUMMARY_GROUPS } from '../../common/cRater/CRaterRubric';
1920

2021
@Component({
2122
imports: [
@@ -95,7 +96,9 @@ export class EditOpenResponseAdvancedComponent extends EditAdvancedComponentComp
9596
enableMultipleAttemptScoringRules: false,
9697
multipleAttemptScoringRules: [],
9798
rubric: {
98-
ideas: []
99+
ideas: [],
100+
ideaColors: [],
101+
ideaSummaryGroups: DEFAULT_IDEAS_SUMMARY_GROUPS
99102
}
100103
};
101104
}

src/assets/wise5/directives/teacher-summary-display/idea-summary/idea-summary.component.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<mat-expansion-panel
22
[disabled]="idea.count === 0"
33
[hideToggle]="idea.count === 0"
4+
[style.background-color]="idea.color"
45
(opened)="toggleDetails()"
56
>
67
<mat-expansion-panel-header>
78
<mat-panel-title>
8-
<span class="font-normal text-sm">{{ idea.id }}. {{ idea.text }}</span>
9+
<span class="text-sm">{{ idea.id }}. {{ idea.text }}</span>
910
</mat-panel-title>
1011
<mat-panel-description>
1112
<span class="font-normal text-sm flex items-center">
@@ -14,10 +15,10 @@
1415
</mat-panel-description>
1516
</mat-expansion-panel-header>
1617
<div class="text-sm bg-white p-1 rounded">
17-
Sample responses:
18+
<span i18n>Sample responses:</span>
1819
<ul>
1920
@for (response of responses; track response.timestamp) {
20-
<li>"{{ response.text }}"</li>
21+
<li i18n>"{{ response.text }}"</li>
2122
}
2223
</ul>
2324
</div>

0 commit comments

Comments
 (0)