Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { | |
| useState, | |
| useEffect, | |
| useRef, | |
| forwardRef, | |
| useImperativeHandle, | |
| } from 'react'; | |
| import { Button } from '@/components/ui/Button'; | |
| import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit'; | |
| import Img from '../ui/Img'; | |
| import { | |
| Tooltip, | |
| TooltipContent, | |
| TooltipTrigger, | |
| } from '@/components/ui/Tooltip'; | |
| import { IconImage, IconArrowUp, IconClose } from '@/components/ui/Icons'; | |
| import { cn } from '@/lib/utils'; | |
| import Chip from '../ui/Chip'; | |
| import Textarea from 'react-textarea-autosize'; | |
| import useMediaUpload from '@/lib/hooks/useMediaUpload'; | |
| export interface ComposerProps { | |
| onSubmit: (params: { input: string; mediaUrl: string }) => Promise<void>; | |
| disabled?: boolean; | |
| title?: string; | |
| initMediaUrl?: string; | |
| initInput?: string; | |
| } | |
| export interface ComposerRef { | |
| setMediaUrl: (url: string) => void; | |
| setInput: (input: string) => void; | |
| } | |
| const Composer = forwardRef<ComposerRef, ComposerProps>( | |
| ({ disabled, onSubmit, initMediaUrl, initInput }, ref) => { | |
| const { formRef, onKeyDown } = useEnterSubmit(); | |
| const inputRef = useRef<HTMLTextAreaElement>(null); | |
| const [localMediaUrl, setLocalMediaUrl] = useState<string | undefined>( | |
| initMediaUrl, | |
| ); | |
| const [isSubmitting, setIsSubmitting] = useState<boolean>(false); | |
| const [input, setLocalInput] = useState(initInput ?? ''); | |
| const noMediaValidation = !localMediaUrl && !!input; | |
| const { | |
| getRootProps, | |
| getInputProps, | |
| isDragActive, | |
| isUploading, | |
| openUpload, | |
| } = useMediaUpload(uploadUrl => setLocalMediaUrl(uploadUrl)); | |
| const finalLoading = isUploading || isSubmitting; | |
| const finalDisabled = finalLoading || disabled; | |
| useEffect(() => { | |
| if (inputRef.current) { | |
| inputRef.current.focus(); | |
| } | |
| }, []); | |
| useImperativeHandle(ref, () => ({ | |
| setMediaUrl(mediaUrl) { | |
| setLocalMediaUrl(mediaUrl); | |
| }, | |
| setInput(input) { | |
| setLocalInput(input); | |
| }, | |
| })); | |
| const mediaName = localMediaUrl?.split('/').pop(); | |
| return ( | |
| <div | |
| {...getRootProps()} | |
| className={cn( | |
| 'mx-auto w-[42rem] max-w-full px-6 py-4 bg-zinc-600 rounded-xl relative shadow-lg shadow-zinc-600/40 z-50', | |
| isDragActive && 'bg-indigo-700/50', | |
| )} | |
| > | |
| <input {...getInputProps()} /> | |
| <div | |
| className={cn( | |
| 'w-1/3 h-1 rounded-full overflow-hidden bg-zinc-700 absolute left-1/2 -translate-x-1/2 top-2', | |
| finalLoading ? 'opacity-100' : 'opacity-0', | |
| )} | |
| > | |
| <div className="h-full bg-primary animate-progress origin-left-right" /> | |
| </div> | |
| {localMediaUrl ? ( | |
| <Chip className="mb-0.5"> | |
| <div className="flex flex-row items-center space-x-2"> | |
| <Tooltip> | |
| <TooltipTrigger> | |
| <div className="flex flex-row items-center space-x-2"> | |
| <IconImage className="size-3" /> | |
| <p>{mediaName ?? 'unnamed_media'}</p> | |
| </div> | |
| </TooltipTrigger> | |
| <TooltipContent sideOffset={12} className="max-w-2xl"> | |
| <Img | |
| src={localMediaUrl} | |
| className="m-1" | |
| quality={100} | |
| alt="zoomed-in-image" | |
| /> | |
| </TooltipContent> | |
| </Tooltip> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| disabled={finalDisabled} | |
| className="size-4" | |
| onClick={() => setLocalMediaUrl(undefined)} | |
| > | |
| <IconClose className="size-3" /> | |
| </Button> | |
| </div> | |
| </Chip> | |
| ) : ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className={cn( | |
| 'ml-[-10px] border border-transparent', | |
| noMediaValidation && 'border-red-500/50 text-red-500', | |
| )} | |
| onClick={openUpload} | |
| > | |
| <IconImage className="mr-2 size-4" /> | |
| {noMediaValidation ? 'Select media (required)' : 'Select media'} | |
| </Button> | |
| )} | |
| <form | |
| onSubmit={async e => { | |
| e.preventDefault(); | |
| if (!input?.trim() || !localMediaUrl) { | |
| return; | |
| } | |
| setIsSubmitting(true); | |
| try { | |
| await onSubmit({ input, mediaUrl: localMediaUrl }); | |
| } finally { | |
| setIsSubmitting(false); | |
| setLocalInput(''); | |
| } | |
| }} | |
| ref={formRef} | |
| className="h-full mt-4" | |
| > | |
| {/* <div className="border-gray-500 flex overflow-hidden size-full flex flex-row items-center"> */} | |
| <Textarea | |
| ref={inputRef} | |
| tabIndex={0} | |
| onKeyDown={onKeyDown} | |
| rows={1} | |
| value={input} | |
| disabled={finalDisabled} | |
| onChange={e => setLocalInput(e.target.value)} | |
| placeholder={ | |
| finalDisabled ? '🤖 Agent working ✨' : 'Message Vision Agent' | |
| } | |
| spellCheck={false} | |
| className="w-full grow resize-none bg-transparent focus-within:outline-none" | |
| /> | |
| {/* Submit Icon */} | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| type="submit" | |
| size="icon" | |
| className={cn('size-6 absolute bottom-3 right-3')} | |
| disabled={finalDisabled || input === '' || noMediaValidation} | |
| > | |
| <IconArrowUp className="size-3" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent>Message Vision Agent</TooltipContent> | |
| </Tooltip> | |
| </form> | |
| </div> | |
| ); | |
| }, | |
| ); | |
| Composer.displayName = 'Composer'; | |
| export default Composer; | |