'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 Textarea from 'rc-textarea' import s from './style.module.css' import LoadingAnim from './loading-anim' import { randomString } from '@/utils/string' import type { Feedbacktype, MessageRating, VisionFile, VisionSettings } from '@/types/app' import { TransferMethod } from '@/types/app' import Tooltip from '@/app/components/base/tooltip' import Toast from '@/app/components/base/toast' import { Markdown } from '@/app/components/base/markdown' import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader' import ImageList from '@/app/components/base/image-uploader/image-list' import { useImageFiles } from '@/app/components/base/image-uploader/hooks' import ImageGallery from '@/app/components/base/image-gallery' 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, files: VisionFile[]) => void useCurrentUserAvatar?: boolean isResponsing?: boolean controlClearQuery?: number visionConfig?: VisionSettings } 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 message_files?: VisionFile[] } 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 & { imgSrcs?: string[] } const Question: FC = ({ id, content, useCurrentUserAvatar, imgSrcs }) => { const userName = '' return (
{imgSrcs && imgSrcs.length > 0 && ( )}
{useCurrentUserAvatar ? (
{userName?.[0].toLocaleUpperCase()}
) : (
)}
) } const Chat: FC = ({ chatList, feedbackDisabled = false, isHideSendInput = false, onFeedback, checkCanSend, onSend = () => { }, useCurrentUserAvatar, isResponsing, controlClearQuery, visionConfig, }) => { 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 { files, onUpload, onRemove, onReUpload, onImageLinkLoadError, onImageLinkLoadSuccess, onClear, } = useImageFiles() const handleSend = () => { if (!valid() || (checkCanSend && !checkCanSend())) return onSend(query, files.filter(file => file.progress !== -1).map(fileItem => ({ type: 'image', transfer_method: fileItem.type, url: fileItem.url, upload_file_id: fileItem.fileId, }))) if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) { if (files.length) onClear() 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 handleKeyDown = (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 ( 0) ? item.message_files.map(item => item.url) : []} /> ) })}
{ !isHideSendInput && (
{ visionConfig?.enabled && ( <>
= visionConfig.number_limits} />
) }