'use client' import type { FC, } from 'react' import React, { useEffect, useRef } from 'react' import cn from 'classnames' import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' import s from './style.module.css' import { randomString } from '@/utils/string' import type { Feedbacktype, MessageRating } from '@/types/app' import Tooltip from '@/app/components/base/tooltip' import Toast from '@/app/components/base/toast' import AutoHeightTextarea from '@/app/components/base/auto-height-textarea' import { Markdown } from '@/app/components/base/markdown' import LoadingAnim from './loading-anim' export type FeedbackFunc = (messageId: string, feedback: Feedbacktype) => Promise export type IChatProps = { chatList: IChatItem[] /** * Whether to display the editing area and rating status */ feedbackDisabled?: boolean /** * Whether to display the input area */ isHideSendInput?: boolean onFeedback?: FeedbackFunc checkCanSend?: () => boolean onSend?: (message: string) => void useCurrentUserAvatar?: boolean isResponsing?: boolean controlClearQuery?: number controlFocus?: number } export type IChatItem = { id: string content: string /** * Specific message type */ isAnswer: boolean /** * The user feedback result of this message */ feedback?: Feedbacktype /** * Whether to hide the feedback area */ feedbackDisabled?: boolean isIntroduction?: boolean useCurrentUserAvatar?: boolean isOpeningStatement?: boolean } const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
{innerContent}
) const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => ( ) const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => { return isLike ? : } const EditIcon: FC<{ className?: string }> = ({ className }) => { return } export const EditIconSolid: FC<{ className?: string }> = ({ className }) => { return } const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => { return
{children}
} type IAnswerProps = { item: IChatItem feedbackDisabled: boolean onFeedback?: FeedbackFunc isResponsing?: boolean } // The component needs to maintain its own state to control whether to display input component const Answer: FC = ({ item, feedbackDisabled = false, onFeedback, isResponsing }) => { const { id, content, feedback } = item const { t } = useTranslation() /** * Render feedback results (distinguish between users and administrators) * User reviews cannot be cancelled in Console * @param rating feedback result * @param isUserFeedback Whether it is user's feedback * @returns comp */ const renderFeedbackRating = (rating: MessageRating | undefined) => { if (!rating) return null const isLike = rating === 'like' const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200' // The tooltip is always displayed, but the content is different for different scenarios. return (
{ await onFeedback?.(id, { rating: null }) }} >
) } /** * Different scenarios have different operation items. * @returns comp */ const renderItemOperation = () => { const userOperation = () => { return feedback?.rating ? null :
{OperationBtn({ innerContent: , onClick: () => onFeedback?.(id, { rating: 'like' }) })} {OperationBtn({ innerContent: , onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
} return (
{userOperation()}
) } return (
{isResponsing &&
}
{item.isOpeningStatement && (
{t('app.chat.openingStatementTitle')}
)} {(isResponsing && !content) ? (
) : ( )}
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()} {/* User feedback must be displayed */} {!feedbackDisabled && renderFeedbackRating(feedback?.rating)}
) } type IQuestionProps = Pick const Question: FC = ({ id, content, useCurrentUserAvatar }) => { const userName = '' return (
{useCurrentUserAvatar ? (
{userName?.[0].toLocaleUpperCase()}
) : (
)}
) } const Chat: FC = ({ chatList, feedbackDisabled = false, isHideSendInput = false, onFeedback, checkCanSend, onSend = () => { }, useCurrentUserAvatar, isResponsing, controlClearQuery, controlFocus, }) => { const { t } = useTranslation() const { notify } = Toast const isUseInputMethod = useRef(false) const [query, setQuery] = React.useState('') const handleContentChange = (e: any) => { 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: any) => { if (e.code === 'Enter') { e.preventDefault() // prevent send message when using input method enter if (!e.shiftKey && !isUseInputMethod.current) { handleSend() } } } const haneleKeyDown = (e: any) => { isUseInputMethod.current = e.nativeEvent.isComposing if (e.code === 'Enter' && !e.shiftKey) { setQuery(query.replace(/\n$/, '')) e.preventDefault() } } return (
{/* Chat List */}
{chatList.map((item) => { if (item.isAnswer) { const isLast = item.id === chatList[chatList.length - 1].id return } return })}
{ !isHideSendInput && (
{query.trim().length}
{t('common.operation.send')} Enter
{t('common.operation.lineBreak')} Shift Enter
} >
) } ) } export default React.memo(Chat)