diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index ae0c556483..c6ab29ee3a 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -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) => { return } + const SessionInputBar = () => { + if (activeAgentId === null) { + return
Active Agent ID is invalid.
+ } + const sessionId = activeSessionId[activeAgentId] + if (!sessionId) { + return
Active Session ID is invalid.
+ } + return + } + return ( {isTopNavbar && ( @@ -192,7 +204,12 @@ const Chat: FC = (props) => { )} - {activeTopicOrSession === 'session' && } + {activeTopicOrSession === 'session' && ( + <> + + + + )} {isMultiSelectMode && } diff --git a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx new file mode 100644 index 0000000000..2b864ef1d6 --- /dev/null +++ b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx @@ -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 = ({ 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(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) => { + //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) => { + 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 ( + + + + +