@@ -28,10 +28,16 @@ export function QuestionEditor({ q, qi, onChange, onRemove, canRemove }: Props)
2828 if ( t === 'true_false' ) {
2929 onChange ( 'options' , [ 'True' , 'False' ] ) ;
3030 onChange ( 'correctIndex' , 0 ) ;
31+ onChange ( 'correctIndices' , undefined ) ;
3132 } else if ( t === 'open_text' ) {
3233 onChange ( 'options' , [ ] ) ;
34+ onChange ( 'correctIndices' , undefined ) ;
35+ } else if ( t === 'multi_select' ) {
36+ if ( ( q . options ?? [ ] ) . length < 2 ) onChange ( 'options' , [ '' , '' , '' , '' ] ) ;
37+ onChange ( 'correctIndices' , [ 0 ] ) ;
3338 } else {
3439 if ( ( q . options ?? [ ] ) . length < 2 ) onChange ( 'options' , [ '' , '' , '' , '' ] ) ;
40+ onChange ( 'correctIndices' , undefined ) ;
3541 }
3642 }
3743
@@ -47,10 +53,26 @@ export function QuestionEditor({ q, qi, onChange, onRemove, canRemove }: Props)
4753
4854 function removeOption ( oi : number ) {
4955 const opts = ( q . options ?? [ ] ) . filter ( ( _ , i ) => i !== oi ) ;
50- const newCorrect =
51- q . correctIndex >= oi && q . correctIndex > 0 ? q . correctIndex - 1 : q . correctIndex ;
56+ if ( type === 'multi_select' ) {
57+ const newIndices = ( q . correctIndices ?? [ ] )
58+ . filter ( ( i ) => i !== oi )
59+ . map ( ( i ) => ( i > oi ? i - 1 : i ) ) ;
60+ onChange ( 'correctIndices' , newIndices ) ;
61+ } else {
62+ const newCorrect =
63+ q . correctIndex >= oi && q . correctIndex > 0 ? q . correctIndex - 1 : q . correctIndex ;
64+ onChange ( 'correctIndex' , Math . min ( newCorrect , opts . length - 1 ) ) ;
65+ }
5266 onChange ( 'options' , opts ) ;
53- onChange ( 'correctIndex' , Math . min ( newCorrect , opts . length - 1 ) ) ;
67+ }
68+
69+ function toggleCorrectIndex ( oi : number ) {
70+ const current = q . correctIndices ?? [ ] ;
71+ if ( current . includes ( oi ) ) {
72+ onChange ( 'correctIndices' , current . filter ( ( i ) => i !== oi ) ) ;
73+ } else {
74+ onChange ( 'correctIndices' , [ ...current , oi ] ) ;
75+ }
5476 }
5577
5678 return (
@@ -105,20 +127,24 @@ export function QuestionEditor({ q, qi, onChange, onRemove, canRemove }: Props)
105127 < div className = "form-group" >
106128 < p className = "form-label" > Question Type</ p >
107129 < div className = "flex gap-2" style = { { flexWrap : 'wrap' } } >
108- { ( [ 'multiple_choice' , 'true_false' , 'open_text' ] as QuestionType [ ] ) . map ( ( t ) => (
109- < button
110- type = "button"
111- key = { t }
112- onClick = { ( ) => setType ( t ) }
113- className = { `btn btn-sm ${ type === t ? 'btn-primary' : 'btn-ghost' } ` }
114- >
115- { t === 'multiple_choice'
116- ? 'Multiple Choice'
117- : t === 'true_false'
118- ? 'True / False'
119- : 'Open Text' }
120- </ button >
121- ) ) }
130+ { ( [ 'multiple_choice' , 'multi_select' , 'true_false' , 'open_text' ] as QuestionType [ ] ) . map (
131+ ( t ) => (
132+ < button
133+ type = "button"
134+ key = { t }
135+ onClick = { ( ) => setType ( t ) }
136+ className = { `btn btn-sm ${ type === t ? 'btn-primary' : 'btn-ghost' } ` }
137+ >
138+ { t === 'multiple_choice'
139+ ? 'Single Choice'
140+ : t === 'multi_select'
141+ ? 'Multiple Answers'
142+ : t === 'true_false'
143+ ? 'True / False'
144+ : 'Open Text' }
145+ </ button >
146+ ) ,
147+ ) }
122148 </ div >
123149 </ div >
124150
@@ -153,7 +179,7 @@ export function QuestionEditor({ q, qi, onChange, onRemove, canRemove }: Props)
153179 />
154180 ) }
155181
156- { /* Options */ }
182+ { /* Options for single choice */ }
157183 { type === 'multiple_choice' && (
158184 < div className = "mb-3" >
159185 < p
@@ -216,6 +242,80 @@ export function QuestionEditor({ q, qi, onChange, onRemove, canRemove }: Props)
216242 </ div >
217243 ) }
218244
245+ { /* Options for multi select */ }
246+ { type === 'multi_select' && (
247+ < div className = "mb-3" >
248+ < p
249+ style = { {
250+ display : 'block' ,
251+ fontSize : '0.82rem' ,
252+ color : 'var(--text2)' ,
253+ marginBottom : 4 ,
254+ fontWeight : 500 ,
255+ } }
256+ >
257+ Answer Options{ ' ' }
258+ < span style = { { fontWeight : 400 , color : 'var(--text3)' } } >
259+ — check all correct answers
260+ </ span >
261+ </ p >
262+ { ( q . options ?? [ ] ) . map ( ( opt , oi ) => {
263+ const isChecked = ( q . correctIndices ?? [ ] ) . includes ( oi ) ;
264+ return (
265+ < div key = { String . fromCharCode ( 65 + oi ) } className = "flex items-center gap-2 mb-2" >
266+ < button
267+ type = "button"
268+ onClick = { ( ) => toggleCorrectIndex ( oi ) }
269+ title = { isChecked ? 'Mark as incorrect' : 'Mark as correct' }
270+ style = { {
271+ width : 28 ,
272+ height : 28 ,
273+ borderRadius : 6 ,
274+ background : isChecked ? 'var(--success)' : 'var(--border)' ,
275+ border : 'none' ,
276+ display : 'flex' ,
277+ alignItems : 'center' ,
278+ justifyContent : 'center' ,
279+ fontWeight : 800 ,
280+ fontSize : '0.75rem' ,
281+ flexShrink : 0 ,
282+ color : isChecked ? '#fff' : 'var(--text2)' ,
283+ cursor : 'pointer' ,
284+ } }
285+ >
286+ { isChecked ? '✓' : String . fromCharCode ( 65 + oi ) }
287+ </ button >
288+ < Input
289+ style = { { flex : 1 , marginBottom : 0 } }
290+ value = { opt }
291+ onChange = { ( e ) => updateOption ( oi , e . target . value ) }
292+ placeholder = { `Option ${ String . fromCharCode ( 65 + oi ) } ` }
293+ />
294+ { ( q . options ?? [ ] ) . length > 2 && (
295+ < button
296+ type = "button"
297+ onClick = { ( ) => removeOption ( oi ) }
298+ className = "btn-icon"
299+ style = { { flexShrink : 0 } }
300+ title = "Remove option"
301+ >
302+ ✕
303+ </ button >
304+ ) }
305+ </ div >
306+ ) ;
307+ } ) }
308+ < button
309+ type = "button"
310+ onClick = { addOption }
311+ className = "btn btn-ghost btn-sm mt-1"
312+ style = { { fontSize : '0.8rem' } }
313+ >
314+ + Add Option
315+ </ button >
316+ </ div >
317+ ) }
318+
219319 { type === 'true_false' && (
220320 < div className = "mb-3" >
221321 < p
0 commit comments