import IconFont from '../../components/IconFont'; import { getTextWidth, groupByColumn, isMobile } from '../../utils/utils'; import { AutoComplete, Select, Tag } from 'antd'; import classNames from 'classnames'; import { debounce } from 'lodash'; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import type { ForwardRefRenderFunction } from 'react'; import { SemanticTypeEnum, SEMANTIC_TYPE_MAP, HOLDER_TAG } from '../constants'; import { AgentType, ModelType } from '../type'; import { searchRecommend } from '../../service'; import styles from './style.module.less'; import { useComposing } from '../../hooks/useComposing'; type Props = { inputMsg: string; chatId?: number; currentAgent?: AgentType; agentList: AgentType[]; onToggleHistoryVisible: () => void; onOpenAgents: () => void; onInputMsgChange: (value: string) => void; onSendMsg: (msg: string, dataSetId?: number) => void; onAddConversation: (agent?: AgentType) => void; onSelectAgent: (agent: AgentType) => void; onOpenShowcase: () => void; }; const { OptGroup, Option } = Select; let isPinyin = false; let isSelect = false; const compositionStartEvent = () => { isPinyin = true; }; const compositionEndEvent = () => { isPinyin = false; }; const ChatFooter: ForwardRefRenderFunction = ( { inputMsg, chatId, currentAgent, agentList, onToggleHistoryVisible, onOpenAgents, onInputMsgChange, onSendMsg, onAddConversation, onSelectAgent, onOpenShowcase, }, ref ) => { const [modelOptions, setModelOptions] = useState<(ModelType | AgentType)[]>([]); const [stepOptions, setStepOptions] = useState>({}); const [open, setOpen] = useState(false); const [focused, setFocused] = useState(false); const inputRef = useRef(); const fetchRef = useRef(0); const inputFocus = () => { inputRef.current?.focus(); }; const inputBlur = () => { inputRef.current?.blur(); }; useImperativeHandle(ref, () => ({ inputFocus, inputBlur, })); const initEvents = () => { const autoCompleteEl = document.getElementById('chatInput'); autoCompleteEl!.addEventListener('compositionstart', compositionStartEvent); autoCompleteEl!.addEventListener('compositionend', compositionEndEvent); }; const removeEvents = () => { const autoCompleteEl = document.getElementById('chatInput'); if (autoCompleteEl) { autoCompleteEl.removeEventListener('compositionstart', compositionStartEvent); autoCompleteEl.removeEventListener('compositionend', compositionEndEvent); } }; useEffect(() => { initEvents(); return () => { removeEvents(); }; }, []); const getStepOptions = (recommends: any[]) => { const data = groupByColumn(recommends, 'dataSetName'); return isMobile && recommends.length > 6 ? Object.keys(data) .slice(0, 4) .reduce((result, key) => { result[key] = data[key].slice( 0, Object.keys(data).length > 2 ? 2 : Object.keys(data).length > 1 ? 3 : 6 ); return result; }, {}) : data; }; const processMsg = (msg: string) => { let msgValue = msg; let dataSetId: number | undefined; if (msg?.[0] === '/') { const agent = agentList.find(item => msg.includes(`/${item.name}`)); msgValue = agent ? msg.replace(`/${agent.name}`, '') : msg; } return { msgValue, dataSetId }; }; const debounceGetWordsFunc = useCallback(() => { const getAssociateWords = async (msg: string, chatId?: number, currentAgent?: AgentType) => { if (isPinyin) { return; } if (msg === '' || (msg.length === 1 && msg[0] === '@')) { return; } fetchRef.current += 1; const fetchId = fetchRef.current; const { msgValue, dataSetId } = processMsg(msg); const res = await searchRecommend(msgValue.trim(), chatId, dataSetId, currentAgent?.id); if (fetchId !== fetchRef.current) { return; } const recommends = msgValue ? res.data || [] : []; const stepOptionList = recommends.map((item: any) => item.subRecommend); if (stepOptionList.length > 0 && stepOptionList.every((item: any) => item !== null)) { setStepOptions(getStepOptions(recommends)); } else { setStepOptions({}); } setOpen(recommends.length > 0); }; return debounce(getAssociateWords, 200); }, []); const [debounceGetWords] = useState(debounceGetWordsFunc); useEffect(() => { if (inputMsg.length === 1 && inputMsg[0] === '/') { setOpen(true); setModelOptions(agentList); setStepOptions({}); return; } if (modelOptions.length > 0) { setTimeout(() => { setModelOptions([]); }, 50); } if (!isSelect) { debounceGetWords(inputMsg, chatId, currentAgent); } else { isSelect = false; } if (!inputMsg) { setStepOptions({}); fetchRef.current = 0; } }, [inputMsg]); useEffect(() => { if (!focused) { setOpen(false); } }, [focused]); useEffect(() => { const autoCompleteDropdown = document.querySelector( `.${styles.autoCompleteDropdown}` ) as HTMLElement; if (!autoCompleteDropdown) { return; } const textWidth = getTextWidth(inputMsg); if (Object.keys(stepOptions).length > 0) { autoCompleteDropdown.style.marginLeft = `${textWidth}px`; } else { setTimeout(() => { autoCompleteDropdown.style.marginLeft = `0px`; }, 200); } }, [stepOptions]); const sendMsg = (value: string) => { const option = Object.keys(stepOptions) .reduce((result: any[], item) => { result = result.concat(stepOptions[item]); return result; }, []) .find(item => Object.keys(stepOptions).length === 1 ? item.recommend === value : `${item.dataSetName || ''}${item.recommend}` === value ); if (option && isSelect) { onSendMsg(option.recommend, option.dataSetIds); } else { onSendMsg(value.trim(), option?.dataSetId); } }; const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, { [styles.mobile]: isMobile, [styles.modelOptions]: modelOptions.length > 0, }); const onSelect = (value: string) => { isSelect = true; if (modelOptions.length > 0) { const agent = agentList.find(item => value.includes(item.name)); if (agent) { if (agent.id !== currentAgent?.id) { onSelectAgent(agent); } onInputMsgChange(''); } } else { onInputMsgChange(value.replace(HOLDER_TAG, '')); } setOpen(false); setTimeout(() => { isSelect = false; }, 200); }; const chatFooterClass = classNames(styles.chatFooter, { [styles.mobile]: isMobile, }); const modelOptionNodes = modelOptions.map(model => { return ( ); }); const associateOptionNodes = Object.keys(stepOptions).map(key => { return ( {stepOptions[key].map(option => { let optionValue = Object.keys(stepOptions).length === 1 ? option.recommend : `${option.dataSetName || ''}${option.recommend}`; if (inputMsg[0] === '/') { const agent = agentList.find(item => inputMsg.includes(item.name)); optionValue = agent ? `/${agent.name} ${option.recommend}` : optionValue; } return ( ); })} ); }); const fixWidthBug = () => { setTimeout(() => { const dropdownDom = document.querySelector( '.' + styles.autoCompleteDropdown + ' .rc-virtual-list-holder-inner' ); if (!dropdownDom) { fixWidthBug(); } else { // 获取popoverDom样式 const popoverDomStyle = window.getComputedStyle(dropdownDom); // 在获取popoverDom中增加样式 width: fit-content dropdownDom.setAttribute('style', `${popoverDomStyle.cssText};width: fit-content`); // 获取popoverDom的宽度 const popoverDomWidth = dropdownDom.clientWidth; // 将popoverDom的宽度赋值给他的父元素 const offset = 20; // 预增加20px的宽度,预留空间给虚拟渲染出来的元素 dropdownDom.parentElement!.style.width = popoverDomWidth + offset + 'px'; } }); }; useEffect(() => { if (modelOptionNodes.length || associateOptionNodes.length) fixWidthBug(); }, [modelOptionNodes.length, associateOptionNodes.length]); const { isComposing } = useComposing(document.getElementById('chatInput')); return (
{ onAddConversation(); }} >
新对话
{!isMobile && (
历史对话
)} {agentList?.length > 1 && (
智能助理
)}
showcase
{ onInputMsgChange(value); }} onSelect={onSelect} autoFocus={!isMobile} ref={inputRef} id="chatInput" onKeyDown={e => { if (e.code === 'Enter' || e.code === 'NumpadEnter') { const chatInputEl: any = document.getElementById('chatInput'); const agent = agentList.find( item => chatInputEl.value[0] === '/' && chatInputEl.value.includes(item.name) ); if (agent) { if (agent.id !== currentAgent?.id) { onSelectAgent(agent); } onInputMsgChange(''); return; } if (!isSelect && !isComposing) { sendMsg(chatInputEl.value); setOpen(false); } } }} onFocus={() => { setFocused(true); }} onBlur={() => { setFocused(false); }} dropdownClassName={autoCompleteDropdownClass} listHeight={500} allowClear open={open} defaultActiveFirstOption={false} getPopupContainer={triggerNode => triggerNode.parentNode} > {modelOptions.length > 0 ? modelOptionNodes : associateOptionNodes}
0, })} onClick={() => { sendMsg(inputMsg); }} >
); }; export default forwardRef(ChatFooter);