'use client' import type { FC } from 'react' import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import { useContext } from 'use-context-selector' import cn from 'classnames' import Recorder from 'js-audio-recorder' import { useTranslation } from 'react-i18next' import s from './style.module.css' import type { DisplayScene, FeedbackFunc, IChatItem, SubmitAnnotationFunc } from './type' import { TryToAskIcon, stopIcon } from './icon-component' import Answer from './answer' import Question from './question' import Tooltip from '@/app/components/base/tooltip' import { ToastContext } from '@/app/components/base/toast' import AutoHeightTextarea from '@/app/components/base/auto-height-textarea' import Button from '@/app/components/base/button' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import VoiceInput from '@/app/components/base/voice-input' import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { XCircle } from '@/app/components/base/icons/src/vender/solid/general' import type { DataSet } from '@/models/datasets' export type IChatProps = { configElem?: React.ReactNode chatList: IChatItem[] controlChatUpdateAllConversation?: number /** * Whether to display the editing area and rating status */ feedbackDisabled?: boolean /** * Whether to display the input area */ isHideFeedbackEdit?: boolean isHideSendInput?: boolean onFeedback?: FeedbackFunc onSubmitAnnotation?: SubmitAnnotationFunc checkCanSend?: () => boolean onSend?: (message: string) => void displayScene?: DisplayScene useCurrentUserAvatar?: boolean isResponsing?: boolean canStopResponsing?: boolean abortResponsing?: () => void controlClearQuery?: number controlFocus?: number isShowSuggestion?: boolean suggestionList?: string[] isShowSpeechToText?: boolean isShowCitation?: boolean answerIconClassName?: string isShowConfigElem?: boolean dataSets?: DataSet[] isShowCitationHitInfo?: boolean } const Chat: FC = ({ configElem, chatList, feedbackDisabled = false, isHideFeedbackEdit = false, isHideSendInput = false, onFeedback, onSubmitAnnotation, checkCanSend, onSend = () => { }, displayScene, useCurrentUserAvatar, isResponsing, canStopResponsing, abortResponsing, controlClearQuery, controlFocus, isShowSuggestion, suggestionList, isShowSpeechToText, isShowCitation, answerIconClassName, isShowConfigElem, dataSets, isShowCitationHitInfo, }) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const isUseInputMethod = useRef(false) const [query, setQuery] = React.useState('') const handleContentChange = (e: React.ChangeEvent) => { const value = e.target.value setQuery(value) } const logError = (message: string) => { notify({ type: 'error', message, duration: 3000 }) } const valid = () => { if (!query || query.trim() === '') { logError('Message cannot be empty') return false } return true } useEffect(() => { if (controlClearQuery) setQuery('') }, [controlClearQuery]) const handleSend = () => { if (!valid() || (checkCanSend && !checkCanSend())) return onSend(query) if (!isResponsing) setQuery('') } const handleKeyUp = (e: React.KeyboardEvent) => { if (e.code === 'Enter') { e.preventDefault() // prevent send message when using input method enter if (!e.shiftKey && !isUseInputMethod.current) handleSend() } } const handleKeyDown = (e: React.KeyboardEvent) => { isUseInputMethod.current = e.nativeEvent.isComposing if (e.code === 'Enter' && !e.shiftKey) { setQuery(query.replace(/\n$/, '')) e.preventDefault() } } const media = useBreakpoints() const isMobile = media === MediaType.mobile const sendBtn =
const suggestionListRef = useRef(null) const [hasScrollbar, setHasScrollbar] = useState(false) useLayoutEffect(() => { if (suggestionListRef.current) { const listDom = suggestionListRef.current const hasScrollbar = listDom.scrollWidth > listDom.clientWidth setHasScrollbar(hasScrollbar) } }, [suggestionList]) const [voiceInputShow, setVoiceInputShow] = useState(false) const handleVoiceInputShow = () => { (Recorder as any).getPermission().then(() => { setVoiceInputShow(true) }, () => { logError(t('common.voiceInput.notAllow')) }) } return (
{isShowConfigElem && (configElem || null)} {/* Chat List */}
{chatList.map((item) => { if (item.isAnswer) { const isLast = item.id === chatList[chatList.length - 1].id const thoughts = item.agent_thoughts?.filter(item => item.thought !== '[DONE]') const citation = item.citation const isThinking = !item.content && item.agent_thoughts && item.agent_thoughts?.length > 0 && !item.agent_thoughts.some(item => item.thought === '[DONE]') return } return })}
{ !isHideSendInput && (
{/* Thinking is sync and can not be stopped */} {(isResponsing && canStopResponsing && !!chatList[chatList.length - 1]?.content) && (
)} { isShowSuggestion && (
{TryToAskIcon} {t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}
{/* has scrollbar would hide part of first item */}
{suggestionList?.map((item, index) => (
))}
) }
{query.trim().length}
{ query ? (
setQuery('')}>
) : isShowSpeechToText ? (
) : null }
{isMobile ? sendBtn : (
{t('common.operation.send')} Enter
{t('common.operation.lineBreak')} Shift Enter
} > {sendBtn} )}
{ voiceInputShow && ( setVoiceInputShow(false)} onConverted={text => setQuery(text)} /> ) }
) }
) } export default React.memo(Chat)