mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +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 ChatNavbar from './ChatNavbar'
|
||||
import AgentSessionInputbar from './Inputbar/AgentSessionInputbar'
|
||||
import Inputbar from './Inputbar/Inputbar'
|
||||
import AgentSessionMessages from './Messages/AgentSessionMessages'
|
||||
import ChatNavigation from './Messages/ChatNavigation'
|
||||
@ -151,6 +152,17 @@ const Chat: FC<Props> = (props) => {
|
||||
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 (
|
||||
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
|
||||
{isTopNavbar && (
|
||||
@ -192,7 +204,12 @@ const Chat: FC<Props> = (props) => {
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||
</>
|
||||
)}
|
||||
{activeTopicOrSession === 'session' && <SessionMessages />}
|
||||
{activeTopicOrSession === 'session' && (
|
||||
<>
|
||||
<SessionMessages />
|
||||
<SessionInputBar />
|
||||
</>
|
||||
)}
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</QuickPanelProvider>
|
||||
</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