You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

operation.tsx 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import type { FC } from 'react'
  2. import {
  3. memo,
  4. useMemo,
  5. useState,
  6. } from 'react'
  7. import { useTranslation } from 'react-i18next'
  8. import type { ChatItem } from '../../types'
  9. import { useChatContext } from '../context'
  10. import RegenerateBtn from '@/app/components/base/regenerate-btn'
  11. import cn from '@/utils/classnames'
  12. import CopyBtn from '@/app/components/base/copy-btn'
  13. import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
  14. import AudioBtn from '@/app/components/base/audio-btn'
  15. import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
  16. import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
  17. import {
  18. ThumbsDown,
  19. ThumbsUp,
  20. } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
  21. import Tooltip from '@/app/components/base/tooltip'
  22. import Log from '@/app/components/base/chat/chat/log'
  23. type OperationProps = {
  24. item: ChatItem
  25. question: string
  26. index: number
  27. showPromptLog?: boolean
  28. maxSize: number
  29. contentWidth: number
  30. hasWorkflowProcess: boolean
  31. noChatInput?: boolean
  32. }
  33. const Operation: FC<OperationProps> = ({
  34. item,
  35. question,
  36. index,
  37. showPromptLog,
  38. maxSize,
  39. contentWidth,
  40. hasWorkflowProcess,
  41. noChatInput,
  42. }) => {
  43. const { t } = useTranslation()
  44. const {
  45. config,
  46. onAnnotationAdded,
  47. onAnnotationEdited,
  48. onAnnotationRemoved,
  49. onFeedback,
  50. onRegenerate,
  51. } = useChatContext()
  52. const [isShowReplyModal, setIsShowReplyModal] = useState(false)
  53. const {
  54. id,
  55. isOpeningStatement,
  56. content: messageContent,
  57. annotation,
  58. feedback,
  59. adminFeedback,
  60. agent_thoughts,
  61. } = item
  62. const hasAnnotation = !!annotation?.id
  63. const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback)
  64. const content = useMemo(() => {
  65. if (agent_thoughts?.length)
  66. return agent_thoughts.reduce((acc, cur) => acc + cur.thought, '')
  67. return messageContent
  68. }, [agent_thoughts, messageContent])
  69. const handleFeedback = async (rating: 'like' | 'dislike' | null) => {
  70. if (!config?.supportFeedback || !onFeedback)
  71. return
  72. await onFeedback?.(id, { rating })
  73. setLocalFeedback({ rating })
  74. }
  75. const operationWidth = useMemo(() => {
  76. let width = 0
  77. if (!isOpeningStatement)
  78. width += 28
  79. if (!isOpeningStatement && showPromptLog)
  80. width += 102 + 8
  81. if (!isOpeningStatement && config?.text_to_speech?.enabled)
  82. width += 33
  83. if (!isOpeningStatement && config?.supportAnnotation && config?.annotation_reply?.enabled)
  84. width += 56 + 8
  85. if (config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement)
  86. width += 60 + 8
  87. if (config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement)
  88. width += 28 + 8
  89. return width
  90. }, [isOpeningStatement, showPromptLog, config?.text_to_speech?.enabled, config?.supportAnnotation, config?.annotation_reply?.enabled, config?.supportFeedback, localFeedback?.rating, onFeedback])
  91. const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize])
  92. return (
  93. <>
  94. <div
  95. className={cn(
  96. 'absolute flex justify-end gap-1',
  97. hasWorkflowProcess && '-top-3.5 -right-3.5',
  98. !positionRight && '-top-3.5 -right-3.5',
  99. !hasWorkflowProcess && positionRight && '!top-[9px]',
  100. )}
  101. style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
  102. >
  103. {!isOpeningStatement && (
  104. <CopyBtn
  105. value={content}
  106. className='hidden group-hover:block'
  107. />
  108. )}
  109. {!isOpeningStatement && (showPromptLog || config?.text_to_speech?.enabled) && (
  110. <div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
  111. {showPromptLog && (
  112. <>
  113. <Log logItem={item} />
  114. <div className='mx-1 w-[1px] h-[14px] bg-gray-200' />
  115. </>
  116. )}
  117. {(config?.text_to_speech?.enabled) && (
  118. <>
  119. <AudioBtn
  120. id={id}
  121. value={content}
  122. noCache={false}
  123. voice={config?.text_to_speech?.voice}
  124. className='hidden group-hover:block'
  125. />
  126. </>
  127. )}
  128. </div>
  129. )}
  130. {(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
  131. <AnnotationCtrlBtn
  132. appId={config?.appId || ''}
  133. messageId={id}
  134. annotationId={annotation?.id || ''}
  135. className='hidden group-hover:block ml-1 shrink-0'
  136. cached={hasAnnotation}
  137. query={question}
  138. answer={content}
  139. onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
  140. onEdit={() => setIsShowReplyModal(true)}
  141. onRemoved={() => onAnnotationRemoved?.(index)}
  142. />
  143. )}
  144. {
  145. annotation?.id && (
  146. <div
  147. className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
  148. >
  149. <div className='p-1 rounded-lg bg-[#EEF4FF] '>
  150. <MessageFast className='w-4 h-4' />
  151. </div>
  152. </div>
  153. )
  154. }
  155. {
  156. !isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
  157. }
  158. {
  159. config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
  160. <div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
  161. <Tooltip popupContent={t('appDebug.operation.agree')}>
  162. <div
  163. className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
  164. onClick={() => handleFeedback('like')}
  165. >
  166. <ThumbsUp className='w-4 h-4' />
  167. </div>
  168. </Tooltip>
  169. <Tooltip
  170. popupContent={t('appDebug.operation.disagree')}
  171. >
  172. <div
  173. className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
  174. onClick={() => handleFeedback('dislike')}
  175. >
  176. <ThumbsDown className='w-4 h-4' />
  177. </div>
  178. </Tooltip>
  179. </div>
  180. )
  181. }
  182. {
  183. config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
  184. <Tooltip
  185. popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}
  186. >
  187. <div
  188. className={`
  189. flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
  190. ${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
  191. ${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
  192. `}
  193. onClick={() => handleFeedback(null)}
  194. >
  195. {
  196. localFeedback.rating === 'like' && (
  197. <ThumbsUp className='w-4 h-4' />
  198. )
  199. }
  200. {
  201. localFeedback.rating === 'dislike' && (
  202. <ThumbsDown className='w-4 h-4' />
  203. )
  204. }
  205. </div>
  206. </Tooltip>
  207. )
  208. }
  209. </div>
  210. <EditReplyModal
  211. isShow={isShowReplyModal}
  212. onHide={() => setIsShowReplyModal(false)}
  213. query={question}
  214. answer={content}
  215. onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)}
  216. onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)}
  217. appId={config?.appId || ''}
  218. messageId={id}
  219. annotationId={annotation?.id || ''}
  220. createdAt={annotation?.created_at}
  221. onRemove={() => onAnnotationRemoved?.(index)}
  222. />
  223. </>
  224. )
  225. }
  226. export default memo(Operation)