Align Human Language edit-question UI with create flow
Defer opening the dialog until details load; improve API mapping for true/false and types; add audio answer field; show edit loading on the row action. Made-with: Cursor
This commit is contained in:
parent
53d16d9f93
commit
cd7d330261
|
|
@ -227,6 +227,8 @@ export function HumanLanguagePage() {
|
||||||
const [savingQuestion, setSavingQuestion] = useState(false)
|
const [savingQuestion, setSavingQuestion] = useState(false)
|
||||||
const [deletingPractice, setDeletingPractice] = useState(false)
|
const [deletingPractice, setDeletingPractice] = useState(false)
|
||||||
const [deletingQuestion, setDeletingQuestion] = useState(false)
|
const [deletingQuestion, setDeletingQuestion] = useState(false)
|
||||||
|
/** While fetching full question detail before opening the edit dialog (avoids empty form flash). */
|
||||||
|
const [loadingQuestionEditId, setLoadingQuestionEditId] = useState<number | null>(null)
|
||||||
/** Show inline field errors only after an explicit save attempt (avoids noisy empty-state on open). */
|
/** Show inline field errors only after an explicit save attempt (avoids noisy empty-state on open). */
|
||||||
const [practiceSubmitAttempted, setPracticeSubmitAttempted] = useState(false)
|
const [practiceSubmitAttempted, setPracticeSubmitAttempted] = useState(false)
|
||||||
const [questionSubmitAttempted, setQuestionSubmitAttempted] = useState(false)
|
const [questionSubmitAttempted, setQuestionSubmitAttempted] = useState(false)
|
||||||
|
|
@ -694,29 +696,44 @@ export function HumanLanguagePage() {
|
||||||
setQuestionFormTouched(false)
|
setQuestionFormTouched(false)
|
||||||
const qid = question.question_id ?? question.id
|
const qid = question.question_id ?? question.id
|
||||||
resetQuestionForm()
|
resetQuestionForm()
|
||||||
setQuestionDialog({ open: true, mode: "edit", practiceId, questionId: qid })
|
setLoadingQuestionEditId(qid)
|
||||||
try {
|
try {
|
||||||
const detail = questionDetailById[qid] ?? (await getQuestionById(qid)).data?.data
|
const detail = questionDetailById[qid] ?? (await getQuestionById(qid)).data?.data
|
||||||
if (!detail) return
|
if (!detail) {
|
||||||
|
toast.error("Could not load question details")
|
||||||
|
return
|
||||||
|
}
|
||||||
setQuestionDetailById((prev) => ({ ...prev, [qid]: detail }))
|
setQuestionDetailById((prev) => ({ ...prev, [qid]: detail }))
|
||||||
const options = (detail.options ?? []).slice().sort((a, b) => a.option_order - b.option_order)
|
const options = (detail.options ?? []).slice().sort((a, b) => a.option_order - b.option_order)
|
||||||
const correct = options.find((o) => o.is_correct)?.option_order ?? 1
|
const correctOpt = options.find((o) => o.is_correct)
|
||||||
|
const correctOrder = correctOpt?.option_order ?? 1
|
||||||
|
let correctOption: "A" | "B" | "C" | "D" = "A"
|
||||||
|
if (detail.question_type === "TRUE_FALSE") {
|
||||||
|
const t = (correctOpt?.option_text ?? "").trim().toLowerCase()
|
||||||
|
if (t === "false" || correctOrder === 2) correctOption = "B"
|
||||||
|
else correctOption = "A"
|
||||||
|
} else {
|
||||||
|
correctOption =
|
||||||
|
(["A", "B", "C", "D"][Math.min(Math.max(correctOrder - 1, 0), 3)] as "A" | "B" | "C" | "D") ?? "A"
|
||||||
|
}
|
||||||
const shortAnswer =
|
const shortAnswer =
|
||||||
Array.isArray(detail.short_answers) && detail.short_answers.length > 0
|
Array.isArray(detail.short_answers) && detail.short_answers.length > 0
|
||||||
? typeof detail.short_answers[0] === "string"
|
? typeof detail.short_answers[0] === "string"
|
||||||
? detail.short_answers[0]
|
? detail.short_answers[0]
|
||||||
: detail.short_answers[0]?.acceptable_answer ?? ""
|
: detail.short_answers[0]?.acceptable_answer ?? ""
|
||||||
: ""
|
: ""
|
||||||
|
const qt = detail.question_type
|
||||||
|
let questionType: "MCQ" | "TRUE_FALSE" | "SHORT" = "MCQ"
|
||||||
|
if (qt === "TRUE_FALSE") questionType = "TRUE_FALSE"
|
||||||
|
else if (qt === "SHORT" || qt === "SHORT_ANSWER" || qt === "AUDIO") questionType = "SHORT"
|
||||||
|
const difficultyRaw = detail.difficulty_level
|
||||||
|
const difficulty =
|
||||||
|
difficultyRaw === "EASY" || difficultyRaw === "MEDIUM" || difficultyRaw === "HARD" ? difficultyRaw : "EASY"
|
||||||
setQuestionForm({
|
setQuestionForm({
|
||||||
questionText: detail.question_text ?? "",
|
questionText: detail.question_text ?? "",
|
||||||
questionType:
|
questionType,
|
||||||
detail.question_type === "TRUE_FALSE" || detail.question_type === "SHORT" || detail.question_type === "SHORT_ANSWER"
|
difficulty,
|
||||||
? detail.question_type === "SHORT_ANSWER"
|
points: detail.points && detail.points > 0 ? detail.points : 1,
|
||||||
? "SHORT"
|
|
||||||
: detail.question_type
|
|
||||||
: "MCQ",
|
|
||||||
difficulty: detail.difficulty_level ?? "EASY",
|
|
||||||
points: detail.points ?? 1,
|
|
||||||
tips: detail.tips ?? "",
|
tips: detail.tips ?? "",
|
||||||
explanation: detail.explanation ?? "",
|
explanation: detail.explanation ?? "",
|
||||||
imageUrl: detail.image_url ?? "",
|
imageUrl: detail.image_url ?? "",
|
||||||
|
|
@ -727,12 +744,16 @@ export function HumanLanguagePage() {
|
||||||
optionB: options[1]?.option_text ?? "",
|
optionB: options[1]?.option_text ?? "",
|
||||||
optionC: options[2]?.option_text ?? "",
|
optionC: options[2]?.option_text ?? "",
|
||||||
optionD: options[3]?.option_text ?? "",
|
optionD: options[3]?.option_text ?? "",
|
||||||
correctOption: (["A", "B", "C", "D"][Math.min(Math.max(correct - 1, 0), 3)] as "A" | "B" | "C" | "D") ?? "A",
|
correctOption,
|
||||||
shortAnswer,
|
shortAnswer,
|
||||||
})
|
})
|
||||||
|
// Open only after the same form shape as create is fully populated (no empty-state flash).
|
||||||
|
setQuestionDialog({ open: true, mode: "edit", practiceId, questionId: qid })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load question detail:", error)
|
console.error("Failed to load question detail:", error)
|
||||||
toast.error("Could not load question details")
|
toast.error("Could not load question details")
|
||||||
|
} finally {
|
||||||
|
setLoadingQuestionEditId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1591,7 +1612,11 @@ export function HumanLanguagePage() {
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-7 px-2 text-[10px]"
|
className="h-7 gap-1 px-2 text-[10px]"
|
||||||
|
disabled={
|
||||||
|
loadingQuestionEditId ===
|
||||||
|
(q.question_id ?? q.id)
|
||||||
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void openEditQuestionDialog(
|
void openEditQuestionDialog(
|
||||||
selectedPracticeMeta.id,
|
selectedPracticeMeta.id,
|
||||||
|
|
@ -1599,6 +1624,10 @@ export function HumanLanguagePage() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{loadingQuestionEditId ===
|
||||||
|
(q.question_id ?? q.id) ? (
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" aria-hidden />
|
||||||
|
) : null}
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -1855,9 +1884,10 @@ export function HumanLanguagePage() {
|
||||||
>
|
>
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{questionDialog.open && questionDialog.mode === "edit" ? "Edit Question" : "Add Question"}</DialogTitle>
|
<DialogTitle>{questionDialog.open && questionDialog.mode === "edit" ? "Edit question" : "Add question"}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Create, edit, and attach questions to the selected practice.
|
Use the same fields as when creating: type, scoring, prompts, media URLs, and answer options. Changes apply to this
|
||||||
|
practice only.
|
||||||
{!questionCanSave ? (
|
{!questionCanSave ? (
|
||||||
<span className="mt-1 block text-amber-700/90">
|
<span className="mt-1 block text-amber-700/90">
|
||||||
Fix the highlighted fields before saving. Save stays disabled until the form is valid.
|
Fix the highlighted fields before saving. Save stays disabled until the form is valid.
|
||||||
|
|
@ -2086,6 +2116,18 @@ export function HumanLanguagePage() {
|
||||||
className="h-10 w-full rounded-md border border-grayScale-200 px-3 text-sm"
|
className="h-10 w-full rounded-md border border-grayScale-200 px-3 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1 sm:col-span-2">
|
||||||
|
<label className="text-xs font-medium text-grayScale-600">Audio / spoken correct answer text</label>
|
||||||
|
<textarea
|
||||||
|
value={questionForm.audioCorrectAnswerText}
|
||||||
|
onChange={(e) => {
|
||||||
|
setQuestionFormTouched(true)
|
||||||
|
setQuestionForm((f) => ({ ...f, audioCorrectAnswerText: e.target.value }))
|
||||||
|
}}
|
||||||
|
className="min-h-[64px] w-full rounded-md border border-grayScale-200 px-3 py-2 text-sm"
|
||||||
|
placeholder="Optional; used for audio-style grading when applicable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" variant="outline" onClick={() => setQuestionDialog({ open: false })}>
|
<Button type="button" variant="outline" onClick={() => setQuestionDialog({ open: false })}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user