Skip to content

Commit 9963b4c

Browse files
committed
cli: Redesign ask-user UI, disable past followups
1 parent 7ceb062 commit 9963b4c

File tree

5 files changed

+312
-127
lines changed

5 files changed

+312
-127
lines changed

cli/src/components/ask-user/index.tsx

Lines changed: 107 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -118,26 +118,44 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
118118
[questions, answers],
119119
)
120120

121+
const setAnswerForQuestion = useCallback(
122+
(
123+
questionIndex: number,
124+
updater: (previous: AccordionAnswer | undefined) => AccordionAnswer,
125+
) => {
126+
setAnswers((prev) => {
127+
const nextAnswers = new Map(prev)
128+
const previousAnswer = prev.get(questionIndex)
129+
nextAnswers.set(questionIndex, updater(previousAnswer))
130+
return nextAnswers
131+
})
132+
},
133+
[],
134+
)
135+
136+
const goToNextUnanswered = useCallback(
137+
(questionIndex: number) => {
138+
const nextUnanswered = findNextUnanswered(questionIndex)
139+
setExpandedIndex(nextUnanswered)
140+
},
141+
[findNextUnanswered],
142+
)
143+
121144
// Handle setting "Other" text (with cursor position)
122145
const handleSetOtherText = useCallback(
123146
(questionIndex: number, text: string, cursorPosition: number) => {
124-
setAnswers((prev) => {
125-
const newAnswers = new Map(prev)
126-
const currentAnswer = prev.get(questionIndex)
127-
newAnswers.set(questionIndex, {
128-
...currentAnswer,
129-
isOther: true,
130-
otherText: text,
131-
})
132-
return newAnswers
133-
})
147+
setAnswerForQuestion(questionIndex, (currentAnswer) => ({
148+
...currentAnswer,
149+
isOther: true,
150+
otherText: text,
151+
}))
134152
setOtherCursorPositions((prev) => {
135153
const newPositions = new Map(prev)
136154
newPositions.set(questionIndex, cursorPosition)
137155
return newPositions
138156
})
139157
},
140-
[],
158+
[setAnswerForQuestion],
141159
)
142160

143161
// Handle "Other" text submit (Enter key)
@@ -149,154 +167,146 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
149167
setIsTypingOther(false)
150168
// If text is entered, move to next question
151169
if (currentText.trim()) {
152-
const nextUnanswered = findNextUnanswered(questionIndex)
153-
setExpandedIndex(nextUnanswered)
170+
goToNextUnanswered(questionIndex)
154171
}
155172
},
156-
[answers, findNextUnanswered],
173+
[answers, goToNextUnanswered],
157174
)
158175

159176
// Handle "Other" text cancel (Escape key) - deselect Custom option entirely
160177
const handleOtherCancel = useCallback(
161178
(questionIndex: number) => {
162179
// Clear text, deselect "Custom" option, and exit typing mode
163-
setAnswers((prev) => {
164-
const newAnswers = new Map(prev)
165-
const currentAnswer = prev.get(questionIndex)
166-
// Deselect "Custom" by setting isOther to false and clearing text
167-
newAnswers.set(questionIndex, {
168-
...currentAnswer,
169-
isOther: false,
170-
otherText: '',
171-
})
172-
return newAnswers
173-
})
180+
setAnswerForQuestion(questionIndex, (currentAnswer) => ({
181+
...currentAnswer,
182+
isOther: false,
183+
otherText: '',
184+
}))
174185
setOtherCursorPositions((prev) => {
175186
const newPositions = new Map(prev)
176187
newPositions.set(questionIndex, 0)
177188
return newPositions
178189
})
179190
setIsTypingOther(false)
180191
},
181-
[],
192+
[setAnswerForQuestion],
182193
)
183194

184195
// Handle selecting an option (single-select)
185196
const handleSelectOption = useCallback(
186197
(questionIndex: number, optionIndex: number) => {
187-
setAnswers((prev) => {
188-
const newAnswers = new Map(prev)
189-
if (optionIndex === OTHER_OPTION_INDEX) {
190-
// "Other" option - enter typing mode
191-
newAnswers.set(questionIndex, {
192-
isOther: true,
193-
otherText: prev.get(questionIndex)?.otherText || '',
194-
})
195-
} else {
196-
newAnswers.set(questionIndex, {
197-
selectedIndex: optionIndex,
198-
isOther: false,
199-
})
200-
}
201-
return newAnswers
202-
})
198+
const isOtherOption = optionIndex === OTHER_OPTION_INDEX
199+
setAnswerForQuestion(questionIndex, (currentAnswer) =>
200+
isOtherOption
201+
? {
202+
...currentAnswer,
203+
isOther: true,
204+
otherText: currentAnswer?.otherText || '',
205+
}
206+
: {
207+
selectedIndex: optionIndex,
208+
isOther: false,
209+
},
210+
)
203211

204212
// For "Other" option, enter typing mode
205-
if (optionIndex === OTHER_OPTION_INDEX) {
213+
if (isOtherOption) {
206214
setIsTypingOther(true)
207215
} else {
208216
// For regular options, collapse and move to next unanswered
209-
const nextUnanswered = findNextUnanswered(questionIndex)
210-
setExpandedIndex(nextUnanswered)
217+
goToNextUnanswered(questionIndex)
211218
}
212219
},
213-
[findNextUnanswered],
220+
[goToNextUnanswered, setAnswerForQuestion],
214221
)
215222

216223
// Handle toggling an option (multi-select)
217224
const handleToggleOption = useCallback(
218225
(questionIndex: number, optionIndex: number) => {
226+
let toggledOtherOn = false
227+
219228
setAnswers((prev) => {
220229
const newAnswers = new Map(prev)
221230
const currentAnswer = prev.get(questionIndex)
222-
const currentIndices = currentAnswer?.selectedIndices ?? new Set()
223231

224232
if (optionIndex === OTHER_OPTION_INDEX) {
225-
// "Other" option toggle
226-
const wasOther = currentAnswer?.isOther ?? false
233+
toggledOtherOn = !(currentAnswer?.isOther ?? false)
227234
newAnswers.set(questionIndex, {
228235
...currentAnswer,
229-
selectedIndices: currentIndices,
230-
isOther: !wasOther,
236+
selectedIndices: new Set(currentAnswer?.selectedIndices ?? []),
237+
isOther: !currentAnswer?.isOther,
231238
otherText: currentAnswer?.otherText || '',
232239
})
240+
return newAnswers
241+
}
242+
243+
const newIndices = new Set(currentAnswer?.selectedIndices ?? [])
244+
if (newIndices.has(optionIndex)) {
245+
newIndices.delete(optionIndex)
233246
} else {
234-
const newIndices = new Set(currentIndices)
235-
if (newIndices.has(optionIndex)) {
236-
newIndices.delete(optionIndex)
237-
} else {
238-
newIndices.add(optionIndex)
239-
}
240-
newAnswers.set(questionIndex, {
241-
...currentAnswer,
242-
selectedIndices: newIndices,
243-
isOther: currentAnswer?.isOther ?? false,
244-
})
247+
newIndices.add(optionIndex)
245248
}
249+
newAnswers.set(questionIndex, {
250+
...currentAnswer,
251+
selectedIndices: newIndices,
252+
isOther: currentAnswer?.isOther ?? false,
253+
})
246254
return newAnswers
247255
})
248256

249257
// For "Other" option in multi-select, also enter typing mode
250258
if (optionIndex === OTHER_OPTION_INDEX) {
251-
// Check if we're toggling ON (not off)
252-
const currentAnswer = answers.get(questionIndex)
253-
if (!currentAnswer?.isOther) {
254-
setIsTypingOther(true)
255-
} else {
256-
setIsTypingOther(false)
257-
}
259+
setIsTypingOther(toggledOtherOn)
258260
}
259261
},
260-
[answers],
262+
[],
261263
)
262264

263-
// Handle submit
264-
const handleSubmit = useCallback(() => {
265-
const formattedAnswers = questions.map(
266-
(question: AskUserQuestion, index: number) => {
267-
const answer = answers.get(index)
268-
if (!answer) {
269-
return { question: question.question, answer: 'Skipped' }
270-
}
271-
272-
if (answer.isOther && answer.otherText) {
273-
return { question: question.question, answer: answer.otherText }
274-
}
265+
const formatAnswer = useCallback(
266+
(
267+
question: AskUserQuestion,
268+
answer: AccordionAnswer | undefined,
269+
) => {
270+
if (!answer) {
271+
return { question: question.question, answer: 'Skipped' }
272+
}
275273

276-
if (question.multiSelect && answer.selectedIndices) {
277-
const selectedLabels = Array.from(answer.selectedIndices)
274+
const selectedOptions = question.multiSelect
275+
? Array.from(answer.selectedIndices ?? [])
278276
.map((idx: number) => getOptionLabel(question.options[idx]))
279277
.filter(Boolean)
280-
return {
281-
question: question.question,
282-
answer: selectedLabels.join(', '),
283-
}
284-
}
278+
: answer.selectedIndex !== undefined
279+
? [getOptionLabel(question.options[answer.selectedIndex])]
280+
: []
285281

286-
if (answer.selectedIndex !== undefined) {
287-
const label = getOptionLabel(question.options[answer.selectedIndex])
288-
return {
289-
question: question.question,
290-
answer: label || 'Unknown',
291-
}
292-
}
282+
const customText =
283+
answer.isOther && (answer.otherText?.trim().length ?? 0) > 0
284+
? (answer.otherText ?? '').trim()
285+
: ''
293286

287+
const parts = customText ? [...selectedOptions, customText] : selectedOptions
288+
if (parts.length === 0) {
294289
return { question: question.question, answer: 'Skipped' }
290+
}
291+
292+
return {
293+
question: question.question,
294+
answer: question.multiSelect ? parts.join(', ') : parts[0],
295+
}
296+
},
297+
[],
298+
)
299+
300+
// Handle submit
301+
const handleSubmit = useCallback(() => {
302+
const formattedAnswers = questions.map(
303+
(question: AskUserQuestion, index: number) => {
304+
return formatAnswer(question, answers.get(index))
295305
},
296306
)
297307

298308
onSubmit(formattedAnswers)
299-
}, [questions, answers, onSubmit])
309+
}, [questions, answers, onSubmit, formatAnswer])
300310

301311
// Keyboard navigation using OpenTUI's useKeyboard hook
302312
useKeyboard(

0 commit comments

Comments
 (0)