Spaces:
Sleeping
Sleeping
'use client'; | |
import * as React from 'react'; | |
import { type UseChatHelpers } from 'ai/react'; | |
import Textarea from 'react-textarea-autosize'; | |
import { Button } from '@/components/ui/Button'; | |
import { MessageBase } from '../../lib/types'; | |
import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit'; | |
import Img from '../ui/Img'; | |
import { | |
Tooltip, | |
TooltipContent, | |
TooltipTrigger, | |
} from '@/components/ui/Tooltip'; | |
import { | |
IconArrowDown, | |
IconArrowElbow, | |
IconRefresh, | |
IconStop, | |
} from '@/components/ui/Icons'; | |
import { cn } from '@/lib/utils'; | |
import { generateInputImageMarkdown } from '@/lib/messageUtils'; | |
import { Switch } from '../ui/Switch'; | |
export interface ComposerProps | |
extends Pick< | |
UseChatHelpers, | |
'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput' | |
> { | |
id?: string; | |
title?: string; | |
messages: MessageBase[]; | |
url?: string; | |
isAtBottom: boolean; | |
scrollToBottom: () => void; | |
} | |
export function Composer({ | |
id, | |
title, | |
isLoading, | |
stop, | |
append, | |
reload, | |
input, | |
setInput, | |
messages, | |
isAtBottom, | |
scrollToBottom, | |
url, | |
}: ComposerProps) { | |
const { formRef, onKeyDown } = useEnterSubmit(); | |
const inputRef = React.useRef<HTMLTextAreaElement>(null); | |
React.useEffect(() => { | |
if (inputRef.current) { | |
inputRef.current.focus(); | |
} | |
}, []); | |
return ( | |
// <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]"> | |
<div className="mx-auto sm:max-w-3xl sm:px-4 h-full"> | |
<div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4 h-full"> | |
<form | |
onSubmit={async e => { | |
e.preventDefault(); | |
if (!input?.trim()) { | |
return; | |
} | |
setInput(''); | |
await append({ | |
id, | |
content: | |
input + (url ? '\n\n' + generateInputImageMarkdown(url) : ''), | |
role: 'user', | |
}); | |
scrollToBottom(); | |
}} | |
ref={formRef} | |
className="h-full" | |
> | |
<div className="relative flex px-8 pl-2 overflow-hidden size-full bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2 items-start"> | |
{url && ( | |
<div className="w-1/5 p-2 h-full flex items-center justify-center relative"> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Img | |
src={url} | |
className="cursor-zoom-in" | |
alt="preview-image" | |
/> | |
</TooltipTrigger> | |
<TooltipContent> | |
<Img | |
src={url} | |
className="m-2" | |
quality={100} | |
width={500} | |
alt="zoomed-in-image" | |
/> | |
</TooltipContent> | |
</Tooltip> | |
</div> | |
)} | |
<div className="flex flex-col gap-2 w-4/5 px-4"> | |
<Textarea | |
ref={inputRef} | |
tabIndex={0} | |
onKeyDown={onKeyDown} | |
rows={1} | |
value={input} | |
disabled={isLoading} | |
onChange={e => setInput(e.target.value)} | |
placeholder={ | |
isLoading | |
? 'Vision Agent is thinking...' | |
: 'Ask question about the image.' | |
} | |
spellCheck={false} | |
className="min-h-[60px] resize-none bg-transparent py-[1.3em] focus-within:outline-none sm:text-sm" | |
/> | |
</div> | |
{/* Scroll to bottom Icon */} | |
<div | |
className={cn( | |
'absolute top-3 right-4 transition-opacity duration-300', | |
isAtBottom ? 'opacity-0' : 'opacity-100', | |
)} | |
> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
variant="outline" | |
size="icon" | |
className="bg-background" | |
onClick={() => scrollToBottom()} | |
> | |
<IconArrowDown /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent>Scroll to bottom</TooltipContent> | |
</Tooltip> | |
</div> | |
{/* Stop / Regenerate Icon */} | |
<div className="absolute bottom-14 right-4"> | |
{isLoading ? ( | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
variant="outline" | |
size="icon" | |
className="bg-background" | |
onClick={() => stop()} | |
> | |
<IconStop /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent>Stop generating</TooltipContent> | |
</Tooltip> | |
) : ( | |
messages?.length >= 2 && ( | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
variant="outline" | |
size="icon" | |
className="bg-background" | |
onClick={() => reload()} | |
> | |
<IconRefresh /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent>Regenerate response</TooltipContent> | |
</Tooltip> | |
) | |
)} | |
</div> | |
{/* Submit Icon */} | |
<div className="absolute bottom-3 right-4"> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
type="submit" | |
size="icon" | |
disabled={isLoading || input === ''} | |
> | |
<IconArrowElbow /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent>Send message</TooltipContent> | |
</Tooltip> | |
</div> | |
</div> | |
</form> | |
</div> | |
</div> | |
// </div> | |
); | |
} | |