bingo / src /components /chat-panel.tsx
ojumpin's picture
Duplicate from hf4all/bingo
59d634d
'use client'
import * as React from 'react'
import Image from 'next/image'
import Textarea from 'react-textarea-autosize'
import { useAtomValue } from 'jotai'
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
import { cn } from '@/lib/utils'
import BrushIcon from '@/assets/images/brush.svg'
import ChatIcon from '@/assets/images/chat.svg'
import VisualSearchIcon from '@/assets/images/visual-search.svg'
import SendIcon from '@/assets/images/send.svg'
import PinIcon from '@/assets/images/pin.svg'
import PinFillIcon from '@/assets/images/pin-fill.svg'
import { useBing } from '@/lib/hooks/use-bing'
import { voiceListenAtom } from '@/state'
import Voice from './voice'
import { ChatImage } from './chat-image'
import { ChatAttachments } from './chat-attachments'
export interface ChatPanelProps
extends Pick<
ReturnType<typeof useBing>,
| 'generating'
| 'input'
| 'setInput'
| 'sendMessage'
| 'resetConversation'
| 'isSpeaking'
| 'attachmentList'
| 'uploadImage'
| 'setAttachmentList'
> {
id?: string
className?: string
}
export function ChatPanel({
isSpeaking,
generating,
input,
setInput,
className,
sendMessage,
resetConversation,
attachmentList,
uploadImage,
setAttachmentList
}: ChatPanelProps) {
const inputRef = React.useRef<HTMLTextAreaElement>(null)
const {formRef, onKeyDown} = useEnterSubmit()
const [focused, setFocused] = React.useState(false)
const [active, setActive] = React.useState(false)
const [pin, setPin] = React.useState(false)
const [tid, setTid] = React.useState<any>()
const voiceListening = useAtomValue(voiceListenAtom)
const setBlur = React.useCallback(() => {
clearTimeout(tid)
setActive(false)
const _tid = setTimeout(() => setFocused(false), 2000);
setTid(_tid)
}, [tid])
const setFocus = React.useCallback(() => {
setFocused(true)
setActive(true)
clearTimeout(tid)
inputRef.current?.focus()
}, [tid])
React.useEffect(() => {
if (input) {
setFocus()
}
}, [input, setFocus])
return (
<form
className={cn('chat-panel', className)}
onSubmit={async e => {
e.preventDefault()
if (generating) {
return;
}
if (!input?.trim()) {
return
}
setInput('')
setPin(false)
await sendMessage(input)
}}
ref={formRef}
>
<div className="action-bar pb-4">
<div className={cn('action-root', { focus: active || pin })} speech-state="hidden" visual-search="" drop-target="">
<div className="fade bottom">
<div className="background"></div>
</div>
<div className={cn('outside-left-container', { collapsed: focused })}>
<div className="button-compose-wrapper">
<button className="body-2 button-compose" type="button" aria-label="新主题" onClick={resetConversation}>
<div className="button-compose-content">
<Image className="pl-2" alt="brush" src={BrushIcon} width={40} />
<div className="button-compose-text">新主题</div>
</div>
</button>
</div>
</div>
<div
className={cn('main-container', { active: active || pin })}
style={{ minHeight: pin ? '360px' : undefined }}
onClick={setFocus}
onBlur={setBlur}
>
<div className="main-bar">
<Image alt="chat" src={ChatIcon} width={20} color="blue" />
<Textarea
ref={inputRef}
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
value={input}
onChange={e => setInput(e.target.value.slice(0, 4000))}
placeholder={voiceListening ? '持续对话中...对话完成说“发送”即可' : 'Shift + Enter 换行'}
spellCheck={false}
className="message-input min-h-[24px] -mx-1 w-full text-base resize-none bg-transparent focus-within:outline-none"
/>
<ChatImage uploadImage={uploadImage}>
<Image alt="visual-search" src={VisualSearchIcon} width={24} />
</ChatImage>
<Voice setInput={setInput} sendMessage={sendMessage} isSpeaking={isSpeaking} input={input} />
<button type="submit">
<Image alt="send" src={SendIcon} width={20} style={{ marginTop: '2px' }} />
</button>
</div>
<ChatAttachments attachmentList={attachmentList} setAttachmentList={setAttachmentList} uploadImage={uploadImage} />
<div className="body-1 bottom-bar">
<div className="letter-counter"><span>{input.length}</span>/4000</div>
<button onClick={() => {
setPin(!pin)
}} className="pr-2">
<Image alt="pin" src={pin ? PinFillIcon : PinIcon} width={20} />
</button>
</div>
</div>
</div>
</div>
</form>
)
}