1- import React , { useCallback , useMemo , useRef , useState } from 'react'
1+ import React , { useCallback , useRef , useState } from 'react'
22import { useRenderer } from '@opentui/react'
33
44import { MultilineInput , type MultilineInputHandle } from './multiline-input'
55import { Button } from './button'
66import { useTheme } from '../hooks/use-theme'
7+ import { BORDER_CHARS } from '../utils/ui-constants'
78import type { ChatMessage } from '../types/chat'
89
910interface FeedbackModalProps {
1011 open : boolean
1112 message : ChatMessage | null
1213 onClose : ( ) => void
13- onSubmit : ( text : string ) => void
14+ onSubmit : ( data : { text : string ; category : string | null } ) => void
1415}
1516
16- export const FeedbackModal : React . FC < FeedbackModalProps > = ( { open, message , onClose, onSubmit } ) => {
17+ export const FeedbackModal : React . FC < FeedbackModalProps > = ( { open, onClose, onSubmit } ) => {
1718 const theme = useTheme ( )
1819 const renderer = useRenderer ( )
19- const [ value , setValue ] = useState ( '' )
20- const [ cursorPosition , setCursorPosition ] = useState ( 0 )
21- const [ showDetails , setShowDetails ] = useState ( false )
20+ const [ feedbackText , setFeedbackText ] = useState ( '' )
21+ const [ feedbackCursor , setFeedbackCursor ] = useState ( 0 )
22+ const [ category , setCategory ] = useState < string > ( 'other' )
2223 const inputRef = useRef < MultilineInputHandle | null > ( null )
2324
2425 const terminalWidth = renderer ?. width || 80
@@ -29,29 +30,25 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, message, onC
2930 const modalLeft = Math . floor ( ( terminalWidth - modalWidth ) / 2 )
3031 const modalTop = Math . floor ( ( terminalHeight - modalHeight ) / 2 )
3132
32- const contextPreview = useMemo ( ( ) => {
33- if ( ! message ) return 'No message context'
34- const runState = message . metadata ?. runState
35- const safe = {
36- id : message . id ,
37- variant : message . variant ,
38- timestamp : message . timestamp ,
39- completionTime : message . completionTime ,
40- credits : message . credits ,
41- runStatePreview : runState ? JSON . stringify ( runState ) . slice ( 0 , 1000 ) + ( JSON . stringify ( runState ) . length > 1000 ? ' …' : '' ) : 'n/a' ,
42- }
43- return JSON . stringify ( safe , null , 2 )
44- } , [ message ] )
45-
4633 const handleSubmit = useCallback ( ( ) => {
47- const text = value . trim ( )
34+ const text = feedbackText . trim ( )
4835 if ( text . length === 0 ) return
49- onSubmit ( text )
50- setValue ( '' )
51- } , [ onSubmit , value ] )
36+ onSubmit ( { text, category } )
37+ setFeedbackText ( '' )
38+ setCategory ( 'other' )
39+ } , [ onSubmit , feedbackText , category ] )
5240
5341 if ( ! open ) return null
5442
43+ const categoryOptions = [
44+ { id : 'good_code' , label : 'Good code' , highlight : theme . success } ,
45+ { id : 'bad_code' , label : 'Bad code' , highlight : theme . error } ,
46+ { id : 'bug' , label : 'Bug' , highlight : theme . warning } ,
47+ { id : 'other' , label : 'Other' , highlight : theme . info } ,
48+ ] as const
49+
50+ const canSubmit = feedbackText . trim ( ) . length > 0
51+
5552 return (
5653 < box
5754 position = "absolute"
@@ -69,61 +66,111 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, message, onC
6966 gap : 1 ,
7067 } }
7168 >
72- < text style = { { wrapMode : 'none' } } >
73- < span fg = { theme . primary } > Share Feedback</ span >
74- </ text >
69+ < box style = { { flexDirection : 'row' , justifyContent : 'space-between' , alignItems : 'center' } } >
70+ < text style = { { wrapMode : 'none' } } >
71+ < span fg = { theme . primary } > Share Feedback</ span >
72+ </ text >
73+ < Button
74+ onClick = { onClose }
75+ style = { {
76+ paddingLeft : 1 ,
77+ paddingRight : 1 ,
78+ borderStyle : 'single' ,
79+ borderColor : theme . border ,
80+ customBorderChars : BORDER_CHARS ,
81+ } }
82+ >
83+ < text style = { { wrapMode : 'none' } } >
84+ < span fg = { theme . muted } > X</ span >
85+ </ text >
86+ </ Button >
87+ </ box >
7588
7689 < text style = { { wrapMode : 'word' } } >
7790 < span fg = { theme . secondary } > Thanks for helping us improve! What happened?</ span >
7891 </ text >
7992
80- < box style = { { flexDirection : 'column' } } >
93+ < box style = { { flexDirection : 'column' , gap : 0 } } >
94+ < text style = { { wrapMode : 'none' , paddingBottom : 1 } } >
95+ < span fg = { theme . muted } > Select a category:</ span >
96+ </ text >
97+ < box style = { { flexDirection : 'row' , gap : 1 , flexWrap : 'wrap' } } >
98+ { categoryOptions . map ( ( option ) => {
99+ const isSelected = category === option . id
100+ return (
101+ < Button
102+ key = { option . id }
103+ onClick = { ( ) => setCategory ( option . id ) }
104+ style = { {
105+ flexDirection : 'row' ,
106+ alignItems : 'center' ,
107+ gap : 1 ,
108+ paddingLeft : 1 ,
109+ paddingRight : 1 ,
110+ paddingTop : 0 ,
111+ paddingBottom : 0 ,
112+ borderStyle : 'single' ,
113+ borderColor : isSelected ? option . highlight : theme . border ,
114+ customBorderChars : BORDER_CHARS ,
115+ backgroundColor : isSelected ? theme . surface : undefined ,
116+ } }
117+ >
118+ < text style = { { wrapMode : 'none' } } >
119+ < span fg = { isSelected ? option . highlight : theme . muted } > { isSelected ? '◉' : '◯' } </ span >
120+ < span fg = { isSelected ? theme . foreground : theme . secondary } > { option . label } </ span >
121+ </ text >
122+ </ Button >
123+ )
124+ } ) }
125+ </ box >
126+ </ box >
127+
128+ < box
129+ border
130+ borderStyle = "single"
131+ borderColor = { theme . border }
132+ customBorderChars = { BORDER_CHARS }
133+ style = { { paddingLeft : 1 , paddingRight : 1 , paddingTop : 0 , paddingBottom : 0 } }
134+ >
81135 < MultilineInput
82- value = { value }
136+ value = { feedbackText }
83137 onChange = { ( next : { text : string ; cursorPosition : number ; lastEditDueToNav : boolean } | ( ( prev : { text : string ; cursorPosition : number ; lastEditDueToNav : boolean } ) => { text : string ; cursorPosition : number ; lastEditDueToNav : boolean } ) ) => {
84- const v = typeof next === 'function' ? next ( { text : value , cursorPosition, lastEditDueToNav : false } ) : next
85- setValue ( v . text )
86- setCursorPosition ( v . cursorPosition )
138+ const v = typeof next === 'function' ? next ( { text : feedbackText , cursorPosition : feedbackCursor , lastEditDueToNav : false } ) : next
139+ setFeedbackText ( v . text )
140+ setFeedbackCursor ( v . cursorPosition )
87141 } }
88142 onSubmit = { handleSubmit }
89143 placeholder = { 'Tell us more...' }
90144 focused = { true }
91145 maxHeight = { 6 }
92- width = { modalWidth - 4 }
146+ width = { modalWidth - 6 }
93147 textAttributes = { undefined }
94148 ref = { inputRef }
95- cursorPosition = { cursorPosition }
149+ cursorPosition = { feedbackCursor }
96150 />
97151 </ box >
98152
99- < box style = { { flexDirection : 'row' , gap : 2 , alignItems : 'center ' } } >
153+ < box style = { { flexDirection : 'row' , alignItems : 'center' , justifyContent : 'space-between ' } } >
100154 < text style = { { wrapMode : 'none' } } >
101155 < span fg = { theme . muted } > Auto-attached: Message content • Trace data • Session info</ span >
102156 </ text >
103- < Button onClick = { ( ) => setShowDetails ( ( s ) => ! s ) } >
104- < text style = { { wrapMode : 'none' } } >
105- < span fg = { theme . info } > { showDetails ? '[Hide details]' : '[View details]' } </ span >
106- </ text >
107- </ Button >
108- </ box >
109-
110- { showDetails && (
111- < box style = { { flexDirection : 'column' , maxHeight : 6 } } >
112- < text style = { { wrapMode : 'word' } } >
113- < span fg = { theme . muted } > { contextPreview } </ span >
114- </ text >
115- </ box >
116- ) }
117-
118- < box style = { { flexDirection : 'row' , justifyContent : 'flex-end' , gap : 2 } } >
119- < Button onClick = { onClose } >
120- < text style = { { wrapMode : 'none' } } >
121- < span fg = { theme . error } > Cancel</ span >
122- </ text >
123- </ Button >
124- < Button onClick = { handleSubmit } >
157+ < Button
158+ onClick = { ( ) => {
159+ if ( canSubmit ) handleSubmit ( )
160+ } }
161+ style = { {
162+ flexDirection : 'row' ,
163+ alignItems : 'center' ,
164+ paddingLeft : 1 ,
165+ paddingRight : 1 ,
166+ borderStyle : 'single' ,
167+ borderColor : canSubmit ? theme . foreground : theme . border ,
168+ customBorderChars : BORDER_CHARS ,
169+ backgroundColor : canSubmit ? theme . surface : undefined ,
170+ } }
171+ >
125172 < text style = { { wrapMode : 'none' } } >
126- < span fg = { theme . success } > Submit </ span >
173+ < span fg = { canSubmit ? theme . foreground : theme . muted } > { '< SUBMIT' } </ span >
127174 </ text >
128175 </ Button >
129176 </ box >
0 commit comments