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; | |