Skip to content

Commit

Permalink
feat(Feedback Rule): New ChoseChoice token
Browse files Browse the repository at this point in the history
  • Loading branch information
hirokiterashima committed Sep 8, 2023
1 parent 21d0fc6 commit 3d119a1
Show file tree
Hide file tree
Showing 16 changed files with 144 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ let evaluator: FeedbackRuleEvaluator<CRaterResponse[]>;
describe('FeedbackRuleEvaluator', () => {
beforeEach(() => {
evaluator = new FeedbackRuleEvaluator(
new FeedbackRuleComponent(DEFAULT_FEEDBACK_RULES, 5, true)
new FeedbackRuleComponent(DEFAULT_FEEDBACK_RULES, 5, true),
null
);
});
matchRule_OneIdea();
Expand Down Expand Up @@ -76,7 +77,8 @@ function matchRule_hasKIScore() {
describe('hasKIScore()', () => {
beforeEach(() => {
evaluator = new FeedbackRuleEvaluator(
new FeedbackRuleComponent(HAS_KI_SCORE_FEEDBACK_RULES, 5, true)
new FeedbackRuleComponent(HAS_KI_SCORE_FEEDBACK_RULES, 5, true),
null
);
});
matchRule_hasKIScoreScoreInRange_ShouldMatchRule();
Expand Down Expand Up @@ -119,7 +121,10 @@ function matchRule_ideaCount() {
feedback: 'ideaCountLessThan(3)'
})
];
evaluator = new FeedbackRuleEvaluator(new FeedbackRuleComponent(feedbackRules, 5, true));
evaluator = new FeedbackRuleEvaluator(
new FeedbackRuleComponent(feedbackRules, 5, true),
null
);
});
matchRule_ideaCount_MatchRulesBasedOnNumIdeasFound();
});
Expand All @@ -142,7 +147,7 @@ function matchNoRule_ReturnDefault() {
function matchNoRule_NoDefaultFeedbackAuthored_ReturnApplicationDefault() {
it(`should return application default rule when no rule is matched and no default is
authored`, () => {
evaluator = new FeedbackRuleEvaluator(new FeedbackRuleComponent([], 5, true));
evaluator = new FeedbackRuleEvaluator(new FeedbackRuleComponent([], 5, true), null);
expectFeedback(['idea10', 'idea11'], [KI_SCORE_1], 1, evaluator.defaultFeedback);
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConstraintService } from '../../../services/constraintService';
import { FeedbackRuleComponent } from '../../feedbackRule/FeedbackRuleComponent';
import { CRaterResponse } from '../cRater/CRaterResponse';
import { FeedbackRule } from './FeedbackRule';
Expand All @@ -7,9 +8,14 @@ import { TermEvaluatorFactory } from './TermEvaluator/TermEvaluatorFactory';

export class FeedbackRuleEvaluator<T extends CRaterResponse[]> {
defaultFeedback = $localize`Thanks for submitting your response.`;
protected factory = new TermEvaluatorFactory();
protected factory;

constructor(protected component: FeedbackRuleComponent) {}
constructor(
protected component: FeedbackRuleComponent,
protected constraintService: ConstraintService
) {
this.factory = new TermEvaluatorFactory(constraintService);
}

getFeedbackRule(responses: T): FeedbackRule {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ let evaluator: FeedbackRuleEvaluatorMultipleStudents;
describe('FeedbackRuleEvaluatorMultipleStudents', () => {
beforeEach(() => {
evaluator = new FeedbackRuleEvaluatorMultipleStudents(
new FeedbackRuleComponent(DEFAULT_FEEDBACK_RULES, 5, true)
new FeedbackRuleComponent(DEFAULT_FEEDBACK_RULES, 5, true),
null
);
});
matchRules_OneIdea();
Expand All @@ -38,7 +39,8 @@ function matchRules_HasKIScore() {
describe('hasKIScoreScore', () => {
beforeEach(() => {
evaluator = new FeedbackRuleEvaluatorMultipleStudents(
new FeedbackRuleComponent(HAS_KI_SCORE_FEEDBACK_RULES, 5, true)
new FeedbackRuleComponent(HAS_KI_SCORE_FEEDBACK_RULES, 5, true),
null
);
});
matchRules_hasKIScoreScoreInRange_ShouldMatchRule();
Expand Down Expand Up @@ -69,7 +71,10 @@ function matchNoRule_ReturnDefault() {
function matchNoRule_NoDefaultFeedbackAuthored_ReturnApplicationDefault() {
it(`should return application default rule when no rule is matched and no default is
authored`, () => {
evaluator = new FeedbackRuleEvaluatorMultipleStudents(new FeedbackRuleComponent([], 5, true));
evaluator = new FeedbackRuleEvaluatorMultipleStudents(
new FeedbackRuleComponent([], 5, true),
null
);
expectRules([createCRaterResponse(['idea10', 'idea11'], [KI_SCORE_1], 1)], ['default']);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class FeedbackRuleEvaluatorMultipleStudents extends FeedbackRuleEvaluator
}

protected evaluateTerm(term: string, responses: CRaterResponse[]): boolean {
const evaluator: TermEvaluator = this.factory.getTermEvaluator(term);
const evaluator: TermEvaluator = this.factory.getTermEvaluator(term, this.constraintService);
return responses.some((response: CRaterResponse) => {
return evaluator.evaluate(response);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class FeedbackRuleExpression {
return this.text
.replace(/ /g, '')
.split(
/(hasKIScore\(\d\)|accumulatedIdeaCountEquals\(\d\)|accumulatedIdeaCountLessThan\(\d\)|accumulatedIdeaCountMoreThan\(\d\)|ideaCountEquals\(\S+\)|ideaCountLessThan\(\S+\)|ideaCountMoreThan\(\S+\)|isSubmitNumber\(\d+\)|&&|\|\||!|\(|\))/g
/(hasKIScore\(\d\)|accumulatedIdeaCountEquals\(\d\)|accumulatedIdeaCountLessThan\(\d\)|accumulatedIdeaCountMoreThan\(\d\)|ideaCountEquals\(\S+\)|ideaCountLessThan\(\S+\)|ideaCountMoreThan\(\S+\)|choseChoice\(\S+\)|isSubmitNumber\(\d+\)|&&|\|\||!|\(|\))/g
)
.filter((el) => el !== '');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ConstraintService } from '../../../../services/constraintService';
import { CRaterResponse } from '../../cRater/CRaterResponse';
import { ChoseChoiceTermEvaluator } from './ChoseChoiceTermEvaluator';

class ConstraintServiceStub {
evaluateCriteria(criteria: any): boolean {
return true;
}
}

describe('ChoseChoiceTermEvaluator', () => {
let evaluator1, mockConstraintService;
beforeEach(() => {
evaluator1 = new ChoseChoiceTermEvaluator('choseChoice("node1", "componentA", "choice1")');
mockConstraintService = new ConstraintServiceStub();
evaluator1.setConstraintService(mockConstraintService as ConstraintService);
});
describe('evaluate()', () => {
[
{ description: 'choice is chosen', choiceChosen: true, expected: true },
{ description: 'choice is not chosen', choiceChosen: false, expected: false }
].forEach(({ description, choiceChosen, expected }) => {
describe(description, () => {
beforeEach(() => {
spyOn(mockConstraintService, 'evaluateCriteria').and.returnValue(choiceChosen);
});
it(`returns ${expected}`, () => {
expect(evaluator1.evaluate(new CRaterResponse())).toEqual(expected);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CRaterResponse } from '../../cRater/CRaterResponse';
import { TermEvaluator } from './TermEvaluator';

export class ChoseChoiceTermEvaluator extends TermEvaluator {
private nodeId: string;
private componentId: string;
private choiceId: string;

constructor(term: string) {
super(term);
const matches = term.match(/choseChoice\("(\w+)",\s*"(\w+)",\s*"(\w+)"\)/);
this.nodeId = matches[1];
this.componentId = matches[2];
this.choiceId = matches[3];
}

evaluate(response: CRaterResponse | CRaterResponse[]): boolean {
return this.constraintService.evaluateCriteria({
name: 'choiceChosen',
params: {
nodeId: this.nodeId,
componentId: this.componentId,
choiceIds: this.choiceId
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { ConstraintService } from '../../../../services/constraintService';
import { CRaterResponse } from '../../cRater/CRaterResponse';

export abstract class TermEvaluator {
protected constraintService: ConstraintService;

constructor(protected term: string) {}
abstract evaluate(response: CRaterResponse | CRaterResponse[]): boolean;

static isAccumulatedIdeaCountTerm(term: string): boolean {
return /accumulatedIdeaCount(MoreThan|Equals|LessThan)\([\d+]\)/.test(term);
}

static isChoseChoiceTerm(term: string): boolean {
return /choseChoice\("\w+",\s*"\w+",\s*"\w+"\)/.test(term);
}

static isHasKIScoreTerm(term: string): boolean {
return /hasKIScore\([1-5]\)/.test(term);
}
Expand All @@ -30,4 +37,8 @@ export abstract class TermEvaluator {
TermEvaluator.isIdeaCountWithResponseIndexTerm(term)
);
}

setConstraintService(service: ConstraintService) {
this.constraintService = service;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { ChoseChoiceTermEvaluator } from './ChoseChoiceTermEvaluator';
import { HasKIScoreTermEvaluator } from './HasKIScoreTermEvaluator';
import { IdeaCountTermEvaluator } from './IdeaCountTermEvaluator';
import { IdeaTermEvaluator } from './IdeaTermEvaluator';
import { IsSubmitNumberEvaluator } from './IsSubmitNumberEvaluator';
import { TermEvaluatorFactory } from './TermEvaluatorFactory';

describe('TermEvaluatorFactory', () => {
const factory = new TermEvaluatorFactory();
const factory = new TermEvaluatorFactory(null);
describe('getTermEvaluator()', () => {
it('should return correct evaluator', () => {
[
{
term: 'choseChoice("node1", "componentA", "choice1")',
instanceType: ChoseChoiceTermEvaluator
},
{ term: 'hasKIScore(3)', instanceType: HasKIScoreTermEvaluator },
{ term: 'ideaCountMoreThan(1)', instanceType: IdeaCountTermEvaluator },
{ term: 'ideaCountEquals(3)', instanceType: IdeaCountTermEvaluator },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ConstraintService } from '../../../../services/constraintService';
import { AccumulatedIdeaCountTermEvaluator } from './AccumulatedIdeaCountTermEvaluator';
import { ChoseChoiceTermEvaluator } from './ChoseChoiceTermEvaluator';
import { HasKIScoreTermEvaluator } from './HasKIScoreTermEvaluator';
import { IdeaCountTermEvaluator } from './IdeaCountTermEvaluator';
import { IdeaCountWithResponseIndexTermEvaluator } from './IdeaCountWithResponseIndexTermEvaluator';
Expand All @@ -7,6 +9,8 @@ import { IsSubmitNumberEvaluator } from './IsSubmitNumberEvaluator';
import { TermEvaluator } from './TermEvaluator';

export class TermEvaluatorFactory {
constructor(private constraintService: ConstraintService) {}

getTermEvaluator(term: string): TermEvaluator {
let evaluator: TermEvaluator;
if (TermEvaluator.isHasKIScoreTerm(term)) {
Expand All @@ -19,9 +23,12 @@ export class TermEvaluatorFactory {
evaluator = new IsSubmitNumberEvaluator(term);
} else if (TermEvaluator.isAccumulatedIdeaCountTerm(term)) {
evaluator = new AccumulatedIdeaCountTermEvaluator(term);
} else if (TermEvaluator.isChoseChoiceTerm(term)) {
evaluator = new ChoseChoiceTermEvaluator(term);
} else {
evaluator = new IdeaTermEvaluator(term);
}
evaluator.setConstraintService(this.constraintService);
return evaluator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ComponentStudent } from '../../component-student.component';
import { DialogGuidanceComponent } from '../DialogGuidanceComponent';
import { copy } from '../../../common/object/object';
import { RawCRaterResponse } from '../../common/cRater/RawCRaterResponse';
import { ConstraintService } from '../../../services/constraintService';

@Component({
selector: 'dialog-guidance-student',
Expand All @@ -50,6 +51,7 @@ export class DialogGuidanceStudentComponent extends ComponentStudent {
protected componentService: ComponentService,
protected computerAvatarService: ComputerAvatarService,
protected configService: ConfigService,
private constraintService: ConstraintService,
protected cRaterService: CRaterService,
protected dialog: MatDialog,
protected dialogGuidanceFeedbackService: DialogGuidanceFeedbackService,
Expand Down Expand Up @@ -86,7 +88,8 @@ export class DialogGuidanceStudentComponent extends ComponentStudent {
this.component.getFeedbackRules(),
this.getMaxSubmitCount(),
this.component.isMultipleFeedbackTextsForSameRuleAllowed()
)
),
this.constraintService
);
if (this.component.isComputerAvatarEnabled()) {
this.initializeComputerAvatar();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { OpenResponseService } from '../openResponseService';
import { copy } from '../../../common/object/object';
import { RawCRaterResponse } from '../../common/cRater/RawCRaterResponse';
import { hasConnectedComponent } from '../../../common/ComponentContent';
import { ConstraintService } from '../../../services/constraintService';

@Component({
selector: 'open-response-student',
Expand All @@ -38,6 +39,7 @@ export class OpenResponseStudent extends ComponentStudent {
protected AnnotationService: AnnotationService,
private changeDetector: ChangeDetectorRef,
protected ComponentService: ComponentService,
private constraintService: ConstraintService,
protected ConfigService: ConfigService,
private CRaterService: CRaterService,
protected dialog: MatDialog,
Expand Down Expand Up @@ -399,7 +401,8 @@ export class OpenResponseStudent extends ComponentStudent {
this.getFeedbackRules(),
this.getMaxSubmitCount(),
this.isMultipleFeedbackTextsForSameRuleAllowed()
)
),
this.constraintService
);
const rule: FeedbackRule = feedbackRuleEvaluator.getFeedbackRule([response]);
autoComment = this.getFeedbackText(rule);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { concatMap, map } from 'rxjs/operators';
import { PeerGroup } from '../PeerGroup';
import { QuestionBankContent } from './QuestionBankContent';
import { copy } from '../../../common/object/object';
import { ConstraintService } from '../../../services/constraintService';

@Component({
selector: 'peer-chat-question-bank',
Expand All @@ -26,7 +27,11 @@ export class PeerChatQuestionBankComponent implements OnInit {
@Output() displayedQuestionBankRulesChange = new EventEmitter<QuestionBankRule[]>();
questions: string[];

constructor(private peerGroupService: PeerGroupService, private projectService: ProjectService) {}
constructor(
private constraintService: ConstraintService,
private peerGroupService: PeerGroupService,
private projectService: ProjectService
) {}

ngOnInit(): void {
if (this.displayedQuestionBankRules == null) {
Expand Down Expand Up @@ -80,7 +85,8 @@ export class PeerChatQuestionBankComponent implements OnInit {
this.content.questionBank.getRules(),
(referenceComponent.content as OpenResponseContent).maxSubmitCount,
false
)
),
this.constraintService
);
return this.filterQuestions(
feedbackRuleEvaluator.getFeedbackRules(cRaterResponses) as QuestionBankRule[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ProjectService } from '../../services/projectService';
import { StudentDataService } from '../../services/studentDataService';
import { DynamicPrompt } from './DynamicPrompt';
import { FeedbackRuleEvaluatorMultipleStudents } from '../../components/common/feedbackRule/FeedbackRuleEvaluatorMultipleStudents';
import { ConstraintService } from '../../services/constraintService';

@Component({
selector: 'dynamic-prompt',
Expand All @@ -32,6 +33,7 @@ export class DynamicPromptComponent implements OnInit {
constructor(
private annotationService: AnnotationService,
private configService: ConfigService,
private constraintService: ConstraintService,
private peerGroupService: PeerGroupService,
private projectService: ProjectService,
private dataService: StudentDataService
Expand Down Expand Up @@ -76,7 +78,8 @@ export class DynamicPromptComponent implements OnInit {
this.dynamicPrompt.getRules(),
referenceComponentContent.maxSubmitCount,
false
)
),
this.constraintService
);
const feedbackRule: FeedbackRule = feedbackRuleEvaluator.getFeedbackRule(cRaterResponses);
this.prompt = feedbackRule.prompt;
Expand Down Expand Up @@ -126,7 +129,8 @@ export class DynamicPromptComponent implements OnInit {
this.dynamicPrompt.getRules(),
referenceComponentContent.maxSubmitCount,
false
)
),
this.constraintService
);
const feedbackRule: FeedbackRule = feedbackRuleEvaluator.getFeedbackRule([cRaterResponse]);
this.prompt = feedbackRule.prompt;
Expand Down
2 changes: 1 addition & 1 deletion src/assets/wise5/services/constraintService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class ConstraintService {
: multipleRemovalCriteria.every((criteria) => this.evaluateCriteria(criteria));
}

private evaluateCriteria(criteria: any): boolean {
evaluateCriteria(criteria: any): boolean {
const strategy = this.criteriaFunctionNameToStrategy[criteria.name];
return strategy == null || this.evaluateStrategy(criteria, strategy);
}
Expand Down
Loading

0 comments on commit 3d119a1

Please sign in to comment.