diff --git a/bases/rsptx/interactives/runestone/mchoice/js/mchoice.js b/bases/rsptx/interactives/runestone/mchoice/js/mchoice.js index 1da216b2d..83bb21d88 100644 --- a/bases/rsptx/interactives/runestone/mchoice/js/mchoice.js +++ b/bases/rsptx/interactives/runestone/mchoice/js/mchoice.js @@ -417,7 +417,7 @@ export default class MultipleChoice extends RunestoneBase { studentVoteCount > 1) { this.renderMCMAFeedBack(); } else { - $(this.feedBackDiv).html("

Your Answer has been recorded

"); + $(this.feedBackDiv).html("

Your answer has been recorded

"); $(this.feedBackDiv).attr("class", "alert alert-info"); } } @@ -572,7 +572,7 @@ export default class MultipleChoice extends RunestoneBase { studentVoteCount > 1) { this.renderMCMAFeedBack(); } else { - $(this.feedBackDiv).html("

Your Answer has been recorded

"); + $(this.feedBackDiv).html("

Your answer has been recorded

"); $(this.feedBackDiv).attr("class", "alert alert-info"); } } diff --git a/bases/rsptx/web2py_server/applications/runestone/controllers/peer.py b/bases/rsptx/web2py_server/applications/runestone/controllers/peer.py index d298360cf..7e5de7980 100644 --- a/bases/rsptx/web2py_server/applications/runestone/controllers/peer.py +++ b/bases/rsptx/web2py_server/applications/runestone/controllers/peer.py @@ -711,6 +711,7 @@ def peer_async(): qnum = int(request.vars.question_num) current_question, all_done = _get_numbered_question(assignment_id, qnum) + assignment = db(db.assignments.id == assignment_id).select().first() course = db(db.courses.course_name == auth.user.course_name).select().first() course_attrs = getCourseAttributesDict(course.id, course.base_course) if "latex_macros" not in course_attrs: @@ -721,6 +722,7 @@ def peer_async(): course=get_course_row(db.courses.ALL), current_question=current_question, assignment_id=assignment_id, + assignment_name = assignment.name, nextQnum=qnum + 1, all_done=all_done, **course_attrs, diff --git a/bases/rsptx/web2py_server/applications/runestone/static/css/peer.css b/bases/rsptx/web2py_server/applications/runestone/static/css/peer.css index 47ffd5e14..e67543374 100644 --- a/bases/rsptx/web2py_server/applications/runestone/static/css/peer.css +++ b/bases/rsptx/web2py_server/applications/runestone/static/css/peer.css @@ -1,5 +1,11 @@ -#pi-instructor-interface .row, #pi-instructor-interface #imessage { +/* +--------------------------------+ */ +/* | Peer Instruction (PI) Stylesheet | */ +/* +--------------------------------+ */ +/* ---------------------- */ +/* Instructor's interface */ +/* ---------------------- */ +#pi-instructor-interface .row, #pi-instructor-interface #imessage { margin-right: 0px; margin-left: 0px; padding-left: 15px; @@ -158,4 +164,172 @@ .autopermalink { display: none; +} + +/* ------------------- */ +/* Student's interface */ +/* ------------------- */ +#pi-student-interface .row, #pi-student-interface #imessage { + margin-right: 0px; + margin-left: 0px; + padding-right: 15px; +} + +#discussion_panel { + margin: 20px 0; + background: #FBFBFB; + border: 0.5px solid #C7CDD1; + border-radius: 12px; + box-shadow: 0px 20px 24px 0px rgba(17, 17, 17, 0.06); + padding-left: 0px; + padding-right: 0px; +} + +#discussion-panel-heading { + font-size: 22px; + font-weight: 500; + padding: 12px 24px; + background: #FBFBFB; + border: 1px solid #C7CDD1; + border-radius: 12px 12px 0px 0px; + box-shadow: 0px 2px 2px 0px rgba(17, 17, 17, 0.06); +} + +#discussion-panel-heading p { + margin: 0; +} + +#discussion-panel-peer-votes { + border-bottom: 1px solid #C7CDD1; + padding: 16px 24px; +} + +#discussion-panel-peer-votes-content { + border-radius: 12px; + border: 2px solid #C7CDD1; + background: #FFFFFF; + padding: 16px 32px; +} + +#discussion-panel-peer-votes-content p { + margin: 0; + line-height: 28px; +} + +#discussion-panel-peer-votes-content #peerlist { + margin-bottom: 8px; +} + +#discussion-panel-messages { + padding: 16px 24px; +} + +#discussion-panel-messages #messages { + margin: 0; + padding-left: 0; + padding-right: 10px; + min-height: 15vh; + max-height: 35vh; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +#discussion-panel-messages #messages > li { + margin-top: 8px; +} + +#discussion-panel-messages #messages .outgoing-mess + .incoming-mess, +#discussion-panel-messages #messages .incoming-mess + .outgoing-mess { + margin-top: 16px; +} + +#discussion-panel-messages #messages > li:first-child { + margin-top: 0; +} + +#discussion-panel-messages #messages .outgoing-mess, #discussion-panel-messages #messages .incoming-mess { + display: flex; + flex-direction: column; + font-style: normal; + color: #333; + gap: 4px; +} + +#discussion-panel-messages #messages .outgoing-mess { + align-items: end; +} + +#discussion-panel-messages #messages .incoming-mess { + align-items: start; +} + +#discussion-panel-messages #messages .sender { + display: flex; + font-size: 11px; + gap: 8px; + align-items: center; +} + +#discussion-panel-messages #messages .sender-initials { + display: flex; + background: #777777; + height: 24px; + width: 24px; + border-radius: 50%; + align-items: center; + justify-content: center; + color: #FFFFFF; +} + +#discussion-panel-messages #messages .sender-name { + font-weight: 700; +} + +#discussion-panel-messages #messages .content { + border-radius: 15px; + padding: 8px 16px; +} + +#discussion-panel-messages #messages .outgoing-mess .sender-initials { + order: 2; +} + +#discussion-panel-messages #messages .outgoing-mess .content { + background: rgba(215, 241, 249, 0.60); +} + +#discussion-panel-messages #messages .incoming-mess .content { + background: rgba(255, 255, 215, 0.60); +} + +#peer-message-box { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + border-radius: 12px; + border: 2px solid #C7CDD1; + background: #FFFFFF; + padding: 8px 16px; + margin-top: 10px; + gap: 16px; +} + +#peer-message-box input { + width: 100%; + border: none; +} + +#sendpeermsg { + background: none; + margin: 0; + padding: 0; + display: flex; +} + +#sendpeermsg.disabled { + pointer-events: none; + opacity: 0.5; } \ No newline at end of file diff --git a/bases/rsptx/web2py_server/applications/runestone/static/js/peer.js b/bases/rsptx/web2py_server/applications/runestone/static/js/peer.js index e98f9443d..d748b9daf 100644 --- a/bases/rsptx/web2py_server/applications/runestone/static/js/peer.js +++ b/bases/rsptx/web2py_server/applications/runestone/static/js/peer.js @@ -1,4 +1,4 @@ -// Configuration for the PI steps and helper functions to handle step progression +// Configuration for the PI steps and helper functions to handle step progression in the instructor's interface const STEP_CONFIG = { vote1: { next: ['makep', 'facechat', 'makeabgroups'], @@ -107,6 +107,42 @@ function handleButtonClick(event) { } } +// Function to render incoming and outgoing messages for the text chat +function renderMessage({ from, text, direction }) { + // Create message element + const message = document.createElement("li"); + message.classList.add(`${direction}-mess`); + + // Sender container + const sender = document.createElement("div"); + sender.classList.add("sender"); + + // Thumbnail using sender initials + const senderInitials = document.createElement("div"); + senderInitials.classList.add("sender-initials"); + let initials = from.split(" ").map(n => n.charAt(0)).join("").toUpperCase(); + senderInitials.textContent = initials; + + // Sender name + const senderName = document.createElement("div"); + senderName.classList.add("sender-name"); + senderName.textContent = direction === "outgoing" ? "You" : from; + + sender.appendChild(senderInitials); + sender.appendChild(senderName); + + // Message content + const content = document.createElement("div"); + content.classList.add("content"); + content.textContent = text; + + // Append sender and content to message + message.appendChild(sender); + message.appendChild(content); + + return message; +} + var ws = null; var alertSet = false; function connect(event) { @@ -119,15 +155,13 @@ function connect(event) { ws.onclose = function () { console.log("Websocket Closed") alert( - "You have been disconnected from the peer instruction server. Will Reconnect." + "You have been disconnected from the peer instruction server. Will reconnect." ); connect(); }; ws.onmessage = function (event) { - var messages = document.getElementById("messages"); - var message = document.createElement("li"); - message.classList.add("incoming-mess"); + const messages = document.getElementById("messages"); let mess = JSON.parse(event.data); // This is an easy to code solution for broadcasting that could go out to // multiple courses. It would be better to catch that on the server side @@ -138,9 +172,15 @@ function connect(event) { } if (mess.type === "text") { if (!(mess.time in messageTrail)) { - var content = document.createTextNode(`${mess.from}: ${mess.message}`); - message.appendChild(content); + let message = renderMessage({ + from: mess.from, + text: mess.message, + direction: "incoming" + }); + + // Append message to messages container messages.appendChild(message); + messages.scrollTop = messages.scrollHeight; messageTrail[mess.time] = mess.message; } } else if (mess.type === "control") { @@ -275,7 +315,7 @@ function connect(event) { for (const key in adict) { let currAnswer = adict[key]; let newpeer = document.createElement("p"); - newpeer.innerHTML = `${key} answered ${currAnswer}`; + newpeer.innerHTML = `${key}: ${currAnswer}`; peerlist.appendChild(newpeer); } break; @@ -396,31 +436,43 @@ async function sendLtiScores(event) { // the server can then broadcast the message or send it to a // specific user async function sendMessage(event) { - var input = document.getElementById("messageText"); - if (input.value.trim() === "") { + const messages = document.getElementById("messages"); + const input = document.getElementById("messageText"); + const sendButton = document.getElementById("sendpeermsg"); + const messageText = input.value.trim(); + + if (messageText === "") { input.focus(); return; } + let mess = { type: "text", from: `${user}`, - message: input.value, + message: messageText, time: Date.now(), broadcast: false, course_name: eBookConfig.course, div_id: currentQuestion, }; + await publishMessage(mess); - var messages = document.getElementById("messages"); - var message = document.createElement("li"); - message.classList.add("outgoing-mess"); - var content = document.createTextNode(`${user}: ${input.value}`); - message.appendChild(content); + + let message = renderMessage({ + from: user, + text: messageText, + direction: "outgoing" + }); + + // Append message to messages container messages.appendChild(message); + messages.scrollTop = messages.scrollHeight; + input.value = ""; - // focus tehe input box again input.focus(); - // not needed for onclick event.preventDefault() + + // Disable the send button after sending a message + sendButton.classList.add("disabled"); } function warnAndStopVote(event) { @@ -731,11 +783,25 @@ async function setupPeerGroup() { $(function () { let tinput = document.getElementById("messageText"); - if (tinput) { - tinput.addEventListener("keyup", function (event) { - if (event.keyCode === 13) { + let sendButton = document.getElementById("sendpeermsg"); + + if (tinput && sendButton) { + tinput.addEventListener("input", function () { + let message = this.value.trim(); + if (message !== "") { + sendButton.classList.remove("disabled"); + } else { + sendButton.classList.add("disabled"); + } + }); + + tinput.addEventListener("keydown", function (event) { + if (event.key === "Enter") { event.preventDefault(); - document.getElementById("sendpeermsg").click(); + let message = this.value.trim(); + if (message != "") { + document.getElementById("sendpeermsg").click(); + } } }); } diff --git a/bases/rsptx/web2py_server/applications/runestone/views/peer/dashboard.html b/bases/rsptx/web2py_server/applications/runestone/views/peer/dashboard.html index a12624c5d..dea920732 100644 --- a/bases/rsptx/web2py_server/applications/runestone/views/peer/dashboard.html +++ b/bases/rsptx/web2py_server/applications/runestone/views/peer/dashboard.html @@ -6,10 +6,12 @@ rel="stylesheet" href="{{=URL('static','css/peer.css')}}?v={{=request.peer_mtime}}" /> + + @@ -124,7 +126,7 @@

Question {{ =current_qnum }} of {{ =num_questions }}

onclick="enableFaceChat(event)" disabled > - Enable in-person Chat + Enable In-Person Chat OR {{ pass }} diff --git a/bases/rsptx/web2py_server/applications/runestone/views/peer/peer_question.html b/bases/rsptx/web2py_server/applications/runestone/views/peer/peer_question.html index a7eb2b871..e25ee8c38 100644 --- a/bases/rsptx/web2py_server/applications/runestone/views/peer/peer_question.html +++ b/bases/rsptx/web2py_server/applications/runestone/views/peer/peer_question.html @@ -4,6 +4,11 @@ {{include '_sphinx_static_files.html'}} {{end}} + +