Skip to content

Commit 3c4b50e

Browse files
committed
feat: Add regenerate button and feature for AI hint generation
1 parent d75a2a0 commit 3c4b50e

File tree

9 files changed

+138
-54
lines changed

9 files changed

+138
-54
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"aws-sdk": "^2.1692.0",
1212
"axios": "^1.4.0",
1313
"clsx": "~1.1.1",
14+
"crypto-js": "^4.2.0",
1415
"equation-editor-react": "https://github.com/CAHLR/equation-editor-react-forked/releases/download/v0.0.10/equation-editor-react-0.0.10.tgz",
1516
"expr-eval": "~2.0.2",
1617
"firebase": "~9.9.1",

public/static/images/icons/reload.png

347 Bytes
Loading

src/components/problem-layout/DynamicHintHelper.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import AWS from "aws-sdk";
44
export async function fetchDynamicHint(
55
DYNAMIC_HINT_URL,
66
promptParameters,
7-
onChunkReceived,
8-
onError,
7+
onChunkReceived,
8+
onSuccessfulCompletion,
9+
onError,
910
problemID,
1011
variabilization,
1112
context
1213
) {
1314
try {
1415
AWS.config.update({
15-
accessKeyId: "",
16-
secretAccessKey: "",
16+
accessKeyId: process.env.AWS_ACCESS_KEY,
17+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
1718
region: "us-west-1",
1819
});
1920

@@ -56,8 +57,10 @@ export async function fetchDynamicHint(
5657
while (true) {
5758
const { done, value } = await reader.read();
5859
if (done) {
59-
streamedHint = renderGPTText(streamedHint, problemID, variabilization, context);
60-
console.log("GPT OUTPUT " + streamedHint);
60+
const finalHint = renderGPTText(streamedHint, problemID, variabilization, context);
61+
console.log("GPT OUTPUT: ", finalHint);
62+
onChunkReceived(finalHint); // Call the final processing callback
63+
onSuccessfulCompletion(); //Set `isHintGenerating` to false
6164
break;
6265
} else {
6366
let chunk = decoder.decode(value, { stream: true });
@@ -66,9 +69,7 @@ export async function fetchDynamicHint(
6669
chunk = ensureProperTermination(chunk);
6770
// Add the chunk to the streamedHint
6871
streamedHint += chunk;
69-
console.log(streamedHint);
7072
}
71-
// Callback to process chunks
7273
onChunkReceived(streamedHint);
7374
}
7475
} catch (error) {

src/components/problem-layout/HintSystem.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Spacer from "../Spacer";
1616
import { stagingProp } from "../../util/addStagingProperty";
1717
import ErrorBoundary from "../ErrorBoundary";
1818
import withTranslation from '../../util/withTranslation';
19+
import ReloadIcon from './ReloadIcon';
1920

2021
class HintSystem extends React.Component {
2122
static contextType = ThemeContext;
@@ -38,7 +39,7 @@ class HintSystem extends React.Component {
3839
this.unlockFirstHint = props.unlockFirstHint;
3940
this.isIncorrect = props.isIncorrect;
4041
this.giveHintOnIncorrect = props.giveHintOnIncorrect
41-
42+
this.generateHintFromGPT = props.generateHintFromGPT;
4243
this.state = {
4344
latestStep: 0,
4445
currentExpanded: (this.unlockFirstHint || this.isIncorrect) ? 0 : -1,
@@ -57,14 +58,14 @@ class HintSystem extends React.Component {
5758
}
5859

5960
unlockHint = (event, expanded, i) => {
60-
console.log("UNLOCK HINT", i);
61-
console.log(this.state.currentExpanded);
61+
//console.log("UNLOCK HINT", i);
62+
//console.log(this.state.currentExpanded);
6263
if (this.state.currentExpanded === i ) {
6364
this.setState({ currentExpanded: -1 });
6465
} else {
6566
this.setState({ currentExpanded: i });
66-
console.log("EXPANDED", expanded);
67-
console.log("HINTSTATUS LENGTH", this.props.hintStatus.length);
67+
//console.log("EXPANDED", expanded);
68+
//console.log("HINTSTATUS LENGTH", this.props.hintStatus.length);
6869
if (expanded && i < this.props.hintStatus.length) {
6970
this.props.unlockHint(i, this.props.hints[i].type);
7071
}
@@ -77,12 +78,12 @@ class HintSystem extends React.Component {
7778
return false;
7879
}
7980
var dependencies = this.props.hints[hintNum].dependencies;
80-
console.log("DEPENDENCIES", dependencies);
81-
console.log(this.props.hintStatus);
81+
//console.log("DEPENDENCIES", dependencies);
82+
//console.log(this.props.hintStatus);
8283
var isSatisfied = dependencies.every(
8384
(dependency) => this.props.hintStatus[dependency] === 1
8485
);
85-
console.log("IS SATSIFIED", hintNum, isSatisfied);
86+
//console.log("IS SATSIFIED", hintNum, isSatisfied);
8687
return !isSatisfied;
8788
};
8889

@@ -204,7 +205,8 @@ class HintSystem extends React.Component {
204205
)}
205206
</Typography>
206207
</AccordionSummary>
207-
<AccordionDetails>
208+
<AccordionDetails >
209+
<div style={{ width: "100%" }}>
208210
<Typography
209211
component={"span"}
210212
style={{ width: "100%" }}
@@ -287,6 +289,27 @@ class HintSystem extends React.Component {
287289
""
288290
)}
289291
</Typography>
292+
{/* Check if the hint is dynamic (AI-generated) */}
293+
{hint.type === "gptHint"
294+
&& !this.props.isGeneratingHint
295+
&& (
296+
<div
297+
style={{
298+
display: "flex",
299+
justifyContent: "flex-end", // Align to the right
300+
}}
301+
>
302+
<ReloadIcon
303+
style={{
304+
cursor: "pointer", // Add pointer cursor for interactivity
305+
fontSize: "24px", // Adjust size if necessary
306+
}}
307+
onClick={() => this.generateHintFromGPT(true)}
308+
title="Regenerate Hint"
309+
/>
310+
</div>
311+
)}
312+
</div>
290313
</AccordionDetails>
291314
</Accordion>
292315
))}

src/components/problem-layout/ProblemCard.js

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from "./ToastNotifyCorrectness";
3535
import { joinList } from "../../util/formListString";
3636
import withTranslation from "../../util/withTranslation.js"
37+
import CryptoJS from "crypto-js";
3738

3839
class ProblemCard extends React.Component {
3940
static contextType = ThemeContext;
@@ -142,6 +143,8 @@ class ProblemCard extends React.Component {
142143
enableHintGeneration: true,
143144
activeHintType: "none", // "none", or "normal".
144145
hints: this.hints,
146+
isGeneratingHint: false,
147+
lastAIHintHash: null,
145148
};
146149

147150
// This is used for AI hint generation
@@ -158,6 +161,10 @@ class ProblemCard extends React.Component {
158161
}
159162
}
160163

164+
hashAnswer = (answer) => {
165+
return CryptoJS.SHA256(answer).toString();
166+
};
167+
161168
_findHintId = (hints, targetId) => {
162169
for (var i = 0; i < hints.length; i++) {
163170
if (hints[i].id === targetId) {
@@ -298,39 +305,28 @@ class ProblemCard extends React.Component {
298305
}
299306
};
300307

301-
toggleHints = () => {
302-
303-
// If AI hints are active
304-
if (this.giveDynamicHint) {
305-
// AI hints: Always show and regenerate on click
306-
this.setState(
307-
{
308-
activeHintType: "normal", // Ensure AI hints are always visible
309-
},
310-
() => {
311-
this.generateHintFromGPT(); // Regenerate AI hint
312-
}
313-
);
314-
} else {
315-
// Otherwise, toggle the hint system visibility
308+
toggleHints = (event) => {
309+
if (this.giveDynamicHint && !this.state.activeHintType !== "normal") {
310+
this.generateHintFromGPT();
311+
} else if (!this.state.displayHints) {
316312
this.setState(
317-
(prevState) => ({
313+
() => ({
318314
enableHintGeneration: false,
319-
activeHintType: prevState.activeHintType === "normal" ? "none" : "normal",
320-
}),
321-
() => {
322-
if (!this.state.displayHints) {
323-
this.props.answerMade(
324-
this.index,
325-
this.step.knowledgeComponents,
326-
false
327-
);
328-
}
329-
}
330-
);
315+
}))
331316
}
317+
this.setState(
318+
(prevState) => ({
319+
activeHintType: prevState.activeHintType === "normal" ? "none" : "normal"
320+
}),
321+
() => {
322+
this.props.answerMade(
323+
this.index,
324+
this.step.knowledgeComponents,
325+
false
326+
);
327+
}
328+
);
332329
};
333-
334330

335331
unlockHint = (hintNum, hintType) => {
336332
// Mark question as wrong if hints are used (on the first time)
@@ -459,9 +455,28 @@ class ProblemCard extends React.Component {
459455
}
460456

461457
};
462-
generateHintFromGPT = async () => {
458+
459+
generateHintFromGPT = async (forceRegenerate) => {
460+
const { inputVal, lastAIHintHash, isGeneratingHint } = this.state;
461+
462+
const currentHash = this.hashAnswer(inputVal);
463+
464+
// If a hint is currently being generated through streaming,
465+
// do not generate a new hint
466+
if (isGeneratingHint) {
467+
return;
468+
}
469+
470+
// If the current hash matches the last hash, skip regeneration
471+
if ((currentHash === lastAIHintHash) && !forceRegenerate) {
472+
console.log("Hint already generated for this answer, skipping regeneration.");
473+
return;
474+
}
475+
463476
this.setState({
464477
dynamicHint: "Loading...", // Clear previous hint
478+
isGeneratingHint: true,
479+
lastAIHintHash: currentHash,
465480
});
466481

467482
const [parsed, correctAnswer, reason] = checkAnswer({
@@ -493,8 +508,21 @@ class ProblemCard extends React.Component {
493508
),
494509
}));
495510
};
511+
512+
/** When the hint generation is completed, set the `isGeneratingHint` state to false
513+
* in order to regenerate the hint.
514+
*/
515+
const onSuccessfulCompletion = () => {
516+
this.setState({
517+
isGeneratingHint: false,
518+
});
519+
}
496520

497521
const onError = (error) => {
522+
523+
this.setState({
524+
isGeneratingHint: false,
525+
})
498526
console.error("Error generating AI hint:", error);
499527

500528
// Revert to manual hints and update state
@@ -566,6 +594,7 @@ class ProblemCard extends React.Component {
566594
DYNAMIC_HINT_URL,
567595
this.generateGPTHintParameters(this.prompt_template, this.state.bioInfo),
568596
onChunkReceived,
597+
onSuccessfulCompletion,
569598
onError,
570599
this.props.problemID,
571600
chooseVariables(
@@ -677,6 +706,8 @@ class ProblemCard extends React.Component {
677706
lesson={this.props.lesson}
678707
courseName={this.props.courseName}
679708
isIncorrect={this.expandFirstIncorrect}
709+
generateHintFromGPT={this.generateHintFromGPT}
710+
isGeneratingHint={this.state.isGeneratingHint}
680711
/>
681712
</ErrorBoundary>
682713
<Spacer />
@@ -723,9 +754,9 @@ class ProblemCard extends React.Component {
723754
{this.showHints && (
724755
<center>
725756
<IconButton
726-
aria-label="hint-toggle"
757+
aria-label="delete"
727758
onClick={this.toggleHints}
728-
title={this.giveDynamicHint ? "Regenerate AI Hint" : "View/Hide Hints"}
759+
title="View available hints"
729760
disabled={
730761
!this.state.enableHintGeneration
731762
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import Tooltip from '@material-ui/core/Tooltip';
3+
import IconButton from '@material-ui/core/IconButton';
4+
5+
const ReloadIconButton = ({ onClick }) => (
6+
<Tooltip title="Regenerate Hint">
7+
<IconButton onClick={onClick} aria-label="regenerate">
8+
<img
9+
src={`${process.env.PUBLIC_URL}/static/images/icons/reload.png`}
10+
alt="Reload Icon"
11+
style={{ width: '24px', height: '24px' }}
12+
/>
13+
</IconButton>
14+
</Tooltip>
15+
);
16+
17+
export default ReloadIconButton;

src/config/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const MIDDLEWARE_URL =
9191
const HELP_DOCUMENT =
9292
"https://docs.google.com/document/d/e/2PACX-1vToe2F3RiCx1nwcX9PEkMiBA2bFy9lQRaeWIbyqlc8W_KJ9q-hAMv34QaO_AdEelVY7zjFAF1uOP4pG/pub";
9393

94-
const DYNAMIC_HINT_URL = "";
94+
const DYNAMIC_HINT_URL = process.env.AI_HINT_GENERATION_AWS_ENDPOINT;
9595

9696
const DYNAMIC_HINT_TEMPLATE =
9797
"<{problem_title}.> <{problem_subtitle}.> <{question_title}.> <{question_subtitle}.> <Student's answer is: {student_answer}.> <The correct answer is: {correct_answer}.> Please give a hint for this.";

src/platform-logic/renderText.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,16 @@ function preprocessChatGPTResponse(input) {
210210
// Step 2: Replace monetary values ($12,000) with (\uFF04)12,000
211211
const moneyRegex = /\$(\d{1,3}(,\d{3})*(\.\d{2})?|(\d+))/g;
212212
input = input.replace(moneyRegex, (_, moneyValue) => `\uFF04${moneyValue}`);
213-
console.log("TRANSFORMATION 1", input);
213+
//console.log("TRANSFORMATION 1", input);
214214

215215
// Step 3: Convert any remaining single dollar signs (not part of monetary values) to double dollars
216216
input = input.replace(/(?<!\$)\$(?!\$)/g, "$$$$");
217-
console.log("TRANSFORMATION 2", input);
217+
//console.log("TRANSFORMATION 2", input);
218218

219219
// Step 4: Replace all instances of \uFF04 back to $
220-
console.log((input.match(/\uFF04/g) || []).length);
220+
//console.log((input.match(/\uFF04/g) || []).length);
221221
input = input.replace(/\uFF04/g, "$");
222-
console.log("TRANSFORMATION 3", input);
222+
//console.log("TRANSFORMATION 3", input);
223223
return input;
224224
}
225225

0 commit comments

Comments
 (0)