AssistantChatForm.tsx128 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { useBreakpoint } from 'common' |
| 4 | import { Loader2 } from 'lucide-react' |
| 5 | import React, { ChangeEvent, memo, useRef } from 'react' |
| 6 | import { ExpandingTextArea } from 'ui' |
| 7 | import { cn } from 'ui/src/lib/utils' |
| 8 | |
| 9 | export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> { |
| 10 | /* The ref for the textarea, optional. Exposed for the CommandsPopover to attach events. */ |
| 11 | textAreaRef?: React.RefObject<HTMLTextAreaElement | null> |
| 12 | /* The loading state of the form */ |
| 13 | loading: boolean |
| 14 | /* The disabled state of the form */ |
| 15 | disabled?: boolean |
| 16 | /* The value of the textarea */ |
| 17 | value?: string |
| 18 | /* The function to handle the value change */ |
| 19 | onValueChange: (value: ChangeEvent<HTMLTextAreaElement>) => void |
| 20 | /* Used to stop onSubmit event when Command popover is open. Use with AssistantCommandsPopover */ |
| 21 | commandsOpen?: boolean |
| 22 | /* Used to close Command popover when onSubmit event happens. Use with AssistantCommandsPopover */ |
| 23 | setCommandsOpen?: (value: boolean) => void |
| 24 | /* The icon to display on the left of the textarea */ |
| 25 | icon?: React.ReactNode |
| 26 | /* The function to handle the form submission */ |
| 27 | onSubmit: React.FormHTMLAttributes<HTMLFormElement>['onSubmit'] |
| 28 | /* The placeholder of the textarea */ |
| 29 | placeholder?: string |
| 30 | } |
| 31 | |
| 32 | const AssistantChatFormComponent = React.forwardRef<HTMLFormElement, FormProps>( |
| 33 | ( |
| 34 | { |
| 35 | loading = false, |
| 36 | disabled = false, |
| 37 | value = '', |
| 38 | textAreaRef, |
| 39 | commandsOpen = false, |
| 40 | icon = null, |
| 41 | onValueChange, |
| 42 | setCommandsOpen, |
| 43 | onSubmit, |
| 44 | placeholder, |
| 45 | ...props |
| 46 | }, |
| 47 | ref |
| 48 | ) => { |
| 49 | const formRef = useRef<HTMLFormElement>(null) |
| 50 | const submitRef = useRef<HTMLButtonElement>(null) |
| 51 | const isMobile = useBreakpoint('md') |
| 52 | |
| 53 | /** |
| 54 | * This function is used to handle the "Enter" key press |
| 55 | */ |
| 56 | const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { |
| 57 | // Check if the pressed key is "Enter" (key code 13) without the "Shift" key |
| 58 | // also checks if the commands popover is open |
| 59 | if (event.key === 'Enter' && event.keyCode === 13 && !event.shiftKey && !commandsOpen) { |
| 60 | event.preventDefault() |
| 61 | if (submitRef.current) submitRef.current.click() |
| 62 | } |
| 63 | |
| 64 | // handles closing the commands popover if open |
| 65 | if (event.key === 'Enter' && commandsOpen) { |
| 66 | if (setCommandsOpen) setCommandsOpen(false) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | return ( |
| 71 | <form |
| 72 | id="assistant-chat" |
| 73 | ref={formRef} |
| 74 | {...props} |
| 75 | onSubmit={onSubmit} |
| 76 | className={cn('relative ', props.className)} |
| 77 | > |
| 78 | <ExpandingTextArea |
| 79 | ref={textAreaRef} |
| 80 | autoFocus={isMobile} |
| 81 | disabled={disabled} |
| 82 | className="text-base md:text-sm pr-10 rounded-sm max-h-64" |
| 83 | placeholder={placeholder} |
| 84 | spellCheck={false} |
| 85 | rows={3} |
| 86 | value={value} |
| 87 | onChange={(event) => onValueChange(event)} |
| 88 | onKeyDown={handleKeyDown} |
| 89 | /> |
| 90 | <div className="absolute right-1.5 top-1.5 flex gap-3 items-center"> |
| 91 | {loading && ( |
| 92 | <Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} /> |
| 93 | )} |
| 94 | |
| 95 | <button |
| 96 | ref={submitRef} |
| 97 | type="submit" |
| 98 | className={cn( |
| 99 | 'transition-all', |
| 100 | 'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative', |
| 101 | !value ? 'text-muted opacity-50' : 'text-default opacity-100', |
| 102 | loading && 'hidden' |
| 103 | )} |
| 104 | > |
| 105 | <svg |
| 106 | width="16" |
| 107 | height="16" |
| 108 | viewBox="0 0 16 16" |
| 109 | fill="none" |
| 110 | xmlns="http://www.w3.org/2000/svg" |
| 111 | > |
| 112 | <path |
| 113 | fillRule="evenodd" |
| 114 | clipRule="evenodd" |
| 115 | d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z" |
| 116 | fill="currentColor" |
| 117 | /> |
| 118 | </svg> |
| 119 | </button> |
| 120 | </div> |
| 121 | </form> |
| 122 | ) |
| 123 | } |
| 124 | ) |
| 125 | |
| 126 | AssistantChatFormComponent.displayName = 'AssistantChatFormComponent' |
| 127 | |
| 128 | export const AssistantChatForm = memo(AssistantChatFormComponent) |