import { ClearOutlined, CodeOutlined, FileSearchOutlined, FormOutlined, FullscreenExitOutlined, FullscreenOutlined, GlobalOutlined, HolderOutlined, PaperClipOutlined, PauseCircleOutlined, QuestionCircleOutlined, ThunderboltOutlined, TranslationOutlined } from '@ant-design/icons' import ASRButton from '@renderer/components/ASRButton' import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' import TranslateButton from '@renderer/components/TranslateButton' import VoiceCallButton from '@renderer/components/VoiceCallButton' import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import FileManager from '@renderer/services/FileManager' import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService' import { getModelUniqId } from '@renderer/services/ModelService' import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService' import { translateText } from '@renderer/services/TranslateService' import WebSearchService from '@renderer/services/WebSearchService' import { useAppDispatch } from '@renderer/store' import { sendMessage as _sendMessage } from '@renderer/store/messages' import { setSearching } from '@renderer/store/runtime' import { Assistant, FileType, KnowledgeBase, KnowledgeItem, MCPServer, Message, Model, Topic } from '@renderer/types' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { getFilesFromDropEvent } from '@renderer/utils/input' 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 Logger from 'electron-log/renderer' import { debounce, isEmpty } from 'lodash' import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' import NarrowLayout from '../Messages/NarrowLayout' import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton' import AttachmentPreview from './AttachmentPreview' import GenerateImageButton from './GenerateImageButton' import KnowledgeBaseButton, { KnowledgeBaseButtonRef } from './KnowledgeBaseButton' import KnowledgeBaseInput from './KnowledgeBaseInput' import MCPToolsButton, { MCPToolsButtonRef } from './MCPToolsButton' import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButton' import MentionModelsInput from './MentionModelsInput' import NewContextButton from './NewContextButton' import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton' import SendMessageButton from './SendMessageButton' import TokenCount from './TokenCount' interface Props { assistant: Assistant setActiveTopic: (topic: Topic) => void topic: Topic } let _text = '' let _files: FileType[] = [] const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) => { const [text, setText] = useState(_text) const [inputFocus, setInputFocus] = useState(false) const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id) const { targetLanguage, sendMessageShortcut, fontSize, pasteLongTextAsFile, pasteLongTextThreshold, showInputEstimatedTokens, autoTranslateWithSpace, enableQuickPanelTriggers } = useSettings() const [expended, setExpend] = useState(false) const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [contextCount, setContextCount] = useState({ current: 0, max: 0 }) const textareaRef = useRef(null) const [files, setFiles] = useState(_files) const { t } = useTranslation() const containerRef = useRef(null) const { searching } = useRuntime() const { isBubbleStyle } = useMessageStyle() const { pauseMessages } = useMessageOperations(topic) const loading = useTopicLoading(topic) const dispatch = useAppDispatch() const [spaceClickCount, setSpaceClickCount] = useState(0) const spaceClickTimer = useRef(null) const [isTranslating, setIsTranslating] = useState(false) const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([]) const [mentionModels, setMentionModels] = useState([]) const [enabledMCPs, setEnabledMCPs] = useState(assistant.mcpServers || []) const [isDragging, setIsDragging] = useState(false) const [textareaHeight, setTextareaHeight] = useState() const startDragY = useRef(0) const startHeight = useRef(0) const currentMessageId = useRef('') const isVision = useMemo(() => isVisionModel(model), [model]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) const navigate = useNavigate() const { activedMcpServers } = useMCPServers() const { bases: knowledgeBases } = useKnowledgeBases() const quickPanel = useQuickPanel() const showKnowledgeIcon = useSidebarIconShow('knowledge') // const showMCPToolsIcon = isFunctionCallingModel(model) const [tokenCount, setTokenCount] = useState(0) const quickPhrasesButtonRef = useRef(null) const mentionModelsButtonRef = useRef(null) const knowledgeBaseButtonRef = useRef(null) const mcpToolsButtonRef = useRef(null) const attachmentButtonRef = useRef(null) // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedEstimate = useCallback( debounce((newText) => { if (showInputEstimatedTokens) { const count = estimateTxtTokens(newText) || 0 setTokenCount(count) } }, 500), [showInputEstimatedTokens] ) useEffect(() => { debouncedEstimate(text) }, [text, debouncedEstimate]) const inputTokenCount = showInputEstimatedTokens ? tokenCount : 0 const newTopicShortcut = useShortcutDisplay('new_topic') const cleanTopicShortcut = useShortcutDisplay('clear_topic') const inputEmpty = isEmpty(text.trim()) && files.length === 0 _text = text _files = files const resizeTextArea = useCallback(() => { const textArea = textareaRef.current?.resizableTextArea?.textArea if (textArea) { // 如果已经手动设置了高度,则不自动调整 if (textareaHeight) { return } textArea.style.height = 'auto' textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px` } }, [textareaHeight]) // Reset to assistant knowledge mcp servers useEffect(() => { setEnabledMCPs(assistant.mcpServers || []) }, [assistant.mcpServers]) const sendMessage = useCallback(async () => { if (inputEmpty || loading) { return } if (checkRateLimit(assistant)) { return } EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE) try { // Dispatch the sendMessage action with all options const uploadedFiles = await FileManager.uploadFiles(files) const userMessage = getUserMessage({ assistant, topic, type: 'text', content: text }) if (uploadedFiles) { userMessage.files = uploadedFiles } const knowledgeBaseIds = selectedKnowledgeBases?.map((base) => base.id) if (knowledgeBaseIds) { userMessage.knowledgeBaseIds = knowledgeBaseIds } if (mentionModels) { userMessage.mentions = mentionModels } if (!isEmpty(enabledMCPs) && !isEmpty(activedMcpServers)) { userMessage.enabledMCPs = activedMcpServers.filter((server) => enabledMCPs?.some((s) => s.id === server.id)) } userMessage.usage = await estimateMessageUsage(userMessage) currentMessageId.current = userMessage.id dispatch( _sendMessage(userMessage, assistant, topic, { mentions: mentionModels }) ) // Clear input setText('') setFiles([]) setTimeout(() => setText(''), 500) setTimeout(() => resizeTextArea(), 0) setExpend(false) } catch (error) { console.error('Failed to send message:', error) } }, [ assistant, dispatch, enabledMCPs, files, inputEmpty, loading, mentionModels, resizeTextArea, selectedKnowledgeBases, text, topic, activedMcpServers ]) const translate = useCallback(async () => { if (isTranslating) { return } try { setIsTranslating(true) const translatedText = await translateText(text, targetLanguage) translatedText && setText(translatedText) setTimeout(() => resizeTextArea(), 0) } catch (error) { console.error('Translation failed:', error) } finally { setIsTranslating(false) } }, [isTranslating, text, targetLanguage, resizeTextArea]) const openKnowledgeFileList = useCallback( (base: KnowledgeBase) => { quickPanel.open({ title: base.name, list: base.items .filter((file): file is KnowledgeItem => ['file'].includes(file.type)) .map((file) => { const fileContent = file.content as FileType return { label: fileContent.origin_name || fileContent.name, description: formatFileSize(fileContent.size) + ' · ' + dayjs(fileContent.created_at).format('YYYY-MM-DD HH:mm'), icon: , isSelected: files.some((f) => f.path === fileContent.path), action: async ({ item }) => { item.isSelected = !item.isSelected if (fileContent.path) { setFiles((prevFiles) => { const fileExists = prevFiles.some((f) => f.path === fileContent.path) if (fileExists) { return prevFiles.filter((f) => f.path !== fileContent.path) } else { return fileContent ? [...prevFiles, fileContent] : prevFiles } }) } } } }), symbol: 'file', multiple: true }) }, [files, quickPanel] ) const openSelectFileMenu = useCallback(() => { quickPanel.open({ title: t('chat.input.upload'), list: [ { label: t('chat.input.upload.upload_from_local'), description: '', icon: , action: () => { attachmentButtonRef.current?.openQuickPanel() } }, ...knowledgeBases.map((base) => { const length = base.items?.filter( (item): item is KnowledgeItem => ['file', 'note'].includes(item.type) && typeof item.content !== 'string' ).length return { label: base.name, description: `${length} ${t('files.count')}`, icon: , disabled: length === 0, isMenu: true, action: () => openKnowledgeFileList(base) } }) ], symbol: 'file' }) }, [knowledgeBases, openKnowledgeFileList, quickPanel, t]) const quickPanelMenu = useMemo(() => { return [ { label: t('settings.quickPhrase.title'), description: '', icon: , isMenu: true, action: () => { quickPhrasesButtonRef.current?.openQuickPanel() } }, { label: t('agents.edit.model.select.title'), description: '', icon: '@', isMenu: true, action: () => { mentionModelsButtonRef.current?.openQuickPanel() } }, { label: t('chat.input.knowledge_base'), description: '', icon: , isMenu: true, disabled: files.length > 0, action: () => { knowledgeBaseButtonRef.current?.openQuickPanel() } }, { label: t('settings.mcp.title'), description: t('settings.mcp.not_support'), icon: , isMenu: true, action: () => { mcpToolsButtonRef.current?.openQuickPanel() } }, { label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'), description: '', icon: , isMenu: true, action: openSelectFileMenu }, { label: t('translate.title'), description: t('translate.menu.description'), icon: , action: () => { if (!text) return translate() } } ] }, [files.length, model, openSelectFileMenu, t, text, translate]) const handleKeyDown = (event: React.KeyboardEvent) => { const isEnterPressed = event.keyCode == 13 // 按下Tab键,自动选中${xxx} if (event.key === 'Tab' && inputFocus) { event.preventDefault() const textArea = textareaRef.current?.resizableTextArea?.textArea if (!textArea) return const cursorPosition = textArea.selectionStart const selectionLength = textArea.selectionEnd - textArea.selectionStart const text = textArea.value let match = text.slice(cursorPosition + selectionLength).match(/\$\{[^}]+\}/) let startIndex = -1 if (!match) { match = text.match(/\$\{[^}]+\}/) startIndex = match?.index ?? -1 } else { startIndex = cursorPosition + selectionLength + match.index! } if (startIndex !== -1) { const endIndex = startIndex + match![0].length textArea.setSelectionRange(startIndex, endIndex) return } } 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 onToggleExpended() } } if (isEnterPressed && !event.shiftKey && sendMessageShortcut === 'Enter') { if (quickPanel.isVisible) return event.preventDefault() sendMessage() return event.preventDefault() } if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) { if (quickPanel.isVisible) return event.preventDefault() sendMessage() return event.preventDefault() } if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) { if (quickPanel.isVisible) return event.preventDefault() sendMessage() return event.preventDefault() } if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) { if (quickPanel.isVisible) return event.preventDefault() sendMessage() return event.preventDefault() } if (event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 0) { setMentionModels((prev) => prev.slice(0, -1)) return event.preventDefault() } if (event.key === 'Backspace' && text.trim() === '' && selectedKnowledgeBases.length > 0) { setSelectedKnowledgeBases((prev) => { const newSelectedKnowledgeBases = prev.slice(0, -1) updateAssistant({ ...assistant, knowledge_bases: newSelectedKnowledgeBases }) return newSelectedKnowledgeBases }) return event.preventDefault() } if (event.key === 'Backspace' && text.trim() === '' && files.length > 0) { setFiles((prev) => prev.slice(0, -1)) return event.preventDefault() } } const addNewTopic = useCallback(async () => { await modelGenerating() const topic = getDefaultTopic(assistant.id) await db.topics.add({ id: topic.id, messages: [] }) await addAssistantMessagesToTopic({ assistant, topic }) // Clear previous state // Reset to assistant default model assistant.defaultModel && setModel(assistant.defaultModel) // Reset to assistant knowledge mcp servers !isEmpty(assistant.mcpServers) && setEnabledMCPs(assistant.mcpServers || []) addTopic(topic) setActiveTopic(topic) setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) }, [addTopic, assistant, setActiveTopic, setModel]) const onPause = async () => { await pauseMessages() } const clearTopic = async () => { if (loading) { await onPause() await delay(1) } EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES) } const onNewContext = () => { if (loading) { onPause() return } EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT) } const onInput = () => !expended && resizeTextArea() const onChange = (e: React.ChangeEvent) => { const newText = e.target.value setText(newText) const textArea = textareaRef.current?.resizableTextArea?.textArea const cursorPosition = textArea?.selectionStart ?? 0 const lastSymbol = newText[cursorPosition - 1] if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') { quickPanel.open({ title: t('settings.quickPanel.title'), list: quickPanelMenu, symbol: '/' }) } if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') { mentionModelsButtonRef.current?.openQuickPanel() } } const onPaste = useCallback( async (event: ClipboardEvent) => { const clipboardText = event.clipboardData?.getData('text') if (clipboardText) { // Prioritize the text when pasting. // handled by the default event } else { for (const file of event.clipboardData?.files || []) { event.preventDefault() if (file.path === '') { if (file.type.startsWith('image/') && isVisionModel(model)) { 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 } else { window.message.info({ key: 'file_not_supported', content: t('chat.input.file_not_supported') }) } } if (file.path) { if (supportExts.includes(getFileExtension(file.path))) { const selectedFile = await window.api.file.get(file.path) selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) } else { window.message.info({ key: 'file_not_supported', content: t('chat.input.file_not_supported') }) } } } } 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(), 50) } }) } } }, [model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t, text] ) const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() } const handleDrop = async (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() const files = await getFilesFromDropEvent(e).catch((err) => { Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] handleDrop:', err) return null }) if (files) { files.forEach((file) => { if (supportExts.includes(getFileExtension(file.path))) { setFiles((prevFiles) => [...prevFiles, file]) } }) } } const onTranslated = (translatedText: string) => { setText(translatedText) setTimeout(() => resizeTextArea(), 0) } const handleDragStart = (e: React.MouseEvent) => { e.preventDefault() setIsDragging(true) startDragY.current = e.clientY const textArea = textareaRef.current?.resizableTextArea?.textArea if (textArea) { startHeight.current = textArea.offsetHeight } } const handleDrag = useCallback( (e: MouseEvent) => { if (!isDragging) return const delta = startDragY.current - e.clientY // 改变计算方向 const viewportHeight = window.innerHeight const maxHeightInPixels = viewportHeight * 0.7 const newHeight = Math.min(maxHeightInPixels, Math.max(startHeight.current + delta, 30)) const textArea = textareaRef.current?.resizableTextArea?.textArea if (textArea) { textArea.style.height = `${newHeight}px` setExpend(newHeight == maxHeightInPixels) setTextareaHeight(newHeight) } }, [isDragging] ) const handleDragEnd = useCallback(() => { setIsDragging(false) }, []) useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleDrag) document.addEventListener('mouseup', handleDragEnd) } return () => { document.removeEventListener('mousemove', handleDrag) document.removeEventListener('mouseup', handleDragEnd) } }, [isDragging, handleDrag, handleDragEnd]) useShortcut('new_topic', () => { addNewTopic() EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR) textareaRef.current?.focus() }) useShortcut('clear_topic', () => { clearTopic() }) useEffect(() => { const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const unsubscribes = [ EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => { setText(message.content) textareaRef.current?.focus() setTimeout(() => resizeTextArea(), 0) }), EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => { _setEstimateTokenCount(tokensCount) setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值 }), EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic), EventEmitter.on(EVENT_NAMES.QUOTE_TEXT, (quotedText: string) => { setText((prevText) => { const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n` setTimeout(() => resizeTextArea(), 0) return newText }) textareaRef.current?.focus() }), // 监听语音通话消息 EventEmitter.on(EVENT_NAMES.VOICE_CALL_MESSAGE, (data: { text: string, model: string }) => { console.log('收到语音通话消息:', data); // 先设置输入框文本 setText(data.text); // 如果有指定模型,切换到该模型 if (data.model) { // 查找对应的模型对象 const modelObj = assistant.model?.id === data.model ? assistant.model : undefined; if (modelObj) { setModel(modelObj); } } // 使用延时确保文本已经设置到输入框 setTimeout(() => { // 直接调用发送消息函数,而不检查inputEmpty console.log('准备自动发送语音识别消息:', data.text); // 直接使用正确的方式发送消息 // 创建用户消息 const userMessage = getUserMessage({ assistant, topic, type: 'text', content: data.text }); // 如果有指定模型,设置模型 if (data.model) { // 查找对应的模型对象 const modelObj = assistant.model?.id === data.model ? assistant.model : undefined; if (modelObj) { userMessage.model = modelObj; } } // 分发发送消息的action dispatch( _sendMessage(userMessage, assistant, topic, {}) ); // 清空输入框 setText(''); console.log('已触发发送消息事件'); }, 300); }) ] return () => unsubscribes.forEach((unsub) => unsub()) }, [addNewTopic, resizeTextArea, sendMessage, model, inputEmpty, loading, dispatch, assistant, topic, setText, getUserMessage, _sendMessage]) useEffect(() => { textareaRef.current?.focus() }, [assistant]) useEffect(() => { setTimeout(() => resizeTextArea(), 0) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { return () => { if (spaceClickTimer.current) { clearTimeout(spaceClickTimer.current) } } }, []) useEffect(() => { window.addEventListener('focus', () => { textareaRef.current?.focus() }) }, []) useEffect(() => { // if assistant knowledge bases are undefined return [] setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : []) }, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon]) const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1 const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => { updateAssistant({ ...assistant, knowledge_bases: bases }) setSelectedKnowledgeBases(bases ?? []) } const handleRemoveModel = (model: Model) => { setMentionModels(mentionModels.filter((m) => m.id !== model.id)) } const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => { const newKnowledgeBases = assistant.knowledge_bases?.filter((kb) => kb.id !== knowledgeBase.id) updateAssistant({ ...assistant, knowledge_bases: newKnowledgeBases }) setSelectedKnowledgeBases(newKnowledgeBases ?? []) } const toggelEnableMCP = (mcp: MCPServer) => { setEnabledMCPs((prev) => { const exists = prev.some((item) => item.id === mcp.id) if (exists) { return prev.filter((item) => item.id !== mcp.id) } else { return [...prev, mcp] } }) } const showWebSearchEnableModal = () => { window.modal.confirm({ title: t('chat.input.web_search.enable'), content: t('chat.input.web_search.enable_content'), centered: true, okText: t('chat.input.web_search.button.ok'), onOk: () => { navigate('/settings/web-search') } }) } const shouldShowEnableModal = () => { // 网络搜索功能是否未启用 const webSearchNotEnabled = !WebSearchService.isWebSearchEnabled() // 非网络搜索模型:仅当网络搜索功能未启用时显示启用提示 if (!isWebSearchModel(model)) { return webSearchNotEnabled } // 网络搜索模型:当允许覆盖但网络搜索功能未启用时显示启用提示 return WebSearchService.isOverwriteEnabled() && webSearchNotEnabled } const onEnableWebSearch = () => { if (shouldShowEnableModal()) { showWebSearchEnableModal() return } updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch }) } const onEnableGenerateImage = () => { updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage }) } useEffect(() => { if (!isWebSearchModel(model) && !WebSearchService.isWebSearchEnabled() && assistant.enableWebSearch) { updateAssistant({ ...assistant, enableWebSearch: false }) } if (!isGenerateImageModel(model) && assistant.enableGenerateImage) { updateAssistant({ ...assistant, enableGenerateImage: false }) } }, [assistant, model, updateAssistant]) const onMentionModel = (model: Model) => { setMentionModels((prev) => { const modelId = getModelUniqId(model) const exists = prev.some((m) => getModelUniqId(m) === modelId) return exists ? prev.filter((m) => getModelUniqId(m) !== modelId) : [...prev, model] }) } const onToggleExpended = () => { if (textareaHeight) { const textArea = textareaRef.current?.resizableTextArea?.textArea if (textArea) { textArea.style.height = 'auto' setTextareaHeight(undefined) setTimeout(() => { textArea.style.height = `${textArea.scrollHeight}px` }, 200) return } } const isExpended = !expended setExpend(isExpended) const textArea = textareaRef.current?.resizableTextArea?.textArea if (textArea) { if (isExpended) { textArea.style.height = '70vh' } else { resetHeight() } } textareaRef.current?.focus() } const resetHeight = () => { if (expended) { setExpend(false) } setTextareaHeight(undefined) requestAnimationFrame(() => { const textArea = textareaRef.current?.resizableTextArea?.textArea if (textArea) { textArea.style.height = 'auto' const contentHeight = textArea.scrollHeight textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px` } }) } const isExpended = expended || !!textareaHeight return ( {files.length > 0 && } {selectedKnowledgeBases.length > 0 && ( )} {mentionModels.length > 0 && ( )}