Skip to content

Commit 4f55b88

Browse files
authored
Add Token Counter Displayer in Repl (#2775)
* Create new InterpreterOutput type in repl for notifications * Add notification card in Repl component * Add style to notification ReplOutput * Add fields to workspace state for storing notification string * Create logic for handling custom notifications and token counter string * Create button in ground control enable/disable token counter * Edit upload assessment form data to include hasTokenCounter field * Add workspace actions to handle enabling token counter * Revert "Edit upload assessment form data to include hasTokenCounter field" This reverts commit b087c55. * Revert "Create button in ground control enable/disable token counter" This reverts commit 8b1137a. * Add hasTokenCount toggle button on assessment configuration panel * Integrate token counter * Conditionally render token counter depending on assessment config * Remove console.log * Fix bug where turning off token counter would not work in repl * Fix compile time errors * Make token count message more concise * Refactor code for readability and css best practices
1 parent 1208290 commit 4f55b88

File tree

17 files changed

+184
-8
lines changed

17 files changed

+184
-8
lines changed

src/commons/application/ApplicationTypes.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,22 @@ export type ErrorOutput = {
8888
consoleLogs: string[];
8989
};
9090

91-
export type InterpreterOutput = RunningOutput | CodeOutput | ResultOutput | ErrorOutput;
91+
/**
92+
* An output which represents a message being displayed to the user. Not a true
93+
* result from the program, but rather a customised notification meant to highlight
94+
* events that occur outside execution of the program.
95+
*/
96+
export type NotificationOutput = {
97+
type: 'notification';
98+
consoleLog: string;
99+
};
100+
101+
export type InterpreterOutput =
102+
| RunningOutput
103+
| CodeOutput
104+
| ResultOutput
105+
| ErrorOutput
106+
| NotificationOutput;
92107

93108
export enum Role {
94109
Student = 'student',
@@ -360,6 +375,9 @@ export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): Wo
360375
originalValue: ''
361376
},
362377
replValue: '',
378+
hasTokenCounter: false,
379+
tokenCount: 0,
380+
customNotification: '',
363381
sharedbConnected: false,
364382
stepLimit: 1000,
365383
globals: [],

src/commons/application/actions/__tests__/SessionActions.ts

+9
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ test('setAssessmentConfigurations generates correct action object', () => {
274274
type: 'Mission1',
275275
isManuallyGraded: true,
276276
displayInDashboard: true,
277+
hasTokenCounter: false,
277278
hoursBeforeEarlyXpDecay: 48,
278279
earlySubmissionXp: 200
279280
},
@@ -282,6 +283,7 @@ test('setAssessmentConfigurations generates correct action object', () => {
282283
type: 'Mission2',
283284
isManuallyGraded: true,
284285
displayInDashboard: true,
286+
hasTokenCounter: false,
285287
hoursBeforeEarlyXpDecay: 48,
286288
earlySubmissionXp: 200
287289
},
@@ -290,6 +292,7 @@ test('setAssessmentConfigurations generates correct action object', () => {
290292
type: 'Mission3',
291293
isManuallyGraded: true,
292294
displayInDashboard: true,
295+
hasTokenCounter: false,
293296
hoursBeforeEarlyXpDecay: 48,
294297
earlySubmissionXp: 200
295298
}
@@ -640,6 +643,7 @@ test('updateAssessmentTypes generates correct action object', () => {
640643
type: 'Missions',
641644
isManuallyGraded: true,
642645
displayInDashboard: true,
646+
hasTokenCounter: false,
643647
hoursBeforeEarlyXpDecay: 48,
644648
earlySubmissionXp: 200
645649
},
@@ -648,6 +652,7 @@ test('updateAssessmentTypes generates correct action object', () => {
648652
type: 'Quests',
649653
isManuallyGraded: true,
650654
displayInDashboard: true,
655+
hasTokenCounter: false,
651656
hoursBeforeEarlyXpDecay: 48,
652657
earlySubmissionXp: 200
653658
},
@@ -656,6 +661,7 @@ test('updateAssessmentTypes generates correct action object', () => {
656661
type: 'Paths',
657662
isManuallyGraded: true,
658663
displayInDashboard: true,
664+
hasTokenCounter: false,
659665
hoursBeforeEarlyXpDecay: 48,
660666
earlySubmissionXp: 200
661667
},
@@ -664,6 +670,7 @@ test('updateAssessmentTypes generates correct action object', () => {
664670
type: 'Contests',
665671
isManuallyGraded: true,
666672
displayInDashboard: true,
673+
hasTokenCounter: false,
667674
hoursBeforeEarlyXpDecay: 48,
668675
earlySubmissionXp: 200
669676
},
@@ -672,6 +679,7 @@ test('updateAssessmentTypes generates correct action object', () => {
672679
type: 'Others',
673680
isManuallyGraded: true,
674681
displayInDashboard: true,
682+
hasTokenCounter: false,
675683
hoursBeforeEarlyXpDecay: 48,
676684
earlySubmissionXp: 200
677685
}
@@ -689,6 +697,7 @@ test('deleteAssessmentConfig generates correct action object', () => {
689697
type: 'Mission1',
690698
isManuallyGraded: true,
691699
displayInDashboard: true,
700+
hasTokenCounter: false,
692701
hoursBeforeEarlyXpDecay: 48,
693702
earlySubmissionXp: 200
694703
};

src/commons/assessment/AssessmentTypes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export type Assessment = {
8181
type: AssessmentType;
8282
globalDeployment?: Library; // For mission control
8383
graderDeployment?: Library; // For mission control
84+
hasTokenCounter?: boolean;
8485
id: number;
8586
longSummary: string;
8687
missionPDF: string;
@@ -95,6 +96,7 @@ export type AssessmentConfiguration = {
9596
displayInDashboard: boolean;
9697
hoursBeforeEarlyXpDecay: number;
9798
earlySubmissionXp: number;
99+
hasTokenCounter: boolean;
98100
};
99101

100102
export interface IProgrammingQuestion extends BaseQuestion {

src/commons/assessment/__tests__/Assessment.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const mockAssessmentProps = assertType<AssessmentProps>()({
1717
type: 'Missions',
1818
isManuallyGraded: true,
1919
displayInDashboard: true,
20+
hasTokenCounter: false,
2021
hoursBeforeEarlyXpDecay: 48,
2122
earlySubmissionXp: 200
2223
}

src/commons/assessmentWorkspace/AssessmentWorkspace.tsx

+24-3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ import {
7676
changeExecTime,
7777
changeSideContentHeight,
7878
clearReplOutput,
79+
disableTokenCounter,
80+
enableTokenCounter,
7981
evalEditor,
8082
evalRepl,
8183
evalTestcase,
@@ -111,6 +113,7 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
111113
const { isMobileBreakpoint } = useResponsive();
112114

113115
const assessment = useTypedSelector(state => state.session.assessments.get(props.assessmentId));
116+
114117
const [selectedTab, setSelectedTab] = useState(
115118
assessment?.questions[props.questionId].grader !== undefined
116119
? SideContentType.grading
@@ -149,7 +152,9 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
149152
handleEditorUpdateBreakpoints,
150153
handleReplEval,
151154
handleSave,
152-
handleUpdateHasUnsavedChanges
155+
handleUpdateHasUnsavedChanges,
156+
handleEnableTokenCounter,
157+
handleDisableTokenCounter
153158
} = useMemo(() => {
154159
return {
155160
handleTestcaseEval: (id: number) => dispatch(evalTestcase(workspaceLocation, id)),
@@ -173,7 +178,9 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
173178
handleSave: (id: number, answer: number | string | ContestEntry[]) =>
174179
dispatch(submitAnswer(id, answer)),
175180
handleUpdateHasUnsavedChanges: (hasUnsavedChanges: boolean) =>
176-
dispatch(updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges))
181+
dispatch(updateHasUnsavedChanges(workspaceLocation, hasUnsavedChanges)),
182+
handleEnableTokenCounter: () => dispatch(enableTokenCounter(workspaceLocation)),
183+
handleDisableTokenCounter: () => dispatch(disableTokenCounter(workspaceLocation))
177184
};
178185
}, [dispatch]);
179186

@@ -238,6 +245,21 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
238245
checkWorkspaceReset();
239246
});
240247

248+
/**
249+
* Handles toggling enabling and disabling token counter depending on assessment properties
250+
*/
251+
useEffect(() => {
252+
if (props.assessmentConfiguration.hasTokenCounter) {
253+
handleEnableTokenCounter();
254+
} else {
255+
handleDisableTokenCounter();
256+
}
257+
}, [
258+
props.assessmentConfiguration.hasTokenCounter,
259+
handleEnableTokenCounter,
260+
handleDisableTokenCounter
261+
]);
262+
241263
/**
242264
* Handles toggling of relevant SideContentTabs when mobile breakpoint it hit
243265
*/
@@ -839,7 +861,6 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
839861
sideBarProps: sideBarProps,
840862
mobileSideContentProps: mobileSideContentProps(questionId)
841863
};
842-
843864
return (
844865
<div className={classNames('WorkspaceParent', Classes.DARK)}>
845866
{overlay}

src/commons/assessmentWorkspace/__tests__/AssessmentWorkspace.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const defaultProps = assertType<AssessmentWorkspaceProps>()({
2626
type: 'Missions',
2727
isManuallyGraded: true,
2828
displayInDashboard: true,
29+
hasTokenCounter: false,
2930
hoursBeforeEarlyXpDecay: 48,
3031
earlySubmissionXp: 200
3132
},

src/commons/mocks/AssessmentMocks.ts

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
2222
isManuallyGraded: true,
2323
displayInDashboard: true,
2424
hoursBeforeEarlyXpDecay: 48,
25+
hasTokenCounter: false,
2526
earlySubmissionXp: 200
2627
},
2728
{
@@ -30,6 +31,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
3031
isManuallyGraded: true,
3132
displayInDashboard: true,
3233
hoursBeforeEarlyXpDecay: 48,
34+
hasTokenCounter: false,
3335
earlySubmissionXp: 200
3436
},
3537
{
@@ -38,6 +40,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
3840
isManuallyGraded: true,
3941
displayInDashboard: true,
4042
hoursBeforeEarlyXpDecay: 48,
43+
hasTokenCounter: false,
4144
earlySubmissionXp: 200
4245
},
4346
{
@@ -46,6 +49,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
4649
isManuallyGraded: true,
4750
displayInDashboard: true,
4851
hoursBeforeEarlyXpDecay: 48,
52+
hasTokenCounter: true,
4953
earlySubmissionXp: 200
5054
},
5155
{
@@ -54,6 +58,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
5458
isManuallyGraded: true,
5559
displayInDashboard: true,
5660
hoursBeforeEarlyXpDecay: 48,
61+
hasTokenCounter: false,
5762
earlySubmissionXp: 200
5863
}
5964
],
@@ -64,6 +69,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
6469
isManuallyGraded: true,
6570
displayInDashboard: true,
6671
hoursBeforeEarlyXpDecay: 48,
72+
hasTokenCounter: false,
6773
earlySubmissionXp: 200
6874
},
6975
{
@@ -72,6 +78,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
7278
isManuallyGraded: true,
7379
displayInDashboard: true,
7480
hoursBeforeEarlyXpDecay: 48,
81+
hasTokenCounter: false,
7582
earlySubmissionXp: 200
7683
},
7784
{
@@ -80,6 +87,7 @@ export const mockAssessmentConfigurations: AssessmentConfiguration[][] = [
8087
isManuallyGraded: true,
8188
displayInDashboard: true,
8289
hoursBeforeEarlyXpDecay: 48,
90+
hasTokenCounter: false,
8391
earlySubmissionXp: 200
8492
}
8593
]

src/commons/profile/__tests__/Profile.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const assessmentConfigurations: AssessmentConfiguration[] = [
2525
type: c,
2626
isManuallyGraded: false,
2727
displayInDashboard: false,
28+
hasTokenCounter: false,
2829
hoursBeforeEarlyXpDecay: 0,
2930
earlySubmissionXp: 0
3031
}));

src/commons/repl/Repl.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ export const Output: React.FC<OutputProps> = (props: OutputProps) => {
122122
</Card>
123123
);
124124
}
125+
case 'notification':
126+
return (
127+
<Card className="notification-output-container">
128+
<Pre className="notification-output">{'💡 ' + props.output.consoleLog}</Pre>
129+
</Card>
130+
);
125131
default:
126132
return <Card>''</Card>;
127133
}

src/commons/sagas/BackendSaga.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,6 @@ function* BackendSaga(): SagaIterator {
777777
getAssessmentConfigs,
778778
tokens
779779
);
780-
781780
if (assessmentConfigs) {
782781
yield put(actions.setAssessmentConfigurations(assessmentConfigs));
783782
}
@@ -982,6 +981,7 @@ function* BackendSaga(): SagaIterator {
982981
isManuallyGraded: true,
983982
displayInDashboard: true,
984983
hoursBeforeEarlyXpDecay: 0,
984+
hasTokenCounter: false,
985985
earlySubmissionXp: 0
986986
}
987987
];

src/commons/sagas/WorkspaceSaga.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { tokenizer } from 'acorn';
12
import { FSModule } from 'browserfs/dist/node/core/FS';
23
import {
34
Context,
@@ -9,7 +10,7 @@ import {
910
runFilesInContext,
1011
runInContext
1112
} from 'js-slang';
12-
import { TRY_AGAIN } from 'js-slang/dist/constants';
13+
import { ACORN_PARSE_OPTIONS, TRY_AGAIN } from 'js-slang/dist/constants';
1314
import { defineSymbol } from 'js-slang/dist/createContext';
1415
import { InterruptedError } from 'js-slang/dist/errors/errors';
1516
import { parse } from 'js-slang/dist/parser/parser';
@@ -1256,6 +1257,13 @@ export function* evalCode(
12561257

12571258
yield* dumpDisplayBuffer(workspaceLocation, isStoriesBlock, storyEnv);
12581259

1260+
// Change token count if its assessment and EVAL_EDITOR
1261+
if (actionType === EVAL_EDITOR && workspaceLocation === 'assessment') {
1262+
const tokens = [...tokenizer(entrypointCode, ACORN_PARSE_OPTIONS)];
1263+
const tokenCounter = tokens.length;
1264+
yield put(actions.setTokenCount(workspaceLocation, tokenCounter));
1265+
}
1266+
12591267
// Do not write interpreter output to REPL, if executing chunks (e.g. prepend/postpend blocks)
12601268
if (actionType !== EVAL_SILENT) {
12611269
if (!isStoriesBlock) {

src/commons/sagas/__tests__/BackendSaga.ts

+6
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [
218218
isManuallyGraded: true,
219219
displayInDashboard: true,
220220
hoursBeforeEarlyXpDecay: 48,
221+
hasTokenCounter: false,
221222
earlySubmissionXp: 200
222223
},
223224
{
@@ -226,6 +227,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [
226227
isManuallyGraded: true,
227228
displayInDashboard: true,
228229
hoursBeforeEarlyXpDecay: 48,
230+
hasTokenCounter: false,
229231
earlySubmissionXp: 200
230232
},
231233
{
@@ -234,6 +236,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [
234236
isManuallyGraded: false,
235237
displayInDashboard: false,
236238
hoursBeforeEarlyXpDecay: 48,
239+
hasTokenCounter: false,
237240
earlySubmissionXp: 200
238241
},
239242
{
@@ -242,6 +245,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [
242245
isManuallyGraded: false,
243246
displayInDashboard: false,
244247
hoursBeforeEarlyXpDecay: 48,
248+
hasTokenCounter: true,
245249
earlySubmissionXp: 200
246250
},
247251
{
@@ -250,6 +254,7 @@ const mockAssessmentConfigurations: AssessmentConfiguration[] = [
250254
isManuallyGraded: true,
251255
displayInDashboard: false,
252256
hoursBeforeEarlyXpDecay: 48,
257+
hasTokenCounter: false,
253258
earlySubmissionXp: 200
254259
}
255260
];
@@ -1002,6 +1007,7 @@ describe('Test CREATE_COURSE action', () => {
10021007
isManuallyGraded: true,
10031008
displayInDashboard: true,
10041009
hoursBeforeEarlyXpDecay: 0,
1010+
hasTokenCounter: false,
10051011
earlySubmissionXp: 0
10061012
}
10071013
];

0 commit comments

Comments
 (0)