mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
feat(chat): add agent session inputbar component
Implement input bar for agent sessions with message sending functionality
This commit is contained in:
parent
e45231376c
commit
4bd6087dc0
@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import ChatNavbar from './ChatNavbar'
|
import ChatNavbar from './ChatNavbar'
|
||||||
|
import AgentSessionInputbar from './Inputbar/AgentSessionInputbar'
|
||||||
import Inputbar from './Inputbar/Inputbar'
|
import Inputbar from './Inputbar/Inputbar'
|
||||||
import AgentSessionMessages from './Messages/AgentSessionMessages'
|
import AgentSessionMessages from './Messages/AgentSessionMessages'
|
||||||
import ChatNavigation from './Messages/ChatNavigation'
|
import ChatNavigation from './Messages/ChatNavigation'
|
||||||
@ -151,6 +152,17 @@ const Chat: FC<Props> = (props) => {
|
|||||||
return <AgentSessionMessages agentId={activeAgentId} sessionId={sessionId} />
|
return <AgentSessionMessages agentId={activeAgentId} sessionId={sessionId} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SessionInputBar = () => {
|
||||||
|
if (activeAgentId === null) {
|
||||||
|
return <div> Active Agent ID is invalid.</div>
|
||||||
|
}
|
||||||
|
const sessionId = activeSessionId[activeAgentId]
|
||||||
|
if (!sessionId) {
|
||||||
|
return <div> Active Session ID is invalid.</div>
|
||||||
|
}
|
||||||
|
return <AgentSessionInputbar agentId={activeAgentId} sessionId={sessionId} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
|
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
|
||||||
{isTopNavbar && (
|
{isTopNavbar && (
|
||||||
@ -192,7 +204,12 @@ const Chat: FC<Props> = (props) => {
|
|||||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeTopicOrSession === 'session' && <SessionMessages />}
|
{activeTopicOrSession === 'session' && (
|
||||||
|
<>
|
||||||
|
<SessionMessages />
|
||||||
|
<SessionInputBar />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||||
</QuickPanelProvider>
|
</QuickPanelProvider>
|
||||||
</Main>
|
</Main>
|
||||||
|
|||||||
236
src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx
Normal file
236
src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||||
|
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
|
import PasteService from '@renderer/services/PasteService'
|
||||||
|
import { classNames } from '@renderer/utils'
|
||||||
|
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||||
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import React, { CSSProperties, FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import NarrowLayout from '../Messages/NarrowLayout'
|
||||||
|
import SendMessageButton from './SendMessageButton'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('Inputbar')
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
agentId: string
|
||||||
|
sessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const _text = ''
|
||||||
|
|
||||||
|
const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||||
|
const [text, setText] = useState(_text)
|
||||||
|
const [inputFocus, setInputFocus] = useState(false)
|
||||||
|
const { createSessionMessage } = useSession(agentId, sessionId)
|
||||||
|
|
||||||
|
const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings()
|
||||||
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
|
||||||
|
const { setTimeoutTimer } = useTimer()
|
||||||
|
|
||||||
|
const focusTextarea = useCallback(() => {
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const inputEmpty = isEmpty(text)
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
//to check if the SendMessage key is pressed
|
||||||
|
//other keys should be ignored
|
||||||
|
const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing
|
||||||
|
if (isEnterPressed) {
|
||||||
|
// 1) 优先判断是否为“发送”(当前仅支持纯 Enter 发送;其余 Enter 组合键均换行)
|
||||||
|
if (isSendMessageKeyPressed(event, sendMessageShortcut)) {
|
||||||
|
sendMessage()
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 不再基于 quickPanel.isVisible 主动拦截。
|
||||||
|
// 纯 Enter 的处理权交由 QuickPanel 的全局捕获(其只在纯 Enter 时拦截),
|
||||||
|
// 其它带修饰键的 Enter 则由输入框处理为换行。
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
const start = textArea.selectionStart
|
||||||
|
const end = textArea.selectionEnd
|
||||||
|
const text = textArea.value
|
||||||
|
const newText = text.substring(0, start) + '\n' + text.substring(end)
|
||||||
|
|
||||||
|
// update text by setState, not directly modify textarea.value
|
||||||
|
setText(newText)
|
||||||
|
|
||||||
|
// set cursor position in the next render cycle
|
||||||
|
setTimeoutTimer(
|
||||||
|
'handleKeyDown',
|
||||||
|
() => {
|
||||||
|
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = useCallback(async () => {
|
||||||
|
if (inputEmpty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Starting to send message')
|
||||||
|
|
||||||
|
try {
|
||||||
|
createSessionMessage(text)
|
||||||
|
// Clear input
|
||||||
|
setText('')
|
||||||
|
setTimeoutTimer('sendMessage_1', () => setText(''), 500)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to send message:', error as Error)
|
||||||
|
}
|
||||||
|
}, [createSessionMessage, inputEmpty, setTimeoutTimer, text])
|
||||||
|
|
||||||
|
const onChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newText = e.target.value
|
||||||
|
setText(newText)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!document.querySelector('.topview-fullscreen-container')) {
|
||||||
|
focusTextarea()
|
||||||
|
}
|
||||||
|
}, [focusTextarea])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onFocus = () => {
|
||||||
|
if (document.activeElement?.closest('.ant-modal')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastFocusedComponent = PasteService.getLastFocusedComponent()
|
||||||
|
|
||||||
|
if (!lastFocusedComponent || lastFocusedComponent === 'inputbar') {
|
||||||
|
focusTextarea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('focus', onFocus)
|
||||||
|
return () => window.removeEventListener('focus', onFocus)
|
||||||
|
}, [focusTextarea])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NarrowLayout style={{ width: '100%' }}>
|
||||||
|
<Container className="inputbar">
|
||||||
|
<QuickPanelView setInputText={setText} />
|
||||||
|
<InputBarContainer
|
||||||
|
id="inputbar"
|
||||||
|
className={classNames('inputbar-container', inputFocus && 'focus')}
|
||||||
|
ref={containerRef}>
|
||||||
|
<Textarea
|
||||||
|
value={text}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })}
|
||||||
|
autoFocus
|
||||||
|
variant="borderless"
|
||||||
|
spellCheck={enableSpellCheck}
|
||||||
|
rows={2}
|
||||||
|
autoSize={{ minRows: 2, maxRows: 20 }}
|
||||||
|
ref={textareaRef}
|
||||||
|
style={{
|
||||||
|
fontSize,
|
||||||
|
minHeight: '30px'
|
||||||
|
}}
|
||||||
|
styles={{ textarea: TextareaStyle }}
|
||||||
|
onFocus={(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
setInputFocus(true)
|
||||||
|
// 记录当前聚焦的组件
|
||||||
|
PasteService.setLastFocusedComponent('inputbar')
|
||||||
|
if (e.target.value.length === 0) {
|
||||||
|
e.target.setSelectionRange(0, 0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => setInputFocus(false)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end px-1">
|
||||||
|
<SendMessageButton sendMessage={sendMessage} disabled={inputEmpty} />
|
||||||
|
</div>
|
||||||
|
</InputBarContainer>
|
||||||
|
</Container>
|
||||||
|
</NarrowLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add these styled components at the bottom
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 0 18px 18px 18px;
|
||||||
|
[navbar-position='top'] & {
|
||||||
|
padding: 0 18px 10px 18px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const InputBarContainer = styled.div`
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 17px;
|
||||||
|
padding-top: 8px; // 为拖动手柄留出空间
|
||||||
|
background-color: var(--color-background-opacity);
|
||||||
|
|
||||||
|
&.file-dragging {
|
||||||
|
border: 2px dashed #2ecc71;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(46, 204, 113, 0.03);
|
||||||
|
border-radius: 14px;
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TextareaStyle: CSSProperties = {
|
||||||
|
paddingLeft: 0,
|
||||||
|
padding: '6px 15px 0px' // 减小顶部padding
|
||||||
|
}
|
||||||
|
|
||||||
|
const Textarea = styled(TextArea)`
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
display: flex;
|
||||||
|
resize: none !important;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: none !important;
|
||||||
|
&.ant-input {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default AgentSessionInputbar
|
||||||
Loading…
Reference in New Issue
Block a user