Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,34 @@
}

.typeColumn {
width: 120px;
width: 160px;
}

.typeCell {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 0.5rem;
}

.asyncPeerGroup {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 0.2rem;
padding-top: 0.25rem;
cursor: pointer;
user-select: none;
}

.asyncPeerText {
font-size: 0.7rem;
color: var(--surface-500);
white-space: nowrap;
}

.asyncPeerGroup:hover .asyncPeerText {
color: var(--surface-700);
}

.typeTag {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { EditableCellFactory } from "@components/ui/EditableTable/EditableCellFactory";
import { TableSelectionOverlay } from "@components/ui/EditableTable/TableOverlay";
import { ExerciseTypeTag } from "@components/ui/ExerciseTypeTag";
import { useReorderAssignmentExercisesMutation } from "@store/assignmentExercise/assignmentExercise.logic.api";
import { useToastContext } from "@components/ui/ToastContext";
import {
useHasApiKeyQuery,
useReorderAssignmentExercisesMutation,
useUpdateAssignmentQuestionsMutation
} from "@store/assignmentExercise/assignmentExercise.logic.api";
import { Button } from "primereact/button";
import { Column } from "primereact/column";
import { DataTable, DataTableSelectionMultipleChangeEvent } from "primereact/datatable";
import { Dropdown } from "primereact/dropdown";
import { OverlayPanel } from "primereact/overlaypanel";
import { Tooltip } from "primereact/tooltip";
import { useRef, useState } from "react";

import { useExercisesSelector } from "@/hooks/useExercisesSelector";

import { difficultyOptions } from "@/config/exerciseTypes";
import { useJwtUser } from "@/hooks/useJwtUser";
import { useSelectedAssignment } from "@/hooks/useSelectedAssignment";
import { DraggingExerciseColumns } from "@/types/components/editableTableCell";
import { Exercise, supportedExerciseTypesToEdit } from "@/types/exercises";

Expand All @@ -19,6 +30,68 @@ import { ExercisePreviewModal } from "../components/ExercisePreview/ExercisePrev

import { SetCurrentEditExercise, ViewModeSetter, MouseUpHandler } from "./types";

const AsyncModeHeader = ({ hasApiKey }: { hasApiKey: boolean }) => {
const { showToast } = useToastContext();
const [updateExercises] = useUpdateAssignmentQuestionsMutation();
const { assignmentExercises = [] } = useExercisesSelector();
const overlayRef = useRef<OverlayPanel>(null);
const [value, setValue] = useState("Standard");

const handleSubmit = async () => {
const exercises = assignmentExercises.map((ex) => ({
...ex,
question_json: JSON.stringify(ex.question_json),
use_llm: value === "LLM"
}));
const { error } = await updateExercises(exercises);
if (!error) {
overlayRef.current?.hide();
showToast({ severity: "success", summary: "Success", detail: "Exercises updated successfully" });
} else {
showToast({ severity: "error", summary: "Error", detail: "Failed to update exercises" });
}
};

return (
<div className="flex align-items-center gap-2">
<span>Async Mode</span>
<Button
className="icon-button-sm"
tooltip='Edit "Async Mode" for all exercises'
rounded
text
severity="secondary"
size="small"
icon="pi pi-pencil"
onClick={(e) => overlayRef.current?.toggle(e)}
/>
<OverlayPanel closeIcon ref={overlayRef} style={{ width: "17rem" }}>
<div className="p-1 flex gap-2 flex-column align-items-center justify-content-around">
<div><span>Edit "Async Mode" for all exercises</span></div>
<div style={{ width: "100%" }}>
<Dropdown
style={{ width: "100%" }}
value={value}
onChange={(e) => setValue(e.value)}
options={[
{ label: "Standard", value: "Standard" },
{ label: "LLM", value: "LLM", disabled: !hasApiKey }
]}
optionLabel="label"
optionDisabled="disabled"
scrollHeight="auto"
/>
</div>
<div className="flex flex-row justify-content-around align-items-center w-full">
<Button size="small" severity="danger" onClick={() => overlayRef.current?.hide()}>Cancel</Button>
<Button size="small" onClick={handleSubmit}>Submit</Button>
</div>
</div>
</OverlayPanel>
</div>
);
};

interface AssignmentExercisesTableProps {
assignmentExercises: Exercise[];
selectedExercises: Exercise[];
Expand Down Expand Up @@ -48,6 +121,11 @@ export const AssignmentExercisesTable = ({
}: AssignmentExercisesTableProps) => {
const { username } = useJwtUser();
const [reorderExercises] = useReorderAssignmentExercisesMutation();
const [updateAssignmentQuestions] = useUpdateAssignmentQuestionsMutation();
const { selectedAssignment } = useSelectedAssignment();
const { data: hasApiKey = false } = useHasApiKeyQuery();
const isPeerAsync =
selectedAssignment?.kind === "Peer" && selectedAssignment?.peer_async_visible === true;
const dataTableRef = useRef<DataTable<Exercise[]>>(null);
const [copyModalVisible, setCopyModalVisible] = useState(false);
const [selectedExerciseForCopy, setSelectedExerciseForCopy] = useState<Exercise | null>(null);
Expand Down Expand Up @@ -276,6 +354,32 @@ export const AssignmentExercisesTable = ({
/>
)}
/>
{isPeerAsync && (
<Column
resizeable={false}
style={{ width: "12rem" }}
header={() => <AsyncModeHeader hasApiKey={hasApiKey} />}
bodyStyle={{ padding: 0 }}
body={(data: Exercise) => (
<div className="editable-table-cell" style={{ position: "relative" }}>
<Dropdown
className="editable-table-dropdown"
value={data.use_llm && hasApiKey ? "LLM" : "Standard"}
onChange={(e) => updateAssignmentQuestions([{ ...data, use_llm: e.value === "LLM" }])}
options={[
{ label: "Standard", value: "Standard" },
{ label: "LLM", value: "LLM", disabled: !hasApiKey }
]}
optionLabel="label"
optionDisabled="disabled"
scrollHeight="auto"
tooltip={!hasApiKey ? "Add an API key to enable LLM mode" : undefined}
tooltipOptions={{ showOnDisabled: true }}
/>
</div>
)}
/>
)}
<Column resizeable={false} rowReorder style={{ width: "3rem" }} />
</DataTable>
<TableSelectionOverlay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ export const assignmentExerciseApi = createApi({
body
})
}),
hasApiKey: build.query<boolean, void>({
query: () => ({
method: "GET",
url: "/assignment/instructor/has_api_key"
}),
transformResponse: (response: DetailResponse<{ has_api_key: boolean }>) =>
response.detail.has_api_key
}),
copyQuestion: build.mutation<
DetailResponse<{ status: string; question_id: number; message: string }>,
{
Expand Down Expand Up @@ -218,5 +226,6 @@ export const {
useReorderAssignmentExercisesMutation,
useUpdateAssignmentExercisesMutation,
useValidateQuestionNameMutation,
useCopyQuestionMutation
useCopyQuestionMutation,
useHasApiKeyQuery
} = assignmentExerciseApi;
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type Exercise = {
reading_assignment: boolean;
sorting_priority: number;
activities_required: number;
use_llm: boolean;
qnumber: string;
name: string;
subchapter: string;
Expand Down
11 changes: 11 additions & 0 deletions bases/rsptx/assignment_server_api/routers/instructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,17 @@ async def add_api_token(
)


@router.get("/has_api_key")
@instructor_role_required()
@with_course()
async def has_api_key(request: Request, user=Depends(auth_manager), course=None):
"""Return whether the course has at least one API token configured."""
tokens = await fetch_all_api_tokens(course.id)
return make_json_response(
status=status.HTTP_200_OK, detail={"has_api_key": len(tokens) > 0}
)


@router.get("/add_token")
@instructor_role_required()
@with_course()
Expand Down
3 changes: 1 addition & 2 deletions bases/rsptx/book_server_api/routers/assessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,7 @@ async def getpollresults(request: Request, course: str, div_id: str):
my_vote = int(user_res.split(":")[0])
my_comment = user_res.split(":")[1]
else:
if user_res.isnumeric():
my_vote = int(user_res)
my_vote = int(user_res) if user_res.isnumeric() else -1
my_comment = ""
else:
my_vote = -1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,25 @@ def dashboard():
is_last=done,
lti=is_lti,
has_vote1=has_vote1,
peer_async_visible=assignment.peer_async_visible or False,
**course_attrs,
)


@auth.requires(
lambda: verifyInstructorStatus(auth.user.course_id, auth.user),
requires_login=True,
)
def toggle_async():
response.headers["content-type"] = "application/json"
assignment_id = request.vars.assignment_id
assignment = db(db.assignments.id == assignment_id).select().first()
new_value = not (assignment.peer_async_visible or False)
db(db.assignments.id == assignment_id).update(peer_async_visible=new_value)
db.commit()
return json.dumps({"peer_async_visible": new_value})


def extra():
assignment_id = request.vars.assignment_id
current_question, done, idx = _get_current_question(assignment_id, False)
Expand Down Expand Up @@ -174,7 +189,9 @@ def _get_current_question(assignment_id, get_next):
idx = 0
db(db.assignments.id == assignment_id).update(current_index=idx)
elif get_next is True:
idx = assignment.current_index + 1
all_questions = _get_assignment_questions(assignment_id)
total_questions = len(all_questions)
idx = min(assignment.current_index + 1, max(total_questions - 1, 0))
db(db.assignments.id == assignment_id).update(current_index=idx)
else:
idx = assignment.current_index
Expand Down Expand Up @@ -743,7 +760,14 @@ def peer_async():
if "latex_macros" not in course_attrs:
course_attrs["latex_macros"] = ""

llm_enabled = _llm_enabled()
aq = None
if current_question:
aq = db(
(db.assignment_questions.assignment_id == assignment_id)
& (db.assignment_questions.question_id == current_question.id)
).select().first()
question_use_llm = bool(aq.use_llm) if aq else False
llm_enabled = _llm_enabled() and question_use_llm
try:
db.useinfo.insert(
course_id=auth.user.course_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@
Field(
"activities_required", type="integer"
), # specifies how many activities in a sub chapter a student must perform in order to receive credit
Field("use_llm", type="boolean", default=False),
migrate=bookserver_owned("assignment_questions"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ <h3>Question {{ =current_qnum }} of {{ =num_questions }}</h3>
</div>

<div id="pi-assignment-navigation">
{{ if current_qnum < num_questions: }}
<button
type="submit"
id="nextq"
Expand All @@ -70,6 +71,19 @@ <h3>Question {{ =current_qnum }} of {{ =num_questions }}</h3>
>
Next Question
</button>
{{ else: }}
<div id="asyncBtnArea" style="display:inline-block;">
<button
type="button"
id="toggleAsyncBtn"
class="btn btn-info" {{ if peer_async_visible: }}style="background-color:#a3d4ec; border-color:#a3d4ec; color:#fff;"{{ pass }}
onclick="showAsyncConfirm()"
style="margin-right: 4px;"
>
{{ if peer_async_visible: }}Undo After-Class Release{{ else: }}Release After-Class PI{{ pass }}
</button>
</div>
{{ pass }}
<button
type="submit"
id="restart"
Expand Down Expand Up @@ -349,8 +363,33 @@ <h3>Question {{ =current_qnum }} of {{ =num_questions }}</h3>
var mess_count = 0;
var answerCount = 0;
var done = {{=is_last }}
if (done) {
document.getElementById("nextq").disabled = true;

var asyncReleased = {{=peer_async_visible}};

function showAsyncConfirm() {
var area = document.getElementById("asyncBtnArea");
var msg = asyncReleased
? "Undo the after-class PI release?"
: "Release after-class PI questions to students?";
area.innerHTML = `
<span style="margin-right:6px; font-weight:bold;">${msg}</span>
<button type="button" class="btn btn-sm btn-default" onclick="confirmToggleAsync()" style="margin-right:4px;">Yes</button>
<button type="button" class="btn btn-sm btn-default" onclick="cancelAsyncConfirm()">Cancel</button>
`;
}

function cancelAsyncConfirm() {
var area = document.getElementById("asyncBtnArea");
var label = asyncReleased ? "Undo After-Class Release" : "Release After-Class PI";
var extraStyle = asyncReleased ? 'style="background-color:#a3d4ec; border-color:#a3d4ec; color:#fff; margin-right:4px;"' : 'style="margin-right:4px;"';
area.innerHTML = `<button type="button" id="toggleAsyncBtn" class="btn btn-info" onclick="showAsyncConfirm()" ${extraStyle}>${label}</button>`;
}

async function confirmToggleAsync() {
var resp = await fetch("/runestone/peer/toggle_async?assignment_id={{=assignment_id}}", { method: "POST" });
var data = await resp.json();
asyncReleased = data.peer_async_visible;
cancelAsyncConfirm();
}
</script>
{{ end }}
Loading
Loading