Message.Actions.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { Pencil, ThumbsDown, ThumbsUp, Trash2 } from 'lucide-react'
  3. import { useEffect, useState, type PropsWithChildren } from 'react'
  4. import { useForm } from 'react-hook-form'
  5. import {
  6. Button,
  7. cn,
  8. Form,
  9. FormControl,
  10. FormField,
  11. Popover,
  12. PopoverContent,
  13. PopoverTrigger,
  14. TextArea,
  15. } from 'ui'
  16. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  17. import * as z from 'zod'
  18. import { ButtonTooltip } from '../ButtonTooltip'
  19. export function MessageActions({
  20. children,
  21. alwaysShow = false,
  22. }: PropsWithChildren<{ alwaysShow?: boolean }>) {
  23. return (
  24. <div className="flex items-center gap-4 mt-2 mb-1">
  25. <span className="h-0.5 w-5 bg-muted" />
  26. <div className={cn('group-hover:opacity-100 transition-opacity', !alwaysShow && 'opacity-0')}>
  27. {children}
  28. </div>
  29. </div>
  30. )
  31. }
  32. function MessageActionsEdit({ onClick, tooltip }: { onClick: () => void; tooltip: string }) {
  33. return (
  34. <ButtonTooltip
  35. type="text"
  36. icon={<Pencil size={14} strokeWidth={1.5} />}
  37. onClick={onClick}
  38. className="text-foreground-light hover:text-foreground p-1 rounded-sm"
  39. aria-label={tooltip}
  40. tooltip={{
  41. content: {
  42. side: 'bottom',
  43. text: tooltip,
  44. },
  45. }}
  46. />
  47. )
  48. }
  49. MessageActions.Edit = MessageActionsEdit
  50. function MessageActionsDelete({ onClick }: { onClick: () => void }) {
  51. return (
  52. <ButtonTooltip
  53. type="text"
  54. icon={<Trash2 size={14} strokeWidth={1.5} />}
  55. tooltip={{ content: { side: 'bottom', text: 'Delete message' } }}
  56. onClick={onClick}
  57. className="text-foreground-light hover:text-foreground p-1 rounded-sm"
  58. title="Delete message"
  59. aria-label="Delete message"
  60. />
  61. )
  62. }
  63. MessageActions.Delete = MessageActionsDelete
  64. function MessageActionsThumbsUp({
  65. onClick,
  66. isActive,
  67. disabled,
  68. }: {
  69. onClick: () => void
  70. isActive?: boolean
  71. disabled?: boolean
  72. }) {
  73. return (
  74. <Button
  75. type="text"
  76. disabled={disabled}
  77. icon={
  78. <ThumbsUp
  79. size={14}
  80. strokeWidth={1.5}
  81. className={cn(
  82. isActive
  83. ? 'text-brand hover:text-brand-700'
  84. : 'text-foreground-light hover:text-foreground'
  85. )}
  86. />
  87. }
  88. onClick={onClick}
  89. className={cn(
  90. 'p-1 rounded-sm transition-colors',
  91. disabled && 'opacity-50 pointer-events-none'
  92. )}
  93. title="Good response"
  94. aria-label="Good response"
  95. />
  96. )
  97. }
  98. MessageActions.ThumbsUp = MessageActionsThumbsUp
  99. const feedbackSchema = z.object({
  100. reason: z.string().optional(),
  101. })
  102. type FeedbackFormValues = z.infer<typeof feedbackSchema>
  103. function MessageActionsThumbsDown({
  104. onClick,
  105. isActive,
  106. disabled,
  107. }: {
  108. onClick: (reason?: string) => void
  109. isActive?: boolean
  110. disabled?: boolean
  111. }) {
  112. const [open, setOpen] = useState(false)
  113. const form = useForm<FeedbackFormValues>({
  114. resolver: zodResolver(feedbackSchema as any),
  115. defaultValues: { reason: '' },
  116. mode: 'onSubmit',
  117. })
  118. const handleOpenChange = (newOpen: boolean) => {
  119. if (disabled) return
  120. // When popover closes, submit the rating if not already submitted
  121. if (!newOpen && open && !form.formState.isSubmitSuccessful) {
  122. onClick()
  123. }
  124. setOpen(newOpen)
  125. if (!newOpen) {
  126. form.reset()
  127. }
  128. }
  129. const onSubmit = (values: FeedbackFormValues) => {
  130. onClick(values.reason || undefined)
  131. }
  132. // Auto-close popover after showing thank you message
  133. useEffect(() => {
  134. if (form.formState.isSubmitSuccessful) {
  135. const timer = setTimeout(() => {
  136. setOpen(false)
  137. }, 2000)
  138. return () => clearTimeout(timer)
  139. }
  140. }, [form.formState.isSubmitSuccessful])
  141. return (
  142. <Popover open={open} onOpenChange={handleOpenChange}>
  143. <PopoverTrigger asChild>
  144. <Button
  145. type="text"
  146. disabled={disabled}
  147. onClick={() => !disabled && setOpen(true)}
  148. className={cn(
  149. 'p-1 rounded-sm transition-colors',
  150. disabled && 'opacity-50 pointer-events-none'
  151. )}
  152. title="Bad response"
  153. aria-label="Bad response"
  154. >
  155. <ThumbsDown
  156. size={14}
  157. strokeWidth={1.5}
  158. className={cn(
  159. isActive
  160. ? 'text-warning hover:text-warning-700'
  161. : 'text-foreground-light hover:text-foreground'
  162. )}
  163. />
  164. </Button>
  165. </PopoverTrigger>
  166. <PopoverContent className="w-80" align="start">
  167. {form.formState.isSubmitSuccessful ? (
  168. <p className="text-sm">We appreciate your feedback!</p>
  169. ) : (
  170. <Form {...form}>
  171. <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
  172. <FormField
  173. control={form.control}
  174. name="reason"
  175. render={({ field }) => (
  176. <FormItemLayout label="What went wrong?" labelOptional="optional">
  177. <FormControl>
  178. <TextArea
  179. placeholder="Describe why the response was not helpful..."
  180. autoComplete="off"
  181. rows={4}
  182. autoFocus
  183. {...field}
  184. />
  185. </FormControl>
  186. </FormItemLayout>
  187. )}
  188. />
  189. <div className="flex justify-end">
  190. <Button type="primary" htmlType="submit" size="tiny">
  191. Submit feedback
  192. </Button>
  193. </div>
  194. </form>
  195. </Form>
  196. )}
  197. </PopoverContent>
  198. </Popover>
  199. )
  200. }
  201. MessageActions.ThumbsDown = MessageActionsThumbsDown