Skip to content
Draft
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
Expand Up @@ -132,6 +132,18 @@ export const AssignmentBuilder = () => {
}
};

const handlePeerAsyncChange = async (assignment: Assignment, peer_async_visible: boolean) => {
try {
await updateAssignment({
...assignment,
peer_async_visible
});
toast.success(`Async peer ${peer_async_visible ? "enabled" : "disabled"}`);
} catch (error) {
toast.error("Failed to update async peer setting");
}
};

const handleVisibilityChange = async (
assignment: Assignment,
data: { visible: boolean; visible_on: string | null; hidden_on: string | null }
Expand Down Expand Up @@ -199,6 +211,7 @@ export const AssignmentBuilder = () => {
onDuplicate={handleDuplicate}
onReleasedChange={handleReleasedChange}
onEnforceDueChange={handleEnforceDueChange}
onPeerAsyncChange={handlePeerAsyncChange}
onVisibilityChange={handleVisibilityChange}
onRemove={onRemove}
/>
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 @@ -2,6 +2,7 @@ import { useCallback, useState } from "react";

import { SearchInput } from "@components/ui/SearchInput";
import { Button } from "primereact/button";
import { Checkbox } from "primereact/checkbox";
import { Column } from "primereact/column";
import { confirmDialog, ConfirmDialog } from "primereact/confirmdialog";
import { DataTable, DataTableSortEvent } from "primereact/datatable";
Expand All @@ -25,6 +26,7 @@ interface AssignmentListProps {
onDuplicate: (assignment: Assignment) => void;
onReleasedChange: (assignment: Assignment, released: boolean) => void;
onEnforceDueChange: (assignment: Assignment, enforce_due: boolean) => void;
onPeerAsyncChange: (assignment: Assignment, peer_async_visible: boolean) => void;
onVisibilityChange: (
assignment: Assignment,
data: { visible: boolean; visible_on: string | null; hidden_on: string | null }
Expand All @@ -41,6 +43,7 @@ export const AssignmentList = ({
onDuplicate,
onReleasedChange,
onEnforceDueChange,
onPeerAsyncChange,
onVisibilityChange,
onRemove
}: AssignmentListProps) => {
Expand Down Expand Up @@ -120,6 +123,17 @@ export const AssignmentList = ({
>
{rowData.kind || "Unknown"}
</span>
{rowData.kind === "Peer" && (
<label className={styles.asyncPeerGroup}>
<span className={styles.asyncPeerText}>Async Peer</span>
<Checkbox
checked={rowData.peer_async_visible}
onChange={(e) => onPeerAsyncChange(rowData, !!e.checked)}
tooltip={rowData.peer_async_visible ? "Disable async peer" : "Enable async peer"}
tooltipOptions={{ position: "top" }}
/>
</label>
)}
</div>
);

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 @@ -742,7 +742,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"),
)
Loading