Spaces:
Running
Running
'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, | |
IconImage, | |
IconRefresh, | |
IconStop, | |
} from '@/components/ui/Icons'; | |
import { cn } from '@/lib/utils'; | |
import { generateInputImageMarkdown } from '@/lib/messageUtils'; | |
import { Switch } from '../ui/Switch'; | |
import Chip from '../ui/Chip'; | |
export interface ComposerProps | |
extends Pick< | |
UseChatHelpers, | |
'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput' | |
> { | |
id?: string; | |
title?: string; | |
messages: MessageBase[]; | |
mediaUrl?: string; | |
} | |
export function Composer({ | |
id, | |
isLoading, | |
append, | |
input, | |
setInput, | |
mediaUrl, | |
// isAtBottom, | |
}: ComposerProps) { | |
const { formRef, onKeyDown } = useEnterSubmit(); | |
const inputRef = React.useRef<HTMLTextAreaElement>(null); | |
React.useEffect(() => { | |
if (inputRef.current) { | |
inputRef.current.focus(); | |
} | |
}, []); | |
const mediaName = mediaUrl?.split('/').pop(); | |
return ( | |
<div className="size-full mx-auto max-w-2xl px-6 py-3 space-y-4 bg-zinc-700 rounded-xl relative shadow-lg shadow-zinc-800/40"> | |
{mediaUrl && ( | |
<Tooltip> | |
<TooltipTrigger> | |
<Chip> | |
<div className="flex flex-row items-center"> | |
<IconImage className="size-3 mr-1" /> | |
<p>{mediaName ?? 'media(0)'}</p> | |
</div> | |
</Chip> | |
</TooltipTrigger> | |
<TooltipContent sideOffset={20}> | |
<Img | |
src={mediaUrl} | |
className="m-1" | |
quality={100} | |
alt="zoomed-in-image" | |
/> | |
</TooltipContent> | |
</Tooltip> | |
)} | |
<form | |
onSubmit={async e => { | |
e.preventDefault(); | |
if (!input?.trim()) { | |
return; | |
} | |
setInput(''); | |
await append({ | |
id, | |
content: | |
input + | |
(mediaUrl ? '\n\n' + generateInputImageMarkdown(mediaUrl) : ''), | |
role: 'user', | |
}); | |
}} | |
ref={formRef} | |
className="h-full" | |
> | |
{/* <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={isLoading} | |
onChange={e => setInput(e.target.value)} | |
placeholder={isLoading ? '🤖 ✨ ...' : 'Message Vision Agent'} | |
spellCheck={false} | |
className="w-full grow resize-none bg-transparent focus-within:outline-none text-sm" | |
/> | |
{/* 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> */} | |
{/* </div> */} | |
{/* Submit Icon */} | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
type="submit" | |
size="icon" | |
className="size-6 absolute bottom-3 right-3" | |
disabled={isLoading || input === ''} | |
> | |
<IconArrowElbow className="size-3" /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent>Send message</TooltipContent> | |
</Tooltip> | |
</form> | |
{/* Scroll to bottom Icon */} | |
{/* <Tooltip> | |
<TooltipTrigger asChild> | |
<Button | |
size="icon" | |
className={cn( | |
'absolute top-1 right-3 transition-opacity duration-300 size-6', | |
isAtBottom ? 'opacity-0' : 'opacity-100', | |
)} | |
onClick={() => scrollToBottom()} | |
> | |
<IconArrowDown className="size-3" /> | |
</Button> | |
</TooltipTrigger> | |
<TooltipContent>Scroll to bottom</TooltipContent> | |
</Tooltip> */} | |
</div> | |
); | |
} | |