Skip to content

Commit 073619d

Browse files
committed
lock student flow order
1 parent 126fef3 commit 073619d

2 files changed

Lines changed: 76 additions & 125 deletions

File tree

bases/rsptx/web2py_server/applications/runestone/controllers/peer.py

Lines changed: 8 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import random
1818
import re
1919
import datetime
20-
20+
import requests
2121

2222
# Third Party
2323
# -----------
@@ -848,28 +848,27 @@ def get_async_llm_reflection():
848848
"you are a student talking to another student during peer instruction.\n"
849849
"you are both looking at the same multiple choice question with code and answers.\n"
850850
"you remember the code and choices.\n"
851-
"most messages should be short (1 to 3 sentences, often very short).\n"
851+
"most messages should be short (1 to 3 sentences often very short).\n"
852852
"use casual informal language and common typos.\n"
853-
"sometimes use words.\n"
854853
"never use commas.\n"
855854
"never use gendered language.\n"
856855
"do not use new lines.\n"
857856
"do not sound like a teacher.\n"
858857
"do not explain step by step.\n"
859858
"do not mention libraries formally.\n"
860859
"never say something is right or wrong.\n"
861-
"never mention a choice letter as the correct answer (like 'c is right' or 'probably b').\n"
860+
"never mention a choice letter as the correct answer.\n"
862861
"never clearly describe the final result of the code.\n"
863862
"never fully state what the program prints.\n"
864863
"keep reasoning partial or uncertain.\n"
865-
"if the other student clearly states confidence or repeats the same answer twice, stop debating and directly tell them to vote again.\n"
866-
"do not continue reasoning after telling them to vote again.\n"
867864
"use common misconceptions and focus on only one part of the code.\n"
868-
"refer to code loosely (like 'that line', 'the loop', 'the head', 'the print').\n"
865+
"refer to code loosely like 'that line' or 'the loop' or 'the head' or 'the print'.\n"
869866
"often hedge with uncertainty.\n"
870867
"never agree with the other student's interpretation even if it sounds correct.\n"
868+
"use content from the other multiple choice options in your reponses when needed\n"
871869
"maintain a mistaken or incomplete mental model throughout the conversation.\n"
872-
"if the other student sounds confident or if they are not changing their perspective, tell them to vote again.\n"
870+
"if the other student clearly sounds confident or repeats the same answer twice stop debating and tell them to vote again or submit it.\n"
871+
"do not continue reasoning after telling them to vote again.\n"
873872
"focus on reasoning not teaching.\n\n"
874873
)
875874

@@ -1011,22 +1010,6 @@ def send_lti_scores():
10111010

10121011

10131012

1014-
1015-
1016-
import os, json
1017-
import requests
1018-
1019-
# --- helper to sanitize long, messy text we include in prompts ---
1020-
def clean_text(s):
1021-
try:
1022-
s = s or ""
1023-
s = str(s)
1024-
# collapse whitespace per-line, avoid huge payloads
1025-
s = "\n".join(line.strip() for line in s.splitlines())
1026-
return s[:2000]
1027-
except Exception:
1028-
return ""
1029-
10301013
def _get_umgpt_settings():
10311014
api_key = os.environ.get("UMGPT_API_KEY", "").strip()
10321015
base_url = os.environ.get("UMGPT_BASE_URL", "").strip()
@@ -1066,76 +1049,4 @@ def _call_openai(messages):
10661049
)
10671050
resp.raise_for_status()
10681051
data = resp.json()
1069-
return data["choices"][0]["message"]["content"].strip()
1070-
1071-
def get_gpt_response():
1072-
"""
1073-
1074-
GET ?message=... -> echo mode
1075-
POST JSON {"messages":[...]} -> calls UMGPT if UMGPT_* config is set, else stub
1076-
"""
1077-
if request.env.request_method == "GET":
1078-
msg = request.vars.message or ""
1079-
return response.json(dict(ok=True, echo=msg, reply="(echo) " + msg, tokens_used=0))
1080-
1081-
try:
1082-
raw = request.body.read().decode("utf-8")
1083-
except Exception:
1084-
raw = "{}"
1085-
1086-
try:
1087-
payload = json.loads(raw or "{}")
1088-
except Exception:
1089-
payload = {}
1090-
1091-
messages = payload.get("messages", [])
1092-
context = payload.get("context") or {}
1093-
1094-
messages_with_context = messages
1095-
try:
1096-
if isinstance(context, dict) and context.get("ok") is True:
1097-
course = context.get("course")
1098-
basecourse = context.get("basecourse")
1099-
username = context.get("username")
1100-
comp_id = context.get("id")
1101-
comp_type = context.get("type")
1102-
prompt_txt = clean_text(context.get("prompt"))
1103-
code_txt = clean_text(context.get("code"))
1104-
choices = context.get("choices") or []
1105-
selected = context.get("selected")
1106-
out_txt = clean_text(context.get("output"))
1107-
err_txt = clean_text(context.get("error"))
1108-
coach_txt = clean_text(context.get("coach"))
1109-
1110-
sys_ctx = (
1111-
"You are a friendly peer in a Runestone ebook helping with the CURRENT exercise. "
1112-
"Be concise, guide reasoning, and avoid giving full solutions. If the user asks 'what is this asking me to do', "
1113-
"summarize the task in plain language, then suggest first steps.\n\n"
1114-
f"Course: {course} | Basecourse: {basecourse} | User: {username}\n"
1115-
f"Component: {comp_type} | ID: {comp_id}\n"
1116-
f"Prompt:\n{prompt_txt}\n\n"
1117-
+ (f"Starter/Current Code:\n{code_txt}\n\n" if code_txt else "")
1118-
+ ("Choices:\n- " + "\n- ".join(map(str, choices)) + (f"\n\nSelected: {selected}" if selected else "") + "\n\n" if choices else "")
1119-
+ (f"Last Run Output:\n{out_txt}\n\n" if out_txt else "")
1120-
+ (f"Last Run Error:\n{err_txt}\n\n" if err_txt else "")
1121-
+ (f"Coach/Guidance:\n{coach_txt}\n\n" if coach_txt else "")
1122-
+ "Rules: Never reveal solutions verbatim. Encourage peer-instruction style hints."
1123-
)
1124-
1125-
messages_with_context = [{"role": "system", "content": sys_ctx}] + messages
1126-
except Exception:
1127-
messages_with_context = messages
1128-
if not isinstance(messages, list) or not messages:
1129-
return response.json(dict(ok=False, error="messages[] required"))
1130-
1131-
try:
1132-
reply = _call_openai(messages_with_context)
1133-
if reply is None:
1134-
user_last = next((m.get("content","") for m in reversed(messages) if m.get("role")=="user"), "")
1135-
reply = f"(stub) Here is how to think about it: {user_last}"
1136-
return response.json(dict(ok=True, reply=reply, tokens_used=0))
1137-
except requests.HTTPError as e:
1138-
return response.json(dict(ok=False, error=f"HTTP {e.response.status_code}: {e.response.text[:200]}"))
1139-
except Exception as e:
1140-
return response.json(dict(ok=False, error=str(e)[:200]))
1141-
1052+
return data["choices"][0]["message"]["content"].strip()

bases/rsptx/web2py_server/applications/runestone/views/peer/peer_async.html

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
eBookConfig.host = window.location.origin;
2222
eBookConfig.isLoggedIn = true;
2323
</script> -->
24-
24+
<!--
2525
{{ if has_vote1: }}
2626
<p><em>Vote 1 detected</em></p>
2727
{{ else: }}
2828
<p><em>No vote yet</em></p>
29-
{{ pass }}
29+
{{ pass }} -->
3030
<div id="peer_async_root" data-div_id="{{=current_question.name}}">
3131
</div>
3232

@@ -141,32 +141,61 @@ <h3>Congratulations, you have completed this assignment!</h3>
141141
<script src="/runestone/static/js/peer.js?v={{=request.peer_mtime}}"></script>
142142
<script>
143143
window._vote2Enabled = false;
144+
window._vote1Locked = false;
145+
146+
function setVoteInputsLocked(locked) {
147+
try {
148+
const comp = window.componentMap[currentQuestion];
149+
if (comp && comp.submitButton) {
150+
comp.submitButton.disabled = locked;
151+
}
152+
if (comp && typeof comp.disableInteraction === "function" && locked) {
153+
comp.disableInteraction();
154+
}
155+
if (comp && typeof comp.enableInteraction === "function" && !locked) {
156+
comp.enableInteraction();
157+
}
158+
} catch (e) {
159+
console.warn("vote lock toggle failed", e);
160+
}
161+
}
162+
163+
function lockVote1AndReflection() {
164+
window._vote1Locked = true;
165+
setVoteInputsLocked(true);
166+
const ta = document.getElementById("messageText");
167+
const btn = document.getElementById("submitReflection");
168+
if (ta) ta.disabled = true;
169+
if (btn) btn.disabled = true;
170+
}
171+
172+
function setReflectionPanelEnabled(enabled) {
173+
const panel = document.getElementById("reflectionPanel");
174+
if (!panel) return;
175+
panel.style.opacity = enabled ? "1" : "0.5";
176+
panel.style.pointerEvents = enabled ? "auto" : "none";
177+
if (enabled) {
178+
const notice = document.getElementById("vote1Notice");
179+
if (notice) notice.remove();
180+
}
181+
}
144182

145183
function enableSecondVoteAsync() {
146184
console.log("PI async: enabling vote 2");
147-
if (!window._vote2Enabled && studentVoteCount < 1) {
185+
if (!hasVote1) {
148186
alert("Please submit your first vote before voting again.");
149187
return;
150188
}
151189

152190
window._vote2Enabled = true;
191+
window._vote1Locked = false;
153192

154193
studentSubmittedVote2 = false;
155194
if (typeof studentVoteCount !== "undefined" && studentVoteCount < 2) {
156195
studentVoteCount = 2;
157196
}
158197

159-
try {
160-
const comp = window.componentMap[currentQuestion];
161-
comp.submitButton.disabled = false;
162-
163-
const root = document.getElementById(currentQuestion);
164-
root?.querySelectorAll("input, button").forEach(el => {
165-
el.disabled = false;
166-
});
167-
} catch (e) {
168-
console.warn("vote 2 enable fallback failed", e);
169-
}
198+
setVoteInputsLocked(false);
170199

171200
const b = document.getElementById("readyVote2Btn");
172201
if (b) b.style.display = "none";
@@ -205,10 +234,13 @@ <h3>Congratulations, you have completed this assignment!</h3>
205234
const nextStepEl = document.getElementById("nextStep");
206235

207236
btn.addEventListener("click", async function () {
208-
if (studentVoteCount < 1) {
237+
if (!hasVote1) {
209238
alert("Please submit your first vote before starting the discussion.");
210239
return;
211240
}
241+
if (window._vote1Locked) {
242+
return;
243+
}
212244

213245
const reflection = (ta.value || "").trim();
214246
if (!reflection) {
@@ -258,6 +290,7 @@ <h3>Congratulations, you have completed this assignment!</h3>
258290
const discussion = document.getElementById("llmDiscussion");
259291
const chat = document.getElementById("llmChat");
260292
const nextStep = document.getElementById("nextStep");
293+
lockVote1AndReflection();
261294

262295
discussion.style.display = "block";
263296
chat.innerHTML = "<p><em>Thinking about your explanation…</em></p>";
@@ -434,7 +467,8 @@ <h3>Congratulations, you have completed this assignment!</h3>
434467
setTimeout(connect, 1000);
435468
});
436469
var studentVoteCount = 1;
437-
var hasVote1 = false;
470+
var hasVote1 = {{='true' if has_vote1 else 'false'}};
471+
var hasReflection = {{='true' if has_reflection else 'false'}};
438472
var studentSubmittedVote2 = false;
439473
var vote2done = false;
440474
var llmUserMessageCount = 0;
@@ -469,18 +503,31 @@ <h3>Congratulations, you have completed this assignment!</h3>
469503
// this cannot happen until the event that indicates components are loaded
470504
$(document).on("runestone:login-complete", function () {
471505
llmUserMessageCount = 0;
506+
if (hasVote1 && !hasReflection) {
507+
setReflectionPanelEnabled(true);
508+
}
472509
logPeerEvent({
473510
sid: eBookConfig.username,
474511
div_id: `${assignId}`,
475512
event: "peer",
476513
act: "start_async",
477514
course_name: eBookConfig.course,
478515
})
479-
setTimeout(function () {
516+
let bindAttempts = 0;
517+
const bindVoteHandler = function () {
480518
const btn = window.componentMap[currentQuestion]?.submitButton;
481-
if (!btn) return;
519+
if (!btn) {
520+
bindAttempts += 1;
521+
if (bindAttempts < 40) {
522+
setTimeout(bindVoteHandler, 100);
523+
}
524+
return;
525+
}
482526

483527
$(btn).off("click.pi_async").on("click.pi_async", function () {
528+
if (window._vote1Locked && !window._vote2Enabled) {
529+
return;
530+
}
484531
const currAnswer = window.componentMap[currentQuestion].answer;
485532
const sAnswer = answerToString(currAnswer);
486533
$("#current_answer").html(sAnswer);
@@ -519,15 +566,7 @@ <h3>Congratulations, you have completed this assignment!</h3>
519566
vote2Btn.title = "Send at least one LLM message to unlock";
520567
}
521568
window._hasVote1 = true;
522-
523-
const panel = document.getElementById("reflectionPanel");
524-
if (panel) {
525-
panel.style.opacity = "1";
526-
panel.style.pointerEvents = "auto";
527-
}
528-
529-
const notice = document.getElementById("vote1Notice");
530-
if (notice) notice.remove();
569+
setReflectionPanelEnabled(true);
531570

532571
return;
533572
}
@@ -549,7 +588,8 @@ <h3>Congratulations, you have completed this assignment!</h3>
549588
}
550589
}
551590
});
552-
}, 2000);
591+
};
592+
bindVoteHandler();
553593
});
554594
</script>
555595
<script>

0 commit comments

Comments
 (0)