feat(practices): DYNAMIC questions with schema-driven payload

- Add practiceDynamicQuestionPayload helper to build stimulus/response slots
- Extend PracticeQuestionEditorFields with DYNAMIC type, definition picker, and per-slot values (JSON-capable)
- Wire AddNewPracticePage and AddNewLessonPage createQuestion to send question_type_definition_id and dynamic_payload
- Use lesson/practice save status (DRAFT/PUBLISHED) for created questions instead of always PUBLISHED

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-13 05:11:48 -07:00
parent 9b35a8bf30
commit 77b71abfd8
4 changed files with 402 additions and 18 deletions

View File

@ -9,7 +9,13 @@ import { Check, Image as ImageIcon, Mic, Plus, Upload, X } from "lucide-react"
import { toast } from "sonner"
import { resolveMediaPreviewUrl } from "../../lib/practiceMedia"
import { uploadAudioFile, uploadImageFile } from "../../api/files.api"
import {
getQuestionTypeDefinitionById,
getQuestionTypeDefinitions,
} from "../../api/questionTypeDefinitions.api"
import type { QuestionTypeDefinition } from "../../types/questionTypeDefinition.types"
import { Input } from "../ui/input"
import { Textarea } from "../ui/textarea"
import { Select } from "../ui/select"
import { Button } from "../ui/button"
import { SpinnerIcon } from "../ui/spinner-icon"
@ -17,7 +23,7 @@ import { cn } from "../../lib/utils"
import { ResolvedAudio } from "../media/ResolvedAudio"
import { ResolvedImage } from "../media/ResolvedImage"
export type PracticeQuestionEditorType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO"
export type PracticeQuestionEditorType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO" | "DYNAMIC"
export type PracticeQuestionEditorDifficulty = "EASY" | "MEDIUM" | "HARD"
export interface PracticeQuestionOptionDraft {
@ -32,6 +38,13 @@ const ALLOWED_AUDIO_EXTENSIONS = new Set(["mp3", "wav", "ogg", "m4a", "aac", "we
const MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024
const ALLOWED_IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "webp", "gif"])
export interface PracticeQuestionDynamicRow {
id: string
kind: string
label?: string
required?: boolean
}
export interface PracticeQuestionEditorValue {
questionText: string
questionType: PracticeQuestionEditorType
@ -46,6 +59,12 @@ export interface PracticeQuestionEditorValue {
shortAnswer: string
/** Stored URL or object key; same semantics as Speaking practice editor */
imageUrl: string
/** When `questionType` is DYNAMIC — definition used to shape `dynamic_payload` */
questionTypeDefinitionId: number | null
dynamicStimulusRows: PracticeQuestionDynamicRow[]
dynamicResponseRows: PracticeQuestionDynamicRow[]
/** Keys `stimulus:${elementId}` and `response:${elementId}` (ids from the type definition schema) */
dynamicFieldValues: Record<string, string>
}
export function createEmptyPracticeQuestionDraft(): PracticeQuestionEditorValue {
@ -67,6 +86,10 @@ export function createEmptyPracticeQuestionDraft(): PracticeQuestionEditorValue
audioCorrectAnswerText: "",
shortAnswer: "",
imageUrl: "",
questionTypeDefinitionId: null,
dynamicStimulusRows: [],
dynamicResponseRows: [],
dynamicFieldValues: {},
}
}
@ -84,6 +107,9 @@ function defaultOptionsForType(
previousType: PracticeQuestionEditorType,
current: PracticeQuestionOptionDraft[],
): PracticeQuestionOptionDraft[] {
if (type === "DYNAMIC") {
return current
}
if (type === "TRUE_FALSE") {
if (previousType === "TRUE_FALSE" && current.length >= 2) {
return current.map((o, i) => ({
@ -146,6 +172,18 @@ export function PracticeQuestionEditorFields({
const setType = (questionType: PracticeQuestionEditorType) => {
const options = defaultOptionsForType(questionType, value.questionType, value.options)
if (questionType === "DYNAMIC" || value.questionType === "DYNAMIC") {
onChange({
...value,
questionType,
options,
questionTypeDefinitionId: null,
dynamicStimulusRows: [],
dynamicResponseRows: [],
dynamicFieldValues: {},
})
return
}
onChange({ ...value, questionType, options })
}
@ -586,6 +624,92 @@ export function PracticeQuestionEditorFields({
const controlsDisabled = mediaBusy
const [typeDefinitions, setTypeDefinitions] = useState<QuestionTypeDefinition[]>([])
const [definitionsLoading, setDefinitionsLoading] = useState(false)
const [definitionDetailLoading, setDefinitionDetailLoading] = useState(false)
useEffect(() => {
if (value.questionType !== "DYNAMIC") return
let cancelled = false
setDefinitionsLoading(true)
;(async () => {
try {
const rows = await getQuestionTypeDefinitions({ include_system: true })
if (!cancelled) setTypeDefinitions(Array.isArray(rows) ? rows : [])
} catch {
if (!cancelled) setTypeDefinitions([])
} finally {
if (!cancelled) setDefinitionsLoading(false)
}
})()
return () => {
cancelled = true
}
}, [value.questionType])
const handleDynamicDefinitionChange = async (rawId: string) => {
if (!rawId) {
onChange({
...value,
questionTypeDefinitionId: null,
dynamicStimulusRows: [],
dynamicResponseRows: [],
dynamicFieldValues: {},
})
return
}
const id = Number(rawId)
if (!Number.isFinite(id) || id <= 0) return
setDefinitionDetailLoading(true)
try {
const def = await getQuestionTypeDefinitionById(id)
if (!def) {
toast.error("Definition not found")
return
}
const fieldValues: Record<string, string> = { ...value.dynamicFieldValues }
const dynamicStimulusRows: PracticeQuestionDynamicRow[] = def.stimulus_schema.map((r) => ({
id: r.id,
kind: r.kind,
label: r.label,
required: r.required,
}))
const dynamicResponseRows: PracticeQuestionDynamicRow[] = def.response_schema.map((r) => ({
id: r.id,
kind: r.kind,
label: r.label,
required: r.required,
}))
for (const r of dynamicStimulusRows) {
const k = `stimulus:${r.id}`
if (fieldValues[k] === undefined) fieldValues[k] = ""
}
for (const r of dynamicResponseRows) {
const k = `response:${r.id}`
if (fieldValues[k] === undefined) fieldValues[k] = ""
}
onChange({
...value,
questionTypeDefinitionId: id,
dynamicStimulusRows,
dynamicResponseRows,
dynamicFieldValues: fieldValues,
})
} catch (e) {
console.error(e)
toast.error("Failed to load definition details")
} finally {
setDefinitionDetailLoading(false)
}
}
const setDynamicField = (key: string, next: string) => {
onChange({
...value,
dynamicFieldValues: { ...value.dynamicFieldValues, [key]: next },
})
}
return (
<>
<div className="mt-5 space-y-5">
@ -615,6 +739,7 @@ export function PracticeQuestionEditorFields({
<option value="TRUE_FALSE">True/False</option>
<option value="SHORT">Short Answer</option>
<option value="AUDIO">Audio</option>
<option value="DYNAMIC">Dynamic (schema-driven)</option>
</Select>
</div>
<div className="space-y-2">
@ -644,6 +769,89 @@ export function PracticeQuestionEditorFields({
</div>
</div>
{value.questionType === "DYNAMIC" && (
<div className="space-y-5 rounded-xl border border-violet-200 bg-violet-50/50 p-4 sm:p-5">
<p className="text-sm leading-relaxed text-grayScale-600">
Pick a question type definition, then fill each stimulus/response slot. Element{" "}
<code className="rounded bg-white px-1 text-xs">id</code> and{" "}
<code className="rounded bg-white px-1 text-xs">kind</code> must match the definition schema. Use JSON
for object values (e.g. <code className="text-xs">{"{\"placeholder\":\"Type here\"}"}</code>).
</p>
<div className="space-y-2">
<label className="text-xs font-medium uppercase tracking-wider text-grayScale-500">
Question type definition <span className="text-red-500">*</span>
</label>
<Select
value={value.questionTypeDefinitionId != null ? String(value.questionTypeDefinitionId) : ""}
onChange={(e) => void handleDynamicDefinitionChange(e.target.value)}
disabled={definitionsLoading || definitionDetailLoading}
>
<option value="">{definitionsLoading ? "Loading definitions…" : "Select definition…"}</option>
{typeDefinitions.map((d) => (
<option key={d.id} value={String(d.id)}>
#{d.id} {d.display_name} ({d.key})
</option>
))}
</Select>
</div>
{definitionDetailLoading ? (
<p className="text-sm font-medium text-grayScale-500">Loading schema</p>
) : null}
{value.dynamicStimulusRows.length > 0 ? (
<div className="space-y-3">
<p className="text-xs font-bold uppercase tracking-wide text-violet-800">Stimulus</p>
{value.dynamicStimulusRows.map((row) => (
<div
key={`stimulus-${row.id}`}
className="space-y-2 rounded-lg border border-grayScale-200 bg-white p-3 shadow-sm"
>
<div className="flex flex-wrap items-baseline justify-between gap-2">
<span className="text-sm font-semibold text-grayScale-900">{row.label || row.id}</span>
<span className="text-[11px] font-mono text-grayScale-500">
{row.id} · {row.kind}
{row.required ? <span className="text-red-500"> *</span> : null}
</span>
</div>
<Textarea
rows={3}
value={value.dynamicFieldValues[`stimulus:${row.id}`] ?? ""}
onChange={(e) => setDynamicField(`stimulus:${row.id}`, e.target.value)}
placeholder="URL, plain text, or JSON object"
className="min-h-[72px] resize-y font-mono text-[13px]"
/>
</div>
))}
</div>
) : null}
{value.dynamicResponseRows.length > 0 ? (
<div className="space-y-3">
<p className="text-xs font-bold uppercase tracking-wide text-violet-800">Response</p>
{value.dynamicResponseRows.map((row) => (
<div
key={`response-${row.id}`}
className="space-y-2 rounded-lg border border-grayScale-200 bg-white p-3 shadow-sm"
>
<div className="flex flex-wrap items-baseline justify-between gap-2">
<span className="text-sm font-semibold text-grayScale-900">{row.label || row.id}</span>
<span className="text-[11px] font-mono text-grayScale-500">
{row.id} · {row.kind}
{row.required ? <span className="text-red-500"> *</span> : null}
</span>
</div>
<Textarea
rows={3}
value={value.dynamicFieldValues[`response:${row.id}`] ?? ""}
onChange={(e) => setDynamicField(`response:${row.id}`, e.target.value)}
placeholder="URL, plain text, or JSON object"
className="min-h-[72px] resize-y font-mono text-[13px]"
/>
</div>
))}
</div>
) : null}
</div>
)}
{value.questionType === "MCQ" && (
<div className="space-y-3 rounded-lg bg-grayScale-50/50 p-4">
<label className="text-xs font-medium uppercase tracking-wider text-grayScale-500">Options</label>
@ -777,6 +985,8 @@ export function PracticeQuestionEditorFields({
</div>
</div>
{value.questionType !== "DYNAMIC" ? (
<>
<div className="space-y-2">
<label className="text-xs font-medium uppercase tracking-wider text-grayScale-500">Voice Prompt (Optional)</label>
<div className="flex flex-col gap-2">
@ -908,6 +1118,8 @@ export function PracticeQuestionEditorFields({
) : null}
</div>
</div>
</>
) : null}
</div>
{recordingModal ? (

View File

@ -0,0 +1,34 @@
import type { DynamicQuestionPayload } from "../types/questionTypeDefinition.types"
/** Parse a single slot value: plain string/URL, or JSON object/array when input looks like JSON. */
export function parseDynamicSlotValue(raw: string | undefined): unknown {
const t = (raw ?? "").trim()
if (!t) return ""
if ((t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]"))) {
try {
return JSON.parse(t) as unknown
} catch {
return t
}
}
return t
}
export function buildDynamicQuestionPayload(input: {
stimulusRows: { id: string; kind: string }[]
responseRows: { id: string; kind: string }[]
fieldValues: Record<string, string>
}): DynamicQuestionPayload {
return {
stimulus: input.stimulusRows.map((row) => ({
id: row.id,
kind: row.kind,
value: parseDynamicSlotValue(input.fieldValues[`stimulus:${row.id}`]),
})),
response: input.responseRows.map((row) => ({
id: row.id,
kind: row.kind,
value: parseDynamicSlotValue(input.fieldValues[`response:${row.id}`]),
})),
}
}

View File

@ -5,6 +5,8 @@ import { toast } from "sonner"
import { addQuestionToSet, createLesson, createQuestion } from "../../api/courses.api"
import { uploadVideoFile } from "../../api/files.api"
import { PracticeQuestionEditorFields } from "../../components/content-management/PracticeQuestionEditorFields"
import type { PracticeQuestionDynamicRow } from "../../components/content-management/PracticeQuestionEditorFields"
import { buildDynamicQuestionPayload } from "../../lib/practiceDynamicQuestionPayload"
import { Button } from "../../components/ui/button"
import { Card } from "../../components/ui/card"
import { Input } from "../../components/ui/input"
@ -12,7 +14,7 @@ import { SpinnerIcon } from "../../components/ui/spinner-icon"
import type { QuestionOption } from "../../types/course.types"
type Step = 1 | 2 | 3 | 4
type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO"
type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO" | "DYNAMIC"
type DifficultyLevel = "EASY" | "MEDIUM" | "HARD"
type ResultStatus = "success" | "error"
@ -35,6 +37,10 @@ interface Question {
audioCorrectAnswerText: string
shortAnswers: string[]
imageUrl: string
questionTypeDefinitionId: number | null
dynamicStimulusRows: PracticeQuestionDynamicRow[]
dynamicResponseRows: PracticeQuestionDynamicRow[]
dynamicFieldValues: Record<string, string>
}
const STEPS = [
@ -63,6 +69,10 @@ function createEmptyQuestion(id: string): Question {
audioCorrectAnswerText: "",
shortAnswers: [],
imageUrl: "",
questionTypeDefinitionId: null,
dynamicStimulusRows: [],
dynamicResponseRows: [],
dynamicFieldValues: {},
}
}
@ -104,6 +114,7 @@ function questionTypeLabel(type: QuestionType): string {
if (type === "TRUE_FALSE") return "True/False"
if (type === "SHORT") return "Short Answer"
if (type === "AUDIO") return "Audio"
if (type === "DYNAMIC") return "Dynamic"
return "Multiple Choice"
}
@ -224,6 +235,39 @@ export function AddNewLessonPage() {
for (let i = 0; i < questions.length; i++) {
const q = questions[i]
if (!q.questionText.trim()) continue
if (q.questionType === "DYNAMIC") {
if (q.questionTypeDefinitionId == null || q.questionTypeDefinitionId <= 0) {
toast.error(`Question ${i + 1}: select a question type definition for dynamic questions.`)
setSaving(false)
return
}
const missingStimulus = q.dynamicStimulusRows.find(
(row) =>
row.required &&
!(q.dynamicFieldValues[`stimulus:${row.id}`]?.trim()),
)
if (missingStimulus) {
toast.error(
`Question ${i + 1}: fill required stimulus "${missingStimulus.label || missingStimulus.id}".`,
)
setSaving(false)
return
}
const missingResponse = q.dynamicResponseRows.find(
(row) =>
row.required &&
!(q.dynamicFieldValues[`response:${row.id}`]?.trim()),
)
if (missingResponse) {
toast.error(
`Question ${i + 1}: fill required response "${missingResponse.label || missingResponse.id}".`,
)
setSaving(false)
return
}
}
const options: QuestionOption[] =
q.questionType === "MCQ"
? q.options.map((opt, idx) => ({
@ -233,6 +277,15 @@ export function AddNewLessonPage() {
}))
: []
const dynamicPayload =
q.questionType === "DYNAMIC" && q.questionTypeDefinitionId != null
? buildDynamicQuestionPayload({
stimulusRows: q.dynamicStimulusRows,
responseRows: q.dynamicResponseRows,
fieldValues: q.dynamicFieldValues,
})
: undefined
const qRes = await createQuestion({
question_text: q.questionText,
question_type: q.questionType,
@ -240,13 +293,22 @@ export function AddNewLessonPage() {
points: q.points,
tips: q.tips || undefined,
explanation: q.explanation || undefined,
status: "PUBLISHED",
status,
options: options.length > 0 ? options : undefined,
voice_prompt: q.voicePrompt || undefined,
sample_answer_voice_prompt: q.sampleAnswerVoicePrompt || undefined,
audio_correct_answer_text: q.audioCorrectAnswerText || undefined,
image_url: q.imageUrl.trim() || undefined,
short_answers: q.shortAnswers.length > 0 ? q.shortAnswers : undefined,
voice_prompt: q.questionType === "DYNAMIC" ? undefined : q.voicePrompt || undefined,
sample_answer_voice_prompt:
q.questionType === "DYNAMIC" ? undefined : q.sampleAnswerVoicePrompt || undefined,
audio_correct_answer_text:
q.questionType === "DYNAMIC" ? undefined : q.audioCorrectAnswerText || undefined,
image_url: q.questionType === "DYNAMIC" ? undefined : q.imageUrl.trim() || undefined,
short_answers:
q.questionType !== "DYNAMIC" && q.shortAnswers.length > 0 ? q.shortAnswers : undefined,
...(q.questionType === "DYNAMIC" && q.questionTypeDefinitionId != null && dynamicPayload
? {
question_type_definition_id: q.questionTypeDefinitionId,
dynamic_payload: dynamicPayload,
}
: {}),
})
const questionId = qRes.data?.data?.id
if (questionId) {
@ -457,6 +519,10 @@ export function AddNewLessonPage() {
audioCorrectAnswerText: question.audioCorrectAnswerText,
shortAnswer: question.shortAnswers[0] ?? "",
imageUrl: question.imageUrl,
questionTypeDefinitionId: question.questionTypeDefinitionId,
dynamicStimulusRows: question.dynamicStimulusRows,
dynamicResponseRows: question.dynamicResponseRows,
dynamicFieldValues: question.dynamicFieldValues,
}}
onChange={(next) =>
updateQuestion(question.id, {
@ -472,6 +538,10 @@ export function AddNewLessonPage() {
audioCorrectAnswerText: next.audioCorrectAnswerText,
shortAnswers: next.shortAnswer.trim() ? [next.shortAnswer.trim()] : [],
imageUrl: next.imageUrl,
questionTypeDefinitionId: next.questionTypeDefinitionId,
dynamicStimulusRows: next.dynamicStimulusRows,
dynamicResponseRows: next.dynamicResponseRows,
dynamicFieldValues: next.dynamicFieldValues,
})
}
mediaBusy={saving}

View File

@ -25,12 +25,13 @@ import {
addQuestionToSet,
} from "../../api/courses.api";
import { uploadVideoFile } from "../../api/files.api";
import { Select } from "../../components/ui/select";
import type { QuestionOption } from "../../types/course.types";
import type { PracticeQuestionDynamicRow } from "../../components/content-management/PracticeQuestionEditorFields";
import { buildDynamicQuestionPayload } from "../../lib/practiceDynamicQuestionPayload";
type Step = 1 | 2 | 3 | 4 | 5;
type ResultStatus = "success" | "error";
type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO";
type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO" | "DYNAMIC";
type DifficultyLevel = "EASY" | "MEDIUM" | "HARD";
interface Persona {
@ -58,6 +59,10 @@ interface Question {
audioCorrectAnswerText: string;
shortAnswers: string[];
imageUrl: string;
questionTypeDefinitionId: number | null;
dynamicStimulusRows: PracticeQuestionDynamicRow[];
dynamicResponseRows: PracticeQuestionDynamicRow[];
dynamicFieldValues: Record<string, string>;
}
const PERSONAS: Persona[] = [
@ -231,6 +236,10 @@ function createEmptyQuestion(id: string): Question {
audioCorrectAnswerText: "",
shortAnswers: [],
imageUrl: "",
questionTypeDefinitionId: null,
dynamicStimulusRows: [],
dynamicResponseRows: [],
dynamicFieldValues: {},
};
}
@ -414,6 +423,38 @@ export function AddNewPracticePage() {
const q = questions[i];
if (!q.questionText.trim()) continue;
if (q.questionType === "DYNAMIC") {
if (q.questionTypeDefinitionId == null || q.questionTypeDefinitionId <= 0) {
toast.error(`Question ${i + 1}: select a question type definition for dynamic questions.`);
setSaving(false);
return;
}
const missingStimulus = q.dynamicStimulusRows.find(
(row) =>
row.required &&
!(q.dynamicFieldValues[`stimulus:${row.id}`]?.trim()),
);
if (missingStimulus) {
toast.error(
`Question ${i + 1}: fill required stimulus "${missingStimulus.label || missingStimulus.id}".`,
);
setSaving(false);
return;
}
const missingResponse = q.dynamicResponseRows.find(
(row) =>
row.required &&
!(q.dynamicFieldValues[`response:${row.id}`]?.trim()),
);
if (missingResponse) {
toast.error(
`Question ${i + 1}: fill required response "${missingResponse.label || missingResponse.id}".`,
);
setSaving(false);
return;
}
}
const options: QuestionOption[] =
q.questionType === "MCQ"
? q.options.map((opt, idx) => ({
@ -423,6 +464,15 @@ export function AddNewPracticePage() {
}))
: [];
const dynamicPayload =
q.questionType === "DYNAMIC" && q.questionTypeDefinitionId != null
? buildDynamicQuestionPayload({
stimulusRows: q.dynamicStimulusRows,
responseRows: q.dynamicResponseRows,
fieldValues: q.dynamicFieldValues,
})
: undefined;
const qRes = await createQuestion({
question_text: q.questionText,
question_type: q.questionType,
@ -430,14 +480,22 @@ export function AddNewPracticePage() {
points: q.points,
tips: q.tips || undefined,
explanation: q.explanation || undefined,
status: "PUBLISHED",
status,
options: options.length > 0 ? options : undefined,
voice_prompt: q.voicePrompt || undefined,
sample_answer_voice_prompt: q.sampleAnswerVoicePrompt || undefined,
audio_correct_answer_text: q.audioCorrectAnswerText || undefined,
image_url: q.imageUrl.trim() || undefined,
voice_prompt: q.questionType === "DYNAMIC" ? undefined : q.voicePrompt || undefined,
sample_answer_voice_prompt:
q.questionType === "DYNAMIC" ? undefined : q.sampleAnswerVoicePrompt || undefined,
audio_correct_answer_text:
q.questionType === "DYNAMIC" ? undefined : q.audioCorrectAnswerText || undefined,
image_url: q.questionType === "DYNAMIC" ? undefined : q.imageUrl.trim() || undefined,
short_answers:
q.shortAnswers.length > 0 ? q.shortAnswers : undefined,
q.questionType !== "DYNAMIC" && q.shortAnswers.length > 0 ? q.shortAnswers : undefined,
...(q.questionType === "DYNAMIC" && q.questionTypeDefinitionId != null && dynamicPayload
? {
question_type_definition_id: q.questionTypeDefinitionId,
dynamic_payload: dynamicPayload,
}
: {}),
});
const questionId = qRes.data?.data?.id;
@ -912,7 +970,7 @@ export function AddNewPracticePage() {
Step 3: Questions
</h2>
<p className="mt-1.5 max-w-3xl text-sm leading-relaxed text-grayScale-500">
Add MCQ, True/False, Short Answer, or Audio items. Use the full
Add MCQ, True/False, Short Answer, Audio, or Dynamic (schema-driven) items. Use the full
width for stems and options.
</p>
</div>
@ -952,6 +1010,10 @@ export function AddNewPracticePage() {
audioCorrectAnswerText: question.audioCorrectAnswerText,
shortAnswer: question.shortAnswers[0] ?? "",
imageUrl: question.imageUrl,
questionTypeDefinitionId: question.questionTypeDefinitionId,
dynamicStimulusRows: question.dynamicStimulusRows,
dynamicResponseRows: question.dynamicResponseRows,
dynamicFieldValues: question.dynamicFieldValues,
}}
onChange={(next) => {
updateQuestion(question.id, {
@ -970,6 +1032,10 @@ export function AddNewPracticePage() {
? [next.shortAnswer.trim()]
: [],
imageUrl: next.imageUrl,
questionTypeDefinitionId: next.questionTypeDefinitionId,
dynamicStimulusRows: next.dynamicStimulusRows,
dynamicResponseRows: next.dynamicResponseRows,
dynamicFieldValues: next.dynamicFieldValues,
});
}}
mediaBusy={saving}
@ -1180,7 +1246,9 @@ export function AddNewPracticePage() {
? "True/False"
: question.questionType === "AUDIO"
? "Audio"
: "Short Answer"}
: question.questionType === "DYNAMIC"
? "Dynamic"
: "Short Answer"}
</span>
<span className="rounded-md bg-purple-50 px-2 py-0.5 text-xs font-medium text-purple-600">
{question.difficultyLevel}