11import { useState , useEffect , useRef , useCallback } from 'react'
2- import { X , ChevronLeft , ChevronRight , Check , Loader2 } from 'lucide-react'
2+ import { X , ChevronRight , ChevronDown , ChevronUp , Check , Loader2 } from 'lucide-react'
33import { Button } from '@/components/ui/button'
44import { Textarea } from '@/components/ui/textarea'
55import type { QuestionRequest , QuestionInfo } from '@/api/types'
@@ -10,19 +10,45 @@ interface QuestionPromptProps {
1010 question : QuestionRequest
1111 onReply : ( requestID : string , answers : string [ ] [ ] ) => Promise < void >
1212 onReject : ( requestID : string ) => Promise < void >
13+ onMinimize ?: ( ) => void
1314}
1415
15- export function QuestionPrompt ( { question, onReply, onReject } : QuestionPromptProps ) {
16+ export function QuestionPrompt ( { question, onReply, onReject, onMinimize } : QuestionPromptProps ) {
1617 const questions = question . questions
1718 const isSingleSelect = questions . length === 1 && ! questions [ 0 ] ?. multiple
1819 const totalSteps = isSingleSelect ? 1 : questions . length + 1
1920
21+ const [ isMinimized , setIsMinimized ] = useState ( false )
2022 const [ currentIndex , setCurrentIndex ] = useState ( 0 )
2123 const [ answers , setAnswers ] = useState < string [ ] [ ] > ( ( ) => questions . map ( ( ) => [ ] ) )
2224 const [ customInputs , setCustomInputs ] = useState < string [ ] > ( ( ) => questions . map ( ( ) => '' ) )
2325 const [ confirmedCustoms , setConfirmedCustoms ] = useState < string [ ] > ( ( ) => questions . map ( ( ) => '' ) )
2426 const [ expandedOther , setExpandedOther ] = useState < number | null > ( null )
2527 const [ isSubmitting , setIsSubmitting ] = useState ( false )
28+ const [ touchStartY , setTouchStartY ] = useState < number | null > ( null )
29+
30+ const handleMinimize = useCallback ( ( ) => {
31+ setIsMinimized ( true )
32+ onMinimize ?.( )
33+ } , [ onMinimize ] )
34+
35+ const handleTouchStart = useCallback ( ( e : React . TouchEvent ) => {
36+ setTouchStartY ( e . touches [ 0 ] . clientY )
37+ } , [ ] )
38+
39+ const handleTouchMove = useCallback ( ( e : React . TouchEvent ) => {
40+ if ( touchStartY === null ) return
41+ const touchY = e . touches [ 0 ] . clientY
42+ const diff = touchY - touchStartY
43+ if ( diff > 100 ) {
44+ handleMinimize ( )
45+ setTouchStartY ( null )
46+ }
47+ } , [ touchStartY , handleMinimize ] )
48+
49+ const handleTouchEnd = useCallback ( ( ) => {
50+ setTouchStartY ( null )
51+ } , [ ] )
2652
2753 const isConfirmStep = ! isSingleSelect && currentIndex === questions . length
2854 const currentQuestion = isConfirmStep ? null : questions [ currentIndex ]
@@ -35,13 +61,6 @@ export function QuestionPrompt({ question, onReply, onReject }: QuestionPromptPr
3561 }
3662 } , [ currentIndex , totalSteps ] )
3763
38- const goToPrev = useCallback ( ( ) => {
39- if ( currentIndex > 0 ) {
40- setCurrentIndex ( prev => prev - 1 )
41- setExpandedOther ( null )
42- }
43- } , [ currentIndex ] )
44-
4564 const handleSubmitSingle = useCallback ( async ( label : string ) => {
4665 setIsSubmitting ( true )
4766 try {
@@ -177,94 +196,75 @@ export function QuestionPrompt({ question, onReply, onReject }: QuestionPromptPr
177196
178197 const allQuestionsAnswered = questions . every ( ( _ , i ) => hasAnswerForQuestion ( i ) )
179198
199+ useEffect ( ( ) => {
200+ const handleKeyDown = ( e : KeyboardEvent ) => {
201+ if ( e . key === 'Escape' && ! isMinimized && ! expandedOther ) {
202+ handleMinimize ( )
203+ onMinimize ?.( )
204+ }
205+ }
206+ window . addEventListener ( 'keydown' , handleKeyDown )
207+ return ( ) => {
208+ window . removeEventListener ( 'keydown' , handleKeyDown )
209+ }
210+ } , [ isMinimized , expandedOther , handleMinimize , onMinimize ] )
211+
180212 return (
181213 < div
182- className = "w-full bg-gradient-to-br from-blue-100 to-blue-200 dark:from-blue-950 dark:to-blue-900 border-2 border-blue-300 dark:border-blue-700 rounded-xl shadow-lg shadow-blue-500/20 mb-3 overflow-hidden"
214+ className = "w-full bg-gradient-to-br from-blue-100 to-blue-200 dark:from-blue-950 dark:to-blue-900 border-2 border-blue-300 dark:border-blue-700 rounded-xl shadow-lg shadow-blue-500/20 mb-1 overflow-hidden"
183215 >
184- < div className = "flex items-center justify-between px-2 py-1.5 sm:px-3 sm:py-2 border-b border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/50" >
185- < div className = "flex items-center gap-2" >
186- { totalSteps > 1 && (
187- < button
188- onClick = { goToPrev }
189- disabled = { currentIndex === 0 }
190- className = "p-0.5 sm:p-1 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
191- >
192- < ChevronLeft className = "w-3.5 h-3.5 sm:w-4 sm:h-4 text-blue-600 dark:text-blue-400" />
193- </ button >
194- ) }
195- < span className = "text-xs sm:text-sm font-semibold text-blue-600 dark:text-white" >
196- { isConfirmStep ? 'Review' : (
197- totalSteps > 1
198- ? `Question ${ currentIndex + 1 } /${ questions . length } `
199- : ( currentQuestion ?. header || 'Question' )
200- ) }
201- </ span >
202- { totalSteps > 1 && (
203- < button
204- onClick = { goToNext }
205- disabled = { currentIndex === totalSteps - 1 }
206- className = "p-0.5 sm:p-1 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
207- >
208- < ChevronRight className = "w-3.5 h-3.5 sm:w-4 sm:h-4 text-blue-600 dark:text-blue-400" />
209- </ button >
210- ) }
211- </ div >
216+ < div className = "flex items-center px-3 py-2 sm:px-4 sm:py-2.5 border-b border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/50" >
217+ < button
218+ onClick = { ( ) => isMinimized ? setIsMinimized ( false ) : handleMinimize ( ) }
219+ className = "flex items-center gap-1.5 flex-1 text-left text-xs sm:text-sm font-semibold text-blue-600 dark:text-white"
220+ >
221+ { isMinimized
222+ ? < ChevronUp className = "w-3 h-3 opacity-60 flex-shrink-0" />
223+ : < ChevronDown className = "w-3 h-3 opacity-60 flex-shrink-0" />
224+ }
225+ { isConfirmStep ? 'Review' : ( currentQuestion ?. header || 'Question' ) }
226+ </ button >
212227 < button
213228 onClick = { handleReject }
214229 disabled = { isSubmitting }
215- className = "p-1 sm:p-1.5 rounded-lg hover:bg-red-500/20 text-muted-foreground hover:text-red-500 transition-colors"
230+ className = "p-1.5 sm:p-2 hover:bg-red-500/20 text-muted-foreground hover:text-red-500 transition-colors"
216231 >
217232 < X className = "w-3.5 h-3.5 sm:w-4 sm:h-4" />
218233 </ button >
219234 </ div >
220235
221- < div className = "p-2 sm:p-3 max-h-[50vh] sm:max-h-[70vh] overflow-y-auto overflow-x-hidden bg-background/60 dark:bg-black/30" >
222- { isConfirmStep ? (
223- < ConfirmStep
224- questions = { questions }
225- answers = { answers }
226- onEditQuestion = { setCurrentIndex }
227- />
228- ) : currentQuestion && (
229- < QuestionStep
230- question = { currentQuestion }
231- answers = { answers [ currentIndex ] ?? [ ] }
232- customInput = { customInputs [ currentIndex ] ?? '' }
233- confirmedCustom = { confirmedCustoms [ currentIndex ] ?? '' }
234- expandedOther = { expandedOther === currentIndex }
235- isMultiSelect = { isMultiSelect }
236- onSelectOption = { ( label ) => selectOption ( currentIndex , label ) }
237- onExpandOther = { ( ) => handleExpandOther ( currentIndex ) }
238- onCustomInputChange = { ( value ) => handleCustomInput ( currentIndex , value ) }
239- onConfirmCustomInput = { ( ) => confirmCustomInput ( currentIndex ) }
240- onCollapseOther = { ( ) => setExpandedOther ( null ) }
241- />
242- ) }
243- </ div >
244-
245- { totalSteps > 1 && (
246- < div className = "flex justify-center gap-1 sm:gap-1.5 py-1.5 sm:py-2 border-t border-blue-200 dark:border-blue-800" >
247- { Array . from ( { length : totalSteps } ) . map ( ( _ , i ) => (
248- < button
249- key = { i }
250- onClick = { ( ) => {
251- setCurrentIndex ( i )
252- setExpandedOther ( null )
253- } }
254- className = { cn (
255- "w-1.5 h-1.5 sm:w-2 sm:h-2 rounded-full transition-all duration-200" ,
256- i === currentIndex
257- ? "bg-blue-500 scale-125"
258- : i < questions . length && hasAnswerForQuestion ( i )
259- ? "bg-green-500/70 hover:bg-green-500"
260- : "bg-blue-500/30 hover:bg-blue-500/50"
261- ) }
236+ { ! isMinimized ? (
237+ < div
238+ className = "p-2 sm:p-3 max-h-[50vh] sm:max-h-[70vh] overflow-y-auto overflow-x-hidden bg-background/60 dark:bg-black/30"
239+ onTouchStart = { handleTouchStart }
240+ onTouchMove = { handleTouchMove }
241+ onTouchEnd = { handleTouchEnd }
242+ >
243+ { isConfirmStep ? (
244+ < ConfirmStep
245+ questions = { questions }
246+ answers = { answers }
247+ onEditQuestion = { setCurrentIndex }
262248 />
263- ) ) }
249+ ) : currentQuestion && (
250+ < QuestionStep
251+ question = { currentQuestion }
252+ answers = { answers [ currentIndex ] ?? [ ] }
253+ customInput = { customInputs [ currentIndex ] ?? '' }
254+ confirmedCustom = { confirmedCustoms [ currentIndex ] ?? '' }
255+ expandedOther = { expandedOther === currentIndex }
256+ isMultiSelect = { isMultiSelect }
257+ onSelectOption = { ( label ) => selectOption ( currentIndex , label ) }
258+ onExpandOther = { ( ) => handleExpandOther ( currentIndex ) }
259+ onCustomInputChange = { ( value ) => handleCustomInput ( currentIndex , value ) }
260+ onConfirmCustomInput = { ( ) => confirmCustomInput ( currentIndex ) }
261+ onCollapseOther = { ( ) => setExpandedOther ( null ) }
262+ />
263+ ) }
264264 </ div >
265- ) }
265+ ) : null }
266266
267- < div className = "flex gap-1.5 sm:gap-2 px-2 py-2 sm:px-3 sm:py-3" >
267+ < div className = "flex gap-1.5 sm:gap-2 px-2 py-2 sm:px-3 sm:py-3 border-t border-blue-200 dark:border-blue-800 " >
268268 < Button
269269 size = "sm"
270270 onClick = { handleReject }
@@ -291,12 +291,10 @@ export function QuestionPrompt({ question, onReply, onReject }: QuestionPromptPr
291291 < Button
292292 size = "sm"
293293 onClick = { handleNext }
294- disabled = { currentIndex === totalSteps - 1 || ( expandedOther === currentIndex && ! customInputs [ currentIndex ] ?. trim ( ) ) }
294+ disabled = { isSubmitting || ( expandedOther === currentIndex && ! customInputs [ currentIndex ] ?. trim ( ) ) }
295295 className = "flex-1 h-8 sm:h-10 text-xs sm:text-sm bg-emerald-600 hover:bg-emerald-700 text-white"
296296 >
297- < span className = "hidden sm:inline" > { expandedOther === currentIndex ? 'Confirm' : ( currentIndex === questions . length - 1 ? 'Review' : 'Next' ) } </ span >
298- < span className = "sm:hidden" > { expandedOther === currentIndex ? 'OK' : ( currentIndex === questions . length - 1 ? 'Review' : '→' ) } </ span >
299- < ChevronRight className = "hidden sm:block w-4 h-4 ml-1" />
297+ { expandedOther === currentIndex ? 'Confirm' : 'Next' }
300298 </ Button >
301299 )
302300 ) }
0 commit comments