fix: mini window send message
This commit is contained in:
parent
4939fc8b03
commit
4dacea04a6
@ -1,443 +0,0 @@
|
|||||||
import { ClearOutlined, PauseCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
|
||||||
import { isVisionModel } from '@renderer/config/models'
|
|
||||||
import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
|
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import AttachmentButton from '@renderer/pages/home/Inputbar/AttachmentButton'
|
|
||||||
import AttachmentPreview from '@renderer/pages/home/Inputbar/AttachmentPreview'
|
|
||||||
import KnowledgeBaseButton from '@renderer/pages/home/Inputbar/KnowledgeBaseButton'
|
|
||||||
import SendMessageButton from '@renderer/pages/home/Inputbar/SendMessageButton'
|
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
|
||||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
|
||||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
|
||||||
import { FileType, KnowledgeBase, Message } from '@renderer/types'
|
|
||||||
import { delay, getFileExtension, uuid } from '@renderer/utils'
|
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
|
||||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
const Inputbar: FC = () => {
|
|
||||||
const [text, setText] = useState('')
|
|
||||||
const [inputFocus, setInputFocus] = useState(false)
|
|
||||||
const { defaultAssistant } = useDefaultAssistant()
|
|
||||||
const { defaultModel } = useDefaultModel()
|
|
||||||
const assistant = defaultAssistant
|
|
||||||
const model = defaultModel
|
|
||||||
const {
|
|
||||||
sendMessageShortcut,
|
|
||||||
fontSize,
|
|
||||||
pasteLongTextAsFile,
|
|
||||||
pasteLongTextThreshold,
|
|
||||||
language,
|
|
||||||
autoTranslateWithSpace
|
|
||||||
} = useSettings()
|
|
||||||
const [expended, setExpend] = useState(false)
|
|
||||||
const generating = useAppSelector((state) => state.runtime.generating)
|
|
||||||
const textareaRef = useRef<TextAreaRef>(null)
|
|
||||||
const [files, setFiles] = useState<FileType[]>([])
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const containerRef = useRef(null)
|
|
||||||
const { searching } = useRuntime()
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
|
||||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
|
||||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>()
|
|
||||||
|
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
|
||||||
|
|
||||||
const inputEmpty = isEmpty(text.trim()) && files.length === 0
|
|
||||||
|
|
||||||
const sendMessage = useCallback(async () => {
|
|
||||||
if (generating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputEmpty) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const message: Message = {
|
|
||||||
id: uuid(),
|
|
||||||
role: 'user',
|
|
||||||
content: text,
|
|
||||||
assistantId: assistant.id,
|
|
||||||
topicId: assistant.topics[0].id || uuid(),
|
|
||||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
type: 'text',
|
|
||||||
status: 'success'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedKnowledgeBase) {
|
|
||||||
message.knowledgeBaseIds = [selectedKnowledgeBase.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
|
||||||
message.files = await FileManager.uploadFiles(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
|
||||||
|
|
||||||
setText('')
|
|
||||||
setFiles([])
|
|
||||||
setTimeout(() => setText(''), 500)
|
|
||||||
setTimeout(() => resizeTextArea(), 0)
|
|
||||||
|
|
||||||
setExpend(false)
|
|
||||||
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
|
|
||||||
|
|
||||||
const translate = async () => {
|
|
||||||
if (isTranslating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsTranslating(true)
|
|
||||||
const translatedText = await translateText(text, 'english')
|
|
||||||
translatedText && setText(translatedText)
|
|
||||||
setTimeout(() => resizeTextArea(), 0)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Translation failed:', error)
|
|
||||||
} finally {
|
|
||||||
setIsTranslating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
const isEnterPressed = event.keyCode == 13
|
|
||||||
|
|
||||||
if (autoTranslateWithSpace) {
|
|
||||||
if (event.key === ' ') {
|
|
||||||
setSpaceClickCount((prev) => prev + 1)
|
|
||||||
|
|
||||||
if (spaceClickTimer.current) {
|
|
||||||
clearTimeout(spaceClickTimer.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
spaceClickTimer.current = setTimeout(() => {
|
|
||||||
setSpaceClickCount(0)
|
|
||||||
}, 200)
|
|
||||||
|
|
||||||
if (spaceClickCount === 2) {
|
|
||||||
console.log('Triple space detected - trigger translation')
|
|
||||||
setSpaceClickCount(0)
|
|
||||||
setIsTranslating(true)
|
|
||||||
translate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expended) {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
return setExpend(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Enter' && isEnterPressed) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendMessage()
|
|
||||||
return event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
|
||||||
sendMessage()
|
|
||||||
return event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
|
||||||
sendMessage()
|
|
||||||
return event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
|
||||||
sendMessage()
|
|
||||||
return event.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearTopic = async () => {
|
|
||||||
if (generating) {
|
|
||||||
onPause()
|
|
||||||
await delay(1)
|
|
||||||
}
|
|
||||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPause = () => {
|
|
||||||
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
|
||||||
store.dispatch(setGenerating(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
const resizeTextArea = () => {
|
|
||||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
|
||||||
if (textArea) {
|
|
||||||
textArea.style.height = 'auto'
|
|
||||||
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onInput = () => !expended && resizeTextArea()
|
|
||||||
|
|
||||||
const onPaste = useCallback(
|
|
||||||
async (event: ClipboardEvent) => {
|
|
||||||
for (const file of event.clipboardData?.files || []) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (file.path === '') {
|
|
||||||
if (file.type.startsWith('image/')) {
|
|
||||||
const tempFilePath = await window.api.file.create(file.name)
|
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
|
||||||
const uint8Array = new Uint8Array(arrayBuffer)
|
|
||||||
await window.api.file.write(tempFilePath, uint8Array)
|
|
||||||
const selectedFile = await window.api.file.get(tempFilePath)
|
|
||||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.path) {
|
|
||||||
if (supportExts.includes(getFileExtension(file.path))) {
|
|
||||||
const selectedFile = await window.api.file.get(file.path)
|
|
||||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pasteLongTextAsFile) {
|
|
||||||
const item = event.clipboardData?.items[0]
|
|
||||||
if (item && item.kind === 'string' && item.type === 'text/plain') {
|
|
||||||
item.getAsString(async (pasteText) => {
|
|
||||||
if (pasteText.length > pasteLongTextThreshold) {
|
|
||||||
const tempFilePath = await window.api.file.create('pasted_text.txt')
|
|
||||||
await window.api.file.write(tempFilePath, pasteText)
|
|
||||||
const selectedFile = await window.api.file.get(tempFilePath)
|
|
||||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
|
||||||
setText(text)
|
|
||||||
setTimeout(() => resizeTextArea(), 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pasteLongTextAsFile, pasteLongTextThreshold, supportExts, text]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files)
|
|
||||||
|
|
||||||
files.forEach(async (file) => {
|
|
||||||
if (supportExts.includes(getFileExtension(file.path))) {
|
|
||||||
const selectedFile = await window.api.file.get(file.path)
|
|
||||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTranslated = (translatedText: string) => {
|
|
||||||
setText(translatedText)
|
|
||||||
setTimeout(() => resizeTextArea(), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
textareaRef.current?.focus()
|
|
||||||
}, [assistant])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => resizeTextArea(), 0)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (spaceClickTimer.current) {
|
|
||||||
clearTimeout(spaceClickTimer.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleKnowledgeBaseSelect = (bases: KnowledgeBase[]) => {
|
|
||||||
setSelectedKnowledgeBase(bases?.[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
|
||||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
|
||||||
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
|
|
||||||
<Textarea
|
|
||||||
value={text}
|
|
||||||
onChange={(e) => setText(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
|
||||||
autoFocus
|
|
||||||
contextMenu="true"
|
|
||||||
variant="borderless"
|
|
||||||
rows={1}
|
|
||||||
ref={textareaRef}
|
|
||||||
style={{ fontSize }}
|
|
||||||
styles={{ textarea: TextareaStyle }}
|
|
||||||
onFocus={() => setInputFocus(true)}
|
|
||||||
onBlur={() => setInputFocus(false)}
|
|
||||||
onInput={onInput}
|
|
||||||
disabled={searching}
|
|
||||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
|
||||||
onClick={() => searching && dispatch(setSearching(false))}
|
|
||||||
/>
|
|
||||||
<Toolbar>
|
|
||||||
<ToolbarMenu>
|
|
||||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
|
||||||
<Popconfirm
|
|
||||||
title={t('chat.input.clear.content')}
|
|
||||||
placement="top"
|
|
||||||
onConfirm={clearTopic}
|
|
||||||
okButtonProps={{ danger: true }}
|
|
||||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
|
||||||
okText={t('chat.input.clear')}>
|
|
||||||
<ToolbarButton type="text">
|
|
||||||
<ClearOutlined />
|
|
||||||
</ToolbarButton>
|
|
||||||
</Popconfirm>
|
|
||||||
</Tooltip>
|
|
||||||
<KnowledgeBaseButton
|
|
||||||
selectedBases={selectedKnowledgeBase ? [selectedKnowledgeBase] : []}
|
|
||||||
onSelect={handleKnowledgeBaseSelect}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
disabled={files.length > 0}
|
|
||||||
/>
|
|
||||||
<AttachmentButton
|
|
||||||
model={model}
|
|
||||||
files={files}
|
|
||||||
setFiles={setFiles}
|
|
||||||
ToolbarButton={ToolbarButton}
|
|
||||||
disabled={!!selectedKnowledgeBase}
|
|
||||||
/>
|
|
||||||
</ToolbarMenu>
|
|
||||||
<ToolbarMenu>
|
|
||||||
{!language.startsWith('en') && (
|
|
||||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
|
||||||
)}
|
|
||||||
{generating && (
|
|
||||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
|
||||||
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
|
||||||
</ToolbarMenu>
|
|
||||||
</Toolbar>
|
|
||||||
</InputBarContainer>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
-webkit-app-region: none;
|
|
||||||
`
|
|
||||||
|
|
||||||
const InputBarContainer = styled.div`
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TextareaStyle: CSSProperties = {
|
|
||||||
paddingLeft: 0,
|
|
||||||
padding: '10px 15px 8px'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Textarea = styled(TextArea)`
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
font-family: Ubuntu;
|
|
||||||
resize: none !important;
|
|
||||||
overflow: auto;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
&.ant-input {
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Toolbar = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 8px;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
height: 36px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ToolbarMenu = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ToolbarButton = styled(Button)`
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
font-size: 17px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
color: var(--color-icon);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
&.anticon,
|
|
||||||
&.iconfont {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
color: var(--color-icon);
|
|
||||||
}
|
|
||||||
.icon-a-addchat {
|
|
||||||
font-size: 19px;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
.anticon,
|
|
||||||
.iconfont {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background-color: var(--color-primary) !important;
|
|
||||||
.anticon,
|
|
||||||
.iconfont {
|
|
||||||
color: var(--color-white-soft);
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default Inputbar
|
|
||||||
@ -3,10 +3,12 @@ import { useModel } from '@renderer/hooks/useModel'
|
|||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
||||||
import MessageErrorBoundary from '@renderer/pages/home/Messages/MessageErrorBoundary'
|
import MessageErrorBoundary from '@renderer/pages/home/Messages/MessageErrorBoundary'
|
||||||
|
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||||
|
import { getDefaultAssistant, getDefaultModel } from '@renderer/services/AssistantService'
|
||||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { isMiniWindow } from '@renderer/utils'
|
import { isMiniWindow } from '@renderer/utils'
|
||||||
import { Dispatch, FC, memo, SetStateAction, useMemo, useRef } from 'react'
|
import { Dispatch, FC, memo, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -21,7 +23,8 @@ interface Props {
|
|||||||
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
|
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
|
||||||
isBubbleStyle ? (isAssistantMessage ? 'transparent' : 'var(--chat-background-user)') : undefined
|
isBubbleStyle ? (isAssistantMessage ? 'transparent' : 'var(--chat-background-user)') : undefined
|
||||||
|
|
||||||
const MessageItem: FC<Props> = ({ message, index, total, route }) => {
|
const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetMessages, onGetMessages }) => {
|
||||||
|
const [message, setMessage] = useState(_message)
|
||||||
const model = useModel(getMessageModelId(message))
|
const model = useModel(getMessageModelId(message))
|
||||||
const isBubbleStyle = true
|
const isBubbleStyle = true
|
||||||
const { messageFont, fontSize } = useSettings()
|
const { messageFont, fontSize } = useSettings()
|
||||||
@ -37,6 +40,32 @@ const MessageItem: FC<Props> = ({ message, index, total, route }) => {
|
|||||||
|
|
||||||
const maxWidth = isMiniWindow() ? '480px' : '100%'
|
const maxWidth = isMiniWindow() ? '480px' : '100%'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onGetMessages && onSetMessages) {
|
||||||
|
if (message.status === 'sending') {
|
||||||
|
const messages = onGetMessages()
|
||||||
|
fetchChatCompletion({
|
||||||
|
message,
|
||||||
|
messages: messages
|
||||||
|
.filter((m) => !m.status.includes('ing'))
|
||||||
|
.slice(
|
||||||
|
0,
|
||||||
|
messages.findIndex((m) => m.id === message.id)
|
||||||
|
),
|
||||||
|
assistant: { ...getDefaultAssistant(), model: getDefaultModel() },
|
||||||
|
onResponse: (msg) => {
|
||||||
|
setMessage(msg)
|
||||||
|
if (msg.status !== 'pending') {
|
||||||
|
const _messages = messages.map((m) => (m.id === msg.id ? msg : m))
|
||||||
|
onSetMessages(_messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [message.status])
|
||||||
|
|
||||||
if (['summary', 'explanation'].includes(route) && index === total - 1) {
|
if (['summary', 'explanation'].includes(route) && index === total - 1) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,12 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
|||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const messagesRef = useRef(messages)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
messagesRef.current = messages
|
||||||
|
|
||||||
const onSendMessage = useCallback(
|
const onSendMessage = useCallback(
|
||||||
async (message: Message) => {
|
async (message: Message) => {
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
@ -37,6 +40,10 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
|||||||
[assistant]
|
[assistant]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onGetMessages = useCallback(() => {
|
||||||
|
return messagesRef.current
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)]
|
const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)]
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
@ -53,7 +60,15 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
|||||||
return (
|
return (
|
||||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||||
{[...messages].reverse().map((message, index) => (
|
{[...messages].reverse().map((message, index) => (
|
||||||
<MessageItem key={message.id} message={message} index={index} total={messages.length} route={route} />
|
<MessageItem
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
index={index}
|
||||||
|
total={messages.length}
|
||||||
|
onSetMessages={setMessages}
|
||||||
|
onGetMessages={onGetMessages}
|
||||||
|
route={route}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user