Skip to content

Commit 99efbe6

Browse files
New version of 'Modeling Tree Growth'
1 parent 4697019 commit 99efbe6

27 files changed

+1934
-382
lines changed

cs/220/2025/Haskell/7-Modeling-Tree-Growth/index.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<!-- external resources (editor, zip)-->
99
<script src="js/ext/src-min-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
1010
<script src="js/ext/src-min-noconflict/ext-modelist.js" type="text/javascript" charset="utf-8"></script>
11+
<script src="js/ext/markdown-it.min.js" type="text/javascript" charset="utf-8"></script>
1112
<script src="js/ext/jszip.min.js" type="text/javascript" charset="utf-8"></script>
1213
<!-- non-ui playback -->
1314
<script src="js/playbackEngine/EditorState.js"></script>
@@ -19,12 +20,15 @@
1920
<!-- playback ui -->
2021
<script src="js/AceEditor.js"></script>
2122
<script src="js/AddEditComment.js"></script>
23+
<script src="js/AIGeneratedQuestion.js"></script>
24+
<script src="js/AIPromptInput.js"></script>
2225
<script src="js/App.js"></script>
2326
<script src="js/AudioVideoRecorder.js"></script>
2427
<script src="js/BlogCodeSnippet.js"></script>
2528
<script src="js/BlogComponent.js"></script>
2629
<script src="js/BlogView.js"></script>
2730
<script src="js/CodeView.js"></script>
31+
<script src="js/Collapsable.js"></script>
2832
<script src="js/CommentGroup.js"></script>
2933
<script src="js/CommentNavigator.js"></script>
3034
<script src="js/CommentTags.js"></script>
@@ -45,6 +49,7 @@
4549
<script src="js/SearchBar.js"></script>
4650
<script src="js/ShowHideComponent.js"></script>
4751
<script src="js/SurroundingLinesSelector.js"></script>
52+
<script src="js/TextToSpeechControl.js"></script>
4853
<script src="js/TagView.js"></script>
4954
<script src="js/TitleBar.js"></script>
5055
<script src="js/VerticalMediaContainer.js"></script>
@@ -75,13 +80,16 @@
7580
//look to see if there is a mode in the querystring
7681
const urlSearchParams = new URLSearchParams(window.location.search);
7782
const mode = urlSearchParams.get('mode');
83+
//for positional queries in the playback (make the numbers zero-based as that is what normal humans expect)
84+
const startingEventIndex = urlSearchParams.get('event') ? Number(urlSearchParams.get('event')) - 1 : null;
85+
const startingCommentIndex = urlSearchParams.get('comment') ? Number(urlSearchParams.get('comment')) - 1 : null;
7886

7987
//check if this is a small screen or an explicit request for blog mode
8088
let initialMode = 'code';
8189
if(window.innerWidth < 800 || (mode && mode === 'blog')) {
8290
initialMode = 'blog';
8391
}
84-
const app = new App(playbackData, initialMode);
92+
const app = new App(playbackData, initialMode, startingCommentIndex, startingEventIndex);
8593
document.body.appendChild(app);
8694
});
8795
</script>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
class AIGeneratedQuestion extends HTMLElement {
2+
constructor(playbackEngine, sendResponseAsEvent=false) {
3+
super();
4+
5+
this.playbackEngine = playbackEngine;
6+
this.sendResponseAsEvent = sendResponseAsEvent;
7+
8+
this.attachShadow({ mode: 'open' });
9+
this.shadowRoot.innerHTML = `
10+
<style>
11+
:host {
12+
}
13+
14+
#submitButton {
15+
margin: 5px;
16+
padding: 5px;
17+
display: block;
18+
margin-left: auto;
19+
margin-right: auto;
20+
background-color: transparent;
21+
color: lightgrey;
22+
border: 1px solid lightgrey;
23+
border-radius: .25rem;
24+
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
25+
opacity: 0.8;
26+
}
27+
28+
#submitButton:hover {
29+
opacity: 1.0;
30+
}
31+
</style>
32+
<button id="submitButton">Generate A Self-Grading Multiple Choice Question</button>
33+
<div class="questionContainer">
34+
</div>
35+
`;
36+
}
37+
38+
connectedCallback() {
39+
const submitButton = this.shadowRoot.querySelector('#submitButton');
40+
submitButton.addEventListener('click', this.submitText);
41+
}
42+
43+
disconnectedCallback() {
44+
const submitButton = this.shadowRoot.querySelector('#submitButton');
45+
submitButton.removeEventListener('click', this.submitText);
46+
}
47+
48+
submitText = async () => {
49+
const submitButton = this.shadowRoot.querySelector('#submitButton');
50+
let codeFromPlayback = this.playbackEngine.getMostRecentFileEdits(false);
51+
let promptWithCode = `
52+
Look at the following code and come up with a multiple choice question that can be asked about it:\n\n${codeFromPlayback}\n\n
53+
Look at the differences and come up with a question that a learner will find it challenging to answer.\n\n
54+
The format of the response should be raw JSON (no markdown, specifically no \`\`\`json designators) with the question in a member called 'question', another member called 'allAnswers' that is an array of all the answers.
55+
The correct answer should be duplicated in a member called 'correctAnswer'. The correct answer should have a very brief explanation of why it is correct stored in a member called 'explanation'.\n\n
56+
Put the correct answer at position 0 of the 'allAnswers' array.
57+
Here is an example of what the response should look like (make sure the response is a valid JSON object):\n\n
58+
{\n
59+
"question": "What is the capital of France?",\n
60+
"allAnswers": [\n
61+
"Paris",\n
62+
"London",\n
63+
"Berlin",\n
64+
"Madrid"\n
65+
],\n
66+
"correctAnswer": "Paris",\n
67+
"explanation": "Paris is the capital of France."\n
68+
}\n\n
69+
If you cannot come up with a question then return this object {question: "Error"}`;
70+
71+
let promptObject = {
72+
requestType: "Generate Question",
73+
prompt: promptWithCode,
74+
playbackViewId: document.body.dataset.playbackViewId ? document.body.dataset.playbackViewId : null
75+
};
76+
77+
submitButton.textContent = `Generating a question...`;
78+
submitButton.setAttribute('disabled', 'true');
79+
80+
//send the formatted one to the server
81+
const serverProxy = new ServerProxy();
82+
const responseObject = await serverProxy.sendAIPromptToServer(promptObject);
83+
84+
submitButton.textContent = 'Generate Another Self-Grading Multiple Choice Question';
85+
submitButton.removeAttribute('disabled');
86+
87+
const questionContainer = this.shadowRoot.querySelector('.questionContainer');
88+
89+
if(responseObject.error) {
90+
questionContainer.textContent = responseObject.response;
91+
} else {
92+
//turn the string response w/Q&A into an object
93+
const questionCommentData = JSON.parse(responseObject.response);
94+
95+
//swap the correct answer with a random one
96+
const randomIndex = Math.floor(Math.random() * questionCommentData.allAnswers.length);
97+
const temp = questionCommentData.allAnswers[0];
98+
questionCommentData.allAnswers[0] = questionCommentData.allAnswers[randomIndex];
99+
questionCommentData.allAnswers[randomIndex] = temp;
100+
101+
const md = markdownit();
102+
questionCommentData.question = md.render(questionCommentData.question);
103+
questionCommentData.explanation = md.render(questionCommentData.explanation);
104+
105+
if(this.sendResponseAsEvent) {
106+
const event = new CustomEvent('ai-generate-question-response', {
107+
detail: {
108+
response: questionCommentData
109+
},
110+
bubbles: true,
111+
composed: true
112+
});
113+
this.dispatchEvent(event);
114+
} else {
115+
const qAndAView = new QuestionAnswerView({ questionCommentData });
116+
questionContainer.prepend(qAndAView);
117+
}
118+
}
119+
}
120+
}
121+
122+
window.customElements.define('st-ai-generated-question', AIGeneratedQuestion);
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
class AIPromptInput extends HTMLElement {
2+
constructor(playbackEngine, sendResponseAsEvent=false, sinceLastCommentPrompt = 'Describe how the code has changed.', allTimePrompt = 'Describe this code.') {
3+
super();
4+
5+
this.playbackEngine = playbackEngine;
6+
this.sendResponseAsEvent = sendResponseAsEvent;
7+
this.sinceLastCommentPrompt = sinceLastCommentPrompt;
8+
this.allTimePrompt = allTimePrompt;
9+
10+
this.attachShadow({ mode: 'open' });
11+
this.shadowRoot.innerHTML = `
12+
<style>
13+
:host {
14+
}
15+
16+
hr {
17+
border: none;
18+
border-top: 1px solid darkgray;
19+
}
20+
21+
#submitChatButton {
22+
background-color: lightgrey;
23+
color: black;
24+
border: 1px solid lightgrey;
25+
border-radius: .25rem;
26+
padding: 5px;
27+
margin: 5px;
28+
width: 70%;
29+
display: block;
30+
margin-left: auto;
31+
margin-right: auto;
32+
33+
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
34+
}
35+
36+
#inputText {
37+
min-height: 100px;
38+
padding: 5px;
39+
color: lightgrey;
40+
outline: none;
41+
border: 1px solid grey;
42+
overflow-y: scroll;
43+
word-wrap: break-word;
44+
scrollbar-width: thin;
45+
resize: vertical;
46+
}
47+
48+
.questionText {
49+
font-style: italic;
50+
}
51+
52+
.responseText {
53+
margin: 10px;
54+
}
55+
</style>
56+
<div>
57+
<input type="checkbox" id="sinceLastCommentOnly" name="sinceLastCommentOnly" checked>
58+
<label for="sinceLastCommentOnly">Since the last comment only</label>
59+
<div id="inputText" contenteditable="true">${this.sinceLastCommentPrompt}</div>
60+
<button id="submitChatButton">Submit Your Question</button>
61+
<div class="aiResponse"></div>
62+
</div>
63+
`;
64+
}
65+
66+
connectedCallback() {
67+
const submitChatButton = this.shadowRoot.querySelector('#submitChatButton');
68+
submitChatButton.addEventListener('click', this.submitText);
69+
70+
const checkbox = this.shadowRoot.querySelector('#sinceLastCommentOnly');
71+
checkbox.addEventListener('change', this.toggleLastComment);
72+
73+
const inputText = this.shadowRoot.querySelector('#inputText');
74+
inputText.addEventListener('keydown', this.ignoreKeyboardControls);
75+
}
76+
77+
disconnectedCallback() {
78+
const submitChatButton = this.shadowRoot.querySelector('#submitChatButton');
79+
submitChatButton.removeEventListener('click', this.submitText);
80+
81+
const checkbox = this.shadowRoot.querySelector('#sinceLastCommentOnly');
82+
checkbox.removeEventListener('change', this.toggleLastComment);
83+
84+
const inputText = this.shadowRoot.querySelector('#inputText');
85+
inputText.removeEventListener('keydown', this.ignoreKeyboardControls);
86+
}
87+
88+
toggleLastComment = (event) => {
89+
const checkbox = this.shadowRoot.querySelector('#sinceLastCommentOnly');
90+
const inputText = this.shadowRoot.querySelector('#inputText');
91+
if (checkbox.checked) {
92+
//if other default is set or the box is empty
93+
if(inputText.textContent.trim() === this.allTimePrompt || inputText.textContent.trim() === "") {
94+
//set the default prompt
95+
inputText.textContent = this.sinceLastCommentPrompt;
96+
} //else- user has something typed in, keep it
97+
} else {
98+
//if other default is set or the box is empty
99+
if(inputText.textContent.trim() === this.sinceLastCommentPrompt || inputText.textContent.trim() === "") {
100+
//set the default prompt
101+
inputText.textContent = this.allTimePrompt;
102+
} //else- user has something typed in, keep it
103+
}
104+
}
105+
106+
ignoreKeyboardControls = (event) => {
107+
//consume keyboad events while typing the question
108+
event.stopImmediatePropagation();
109+
}
110+
111+
submitText = async () => {
112+
const inputText = this.shadowRoot.querySelector('#inputText');
113+
const sinceLastCommentCheckbox = this.shadowRoot.querySelector('#sinceLastCommentOnly');
114+
const submitChatButton = this.shadowRoot.querySelector('#submitChatButton');
115+
116+
let codeFromPlayback = this.playbackEngine.getMostRecentFileEdits(sinceLastCommentCheckbox.checked);
117+
118+
//add prompt from user
119+
const promptWithCode = `${codeFromPlayback}\n\nBriefly respond to this prompt.\n\n${inputText.innerText}`;
120+
121+
let promptObject = {
122+
requestType: "Ask",
123+
prompt: promptWithCode,
124+
playbackViewId: document.body.dataset.playbackViewId ? document.body.dataset.playbackViewId : null
125+
};
126+
127+
submitChatButton.textContent = 'Generating response...';
128+
//make the submitChatButton disabled
129+
submitChatButton.setAttribute('disabled', 'true');
130+
inputText.setAttribute('contenteditable', 'false');
131+
sinceLastCommentCheckbox.setAttribute('disabled', 'true');
132+
133+
//send the formatted one to the server
134+
const serverProxy = new ServerProxy();
135+
const responseObject = await serverProxy.sendAIPromptToServer(promptObject);
136+
137+
submitChatButton.textContent = 'Submit Another Question';
138+
submitChatButton.removeAttribute('disabled');
139+
inputText.setAttribute('contenteditable', 'true');
140+
inputText.focus();
141+
sinceLastCommentCheckbox.removeAttribute('disabled');
142+
143+
if(responseObject.error) {
144+
const aiResponse = this.shadowRoot.querySelector('.aiResponse');
145+
aiResponse.textContent = responseObject.response;
146+
} else {
147+
const md = markdownit();
148+
149+
if(this.sendResponseAsEvent) {
150+
const event = new CustomEvent('ai-prompt-response', {
151+
detail: {
152+
prompt: inputText.innerText,
153+
response: md.render(responseObject.response)
154+
},
155+
bubbles: true,
156+
composed: true
157+
});
158+
this.dispatchEvent(event);
159+
} else {
160+
const answeredQuestion = document.createElement('div');
161+
answeredQuestion.appendChild(document.createElement('hr'));
162+
163+
const questionElement = document.createElement('div');
164+
questionElement.classList.add('questionText');
165+
questionElement.textContent = inputText.innerText;
166+
answeredQuestion.appendChild(questionElement);
167+
168+
const responseElement = document.createElement('div');
169+
responseElement.classList.add('responseText');
170+
responseElement.innerHTML = md.render(responseObject.response);
171+
answeredQuestion.appendChild(responseElement);
172+
173+
const aiResponse = this.shadowRoot.querySelector('.aiResponse');
174+
aiResponse.prepend(answeredQuestion);
175+
}
176+
}
177+
}
178+
}
179+
180+
window.customElements.define('st-ai-prompt-input', AIPromptInput);

0 commit comments

Comments
 (0)