Skip to content

Commit 0234472

Browse files
committed
refactor: use Form, add dropdown
1 parent 16834a7 commit 0234472

File tree

2 files changed

+139
-40
lines changed

2 files changed

+139
-40
lines changed

packages/backend/src/routes/api/chat/feedback.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { getAuthenticatedContext } from '../middleware/authentication'
99

1010
interface ChatFeedbackRequest {
1111
traceId: string
12-
feedback: string
12+
feedback: {
13+
category?: string
14+
comment?: string
15+
}
1316
score: number
1417
}
1518

@@ -32,7 +35,10 @@ const handleChatFeedback = async (
3235
environment: appConfig.appEnv,
3336
name: 'user-feedback',
3437
value: score, // 1 for positive, 0 for negative
35-
comment: feedback,
38+
comment: feedback.comment,
39+
...(feedback?.category && {
40+
metadata: { category: feedback.category },
41+
}),
3642
})
3743
res.status(200).json({ success: true })
3844
} catch (error) {

packages/frontend/src/pages/AiBuilder/components/ChatMessages/ChatMessageToolbar.tsx

Lines changed: 131 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef, useState } from 'react'
1+
import { Controller, useFormContext } from 'react-hook-form'
22
import { FaRegThumbsDown, FaRegThumbsUp } from 'react-icons/fa'
33
import {
44
Button,
@@ -20,6 +20,14 @@ import {
2020
} from '@chakra-ui/react'
2121
import { useToast } from '@opengovsg/design-system-react'
2222

23+
import Form from '@/components/Form'
24+
import { SingleSelect } from '@/components/SingleSelect'
25+
26+
interface FeedbackFormData {
27+
'feedback-dropdown'?: string
28+
'feedback-details': string
29+
}
30+
2331
interface ChatMessageToolbarProps {
2432
traceId: string
2533
}
@@ -29,20 +37,97 @@ interface FeedbackButtonProps {
2937
traceId: string
3038
}
3139

40+
const FEEDBACK_POPOVER_DETAILS = {
41+
positive: {
42+
dropdownLabel: null,
43+
dropdownOptions: null,
44+
textAreaLabel:
45+
'Provide details on what was satisfying about this response:',
46+
textAreaPlaceholder: 'What was satisfying about this response?',
47+
score: 1,
48+
},
49+
negative: {
50+
dropdownLabel: 'What type of issue do you wish to report?',
51+
dropdownOptions: [
52+
'Incorrect workflow generated',
53+
'Incomplete response',
54+
'UI bug',
55+
"I don't understand the response",
56+
'Other',
57+
],
58+
textAreaLabel: 'Provide details on what was wrong with this response:',
59+
textAreaPlaceholder: 'What was wrong with this response?',
60+
score: 0,
61+
},
62+
}
63+
64+
const FeedbackFormContent = ({
65+
dropdownLabel,
66+
dropdownOptions,
67+
textAreaLabel,
68+
textAreaPlaceholder,
69+
autoFocus,
70+
}: {
71+
dropdownLabel: string | null
72+
dropdownOptions: string[] | null
73+
textAreaLabel: string
74+
textAreaPlaceholder: string
75+
autoFocus?: boolean
76+
}) => {
77+
const { control, register } = useFormContext<FeedbackFormData>()
78+
79+
return (
80+
<Flex direction="column">
81+
{dropdownLabel != null && dropdownOptions != null && (
82+
<>
83+
<FormLabel htmlFor="feedback-dropdown" mt={2}>
84+
{dropdownLabel}
85+
</FormLabel>
86+
<Controller
87+
name="feedback-dropdown"
88+
control={control}
89+
defaultValue=""
90+
render={({ field }) => (
91+
<SingleSelect
92+
colorScheme="secondary"
93+
name="feedback-dropdown"
94+
items={dropdownOptions}
95+
value={field.value ?? ''}
96+
onChange={field.onChange}
97+
isClearable={false}
98+
/>
99+
)}
100+
/>
101+
</>
102+
)}
103+
<FormLabel htmlFor="feedback-details" mt={2}>
104+
{textAreaLabel}
105+
</FormLabel>
106+
<Textarea
107+
id="feedback-details"
108+
rows={3}
109+
resize="none"
110+
autoFocus={autoFocus}
111+
placeholder={textAreaPlaceholder}
112+
{...register('feedback-details')}
113+
/>
114+
</Flex>
115+
)
116+
}
117+
32118
const FeedbackButton = ({ feedbackType, traceId }: FeedbackButtonProps) => {
33119
const { onOpen, onClose, isOpen } = useDisclosure()
34-
const firstFieldRef = useRef(null)
35120
const toast = useToast()
36121
const icon = feedbackType === 'positive' ? FaRegThumbsUp : FaRegThumbsDown
37-
const formLabel =
38-
feedbackType === 'positive'
39-
? 'What was helpful about this?'
40-
: 'Why was this not helpful?'
41-
const score = feedbackType === 'positive' ? 1 : 0
42-
43-
const [feedback, setFeedback] = useState('')
122+
const {
123+
dropdownLabel,
124+
dropdownOptions,
125+
textAreaLabel,
126+
textAreaPlaceholder,
127+
score,
128+
} = FEEDBACK_POPOVER_DETAILS[feedbackType]
44129

45-
const handleSubmitFeedback = async (comment: string) => {
130+
const handleSubmitFeedback = async (data: FeedbackFormData) => {
46131
try {
47132
if (!traceId) {
48133
return
@@ -54,13 +139,20 @@ const FeedbackButton = ({ feedbackType, traceId }: FeedbackButtonProps) => {
54139
await fetch('/api/chat/feedback', {
55140
method: 'POST',
56141
headers: { 'Content-Type': 'application/json' },
57-
body: JSON.stringify({ traceId, feedback, score }),
142+
body: JSON.stringify({
143+
traceId,
144+
feedback: {
145+
category: data['feedback-dropdown'],
146+
comment: data['feedback-details'],
147+
},
148+
score,
149+
}),
58150
})
59151
} catch {
60152
// don't throw error if feedback submission fails
61153
// as it is not critical to the user experience
62154
} finally {
63-
// NOTE: do not reset comment here
155+
// NOTE: do not reset form here
64156
// so that user will see what they previously typed or submitted
65157
// if they attempt to submit again
66158
onClose()
@@ -77,9 +169,9 @@ const FeedbackButton = ({ feedbackType, traceId }: FeedbackButtonProps) => {
77169
return (
78170
<Popover
79171
isOpen={isOpen}
80-
initialFocusRef={firstFieldRef}
81172
onOpen={onOpen}
82173
onClose={onClose}
174+
placement="top-start"
83175
>
84176
<PopoverTrigger>
85177
<IconButton
@@ -94,31 +186,32 @@ const FeedbackButton = ({ feedbackType, traceId }: FeedbackButtonProps) => {
94186
<FocusLock persistentFocus={false}>
95187
<PopoverArrow />
96188

97-
<PopoverBody>
98-
<Stack spacing={4}>
99-
<FormControl>
100-
<FormLabel htmlFor="feedback-details">{formLabel}</FormLabel>
101-
<Textarea
102-
ref={firstFieldRef}
103-
id="feedback-details"
104-
rows={3}
105-
resize="none"
106-
value={feedback}
107-
onChange={(e) => setFeedback(e.target.value)}
108-
/>
109-
</FormControl>
110-
<ButtonGroup display="flex" justifyContent="flex-end">
111-
<Button variant="outline" onClick={onClose}>
112-
Cancel
113-
</Button>
114-
<Button
115-
isDisabled={!feedback}
116-
onClick={() => handleSubmitFeedback(feedback)}
117-
>
118-
Submit
119-
</Button>
120-
</ButtonGroup>
121-
</Stack>
189+
<PopoverBody p={4}>
190+
<Form
191+
onSubmit={(data) =>
192+
handleSubmitFeedback(data as FeedbackFormData)
193+
}
194+
>
195+
<Stack spacing={4}>
196+
<FormControl>
197+
<FeedbackFormContent
198+
dropdownLabel={dropdownLabel}
199+
dropdownOptions={dropdownOptions}
200+
textAreaLabel={textAreaLabel}
201+
textAreaPlaceholder={textAreaPlaceholder}
202+
autoFocus={feedbackType === 'positive'}
203+
/>
204+
</FormControl>
205+
<ButtonGroup display="flex" justifyContent="flex-end">
206+
<Button variant="clear" onClick={onClose} size="sm">
207+
Cancel
208+
</Button>
209+
<Button type="submit" size="sm">
210+
Submit
211+
</Button>
212+
</ButtonGroup>
213+
</Stack>
214+
</Form>
122215
</PopoverBody>
123216
</FocusLock>
124217
</PopoverContent>

0 commit comments

Comments
 (0)