|
'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> |
|
) |
|
} |
|
|