Spaces:
Sleeping
Sleeping
'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> | |
) | |
} | |