| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { Pencil, ThumbsDown, ThumbsUp, Trash2 } from 'lucide-react'
- import { useEffect, useState, type PropsWithChildren } from 'react'
- import { useForm } from 'react-hook-form'
- import {
- Button,
- cn,
- Form,
- FormControl,
- FormField,
- Popover,
- PopoverContent,
- PopoverTrigger,
- TextArea,
- } from 'ui'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import * as z from 'zod'
- import { ButtonTooltip } from '../ButtonTooltip'
- export function MessageActions({
- children,
- alwaysShow = false,
- }: PropsWithChildren<{ alwaysShow?: boolean }>) {
- return (
- <div className="flex items-center gap-4 mt-2 mb-1">
- <span className="h-0.5 w-5 bg-muted" />
- <div className={cn('group-hover:opacity-100 transition-opacity', !alwaysShow && 'opacity-0')}>
- {children}
- </div>
- </div>
- )
- }
- function MessageActionsEdit({ onClick, tooltip }: { onClick: () => void; tooltip: string }) {
- return (
- <ButtonTooltip
- type="text"
- icon={<Pencil size={14} strokeWidth={1.5} />}
- onClick={onClick}
- className="text-foreground-light hover:text-foreground p-1 rounded-sm"
- aria-label={tooltip}
- tooltip={{
- content: {
- side: 'bottom',
- text: tooltip,
- },
- }}
- />
- )
- }
- MessageActions.Edit = MessageActionsEdit
- function MessageActionsDelete({ onClick }: { onClick: () => void }) {
- return (
- <ButtonTooltip
- type="text"
- icon={<Trash2 size={14} strokeWidth={1.5} />}
- tooltip={{ content: { side: 'bottom', text: 'Delete message' } }}
- onClick={onClick}
- className="text-foreground-light hover:text-foreground p-1 rounded-sm"
- title="Delete message"
- aria-label="Delete message"
- />
- )
- }
- MessageActions.Delete = MessageActionsDelete
- function MessageActionsThumbsUp({
- onClick,
- isActive,
- disabled,
- }: {
- onClick: () => void
- isActive?: boolean
- disabled?: boolean
- }) {
- return (
- <Button
- type="text"
- disabled={disabled}
- icon={
- <ThumbsUp
- size={14}
- strokeWidth={1.5}
- className={cn(
- isActive
- ? 'text-brand hover:text-brand-700'
- : 'text-foreground-light hover:text-foreground'
- )}
- />
- }
- onClick={onClick}
- className={cn(
- 'p-1 rounded-sm transition-colors',
- disabled && 'opacity-50 pointer-events-none'
- )}
- title="Good response"
- aria-label="Good response"
- />
- )
- }
- MessageActions.ThumbsUp = MessageActionsThumbsUp
- const feedbackSchema = z.object({
- reason: z.string().optional(),
- })
- type FeedbackFormValues = z.infer<typeof feedbackSchema>
- function MessageActionsThumbsDown({
- onClick,
- isActive,
- disabled,
- }: {
- onClick: (reason?: string) => void
- isActive?: boolean
- disabled?: boolean
- }) {
- const [open, setOpen] = useState(false)
- const form = useForm<FeedbackFormValues>({
- resolver: zodResolver(feedbackSchema as any),
- defaultValues: { reason: '' },
- mode: 'onSubmit',
- })
- const handleOpenChange = (newOpen: boolean) => {
- if (disabled) return
- // When popover closes, submit the rating if not already submitted
- if (!newOpen && open && !form.formState.isSubmitSuccessful) {
- onClick()
- }
- setOpen(newOpen)
- if (!newOpen) {
- form.reset()
- }
- }
- const onSubmit = (values: FeedbackFormValues) => {
- onClick(values.reason || undefined)
- }
- // Auto-close popover after showing thank you message
- useEffect(() => {
- if (form.formState.isSubmitSuccessful) {
- const timer = setTimeout(() => {
- setOpen(false)
- }, 2000)
- return () => clearTimeout(timer)
- }
- }, [form.formState.isSubmitSuccessful])
- return (
- <Popover open={open} onOpenChange={handleOpenChange}>
- <PopoverTrigger asChild>
- <Button
- type="text"
- disabled={disabled}
- onClick={() => !disabled && setOpen(true)}
- className={cn(
- 'p-1 rounded-sm transition-colors',
- disabled && 'opacity-50 pointer-events-none'
- )}
- title="Bad response"
- aria-label="Bad response"
- >
- <ThumbsDown
- size={14}
- strokeWidth={1.5}
- className={cn(
- isActive
- ? 'text-warning hover:text-warning-700'
- : 'text-foreground-light hover:text-foreground'
- )}
- />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-80" align="start">
- {form.formState.isSubmitSuccessful ? (
- <p className="text-sm">We appreciate your feedback!</p>
- ) : (
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
- <FormField
- control={form.control}
- name="reason"
- render={({ field }) => (
- <FormItemLayout label="What went wrong?" labelOptional="optional">
- <FormControl>
- <TextArea
- placeholder="Describe why the response was not helpful..."
- autoComplete="off"
- rows={4}
- autoFocus
- {...field}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <div className="flex justify-end">
- <Button type="primary" htmlType="submit" size="tiny">
- Submit feedback
- </Button>
- </div>
- </form>
- </Form>
- )}
- </PopoverContent>
- </Popover>
- )
- }
- MessageActions.ThumbsDown = MessageActionsThumbsDown
|