index.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import type {
  2. FC,
  3. ReactNode,
  4. } from 'react'
  5. import { memo, useEffect, useRef, useState } from 'react'
  6. import { useTranslation } from 'react-i18next'
  7. import type {
  8. ChatConfig,
  9. ChatItem,
  10. } from '../../types'
  11. import Operation from './operation'
  12. import AgentContent from './agent-content'
  13. import BasicContent from './basic-content'
  14. import SuggestedQuestions from './suggested-questions'
  15. import More from './more'
  16. import WorkflowProcess from './workflow-process'
  17. import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
  18. import Citation from '@/app/components/base/chat/chat/citation'
  19. import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
  20. import type { AppData } from '@/models/share'
  21. import AnswerIcon from '@/app/components/base/answer-icon'
  22. import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
  23. import cn from '@/utils/classnames'
  24. import { FileList } from '@/app/components/base/file-uploader'
  25. type AnswerProps = {
  26. item: ChatItem
  27. question: string
  28. index: number
  29. config?: ChatConfig
  30. answerIcon?: ReactNode
  31. responding?: boolean
  32. showPromptLog?: boolean
  33. chatAnswerContainerInner?: string
  34. hideProcessDetail?: boolean
  35. appData?: AppData
  36. noChatInput?: boolean
  37. switchSibling?: (siblingMessageId: string) => void
  38. }
  39. const Answer: FC<AnswerProps> = ({
  40. item,
  41. question,
  42. index,
  43. config,
  44. answerIcon,
  45. responding,
  46. showPromptLog,
  47. chatAnswerContainerInner,
  48. hideProcessDetail,
  49. appData,
  50. noChatInput,
  51. switchSibling,
  52. }) => {
  53. const { t } = useTranslation()
  54. const {
  55. content,
  56. citation,
  57. agent_thoughts,
  58. more,
  59. annotation,
  60. workflowProcess,
  61. allFiles,
  62. message_files,
  63. } = item
  64. const hasAgentThoughts = !!agent_thoughts?.length
  65. const [containerWidth, setContainerWidth] = useState(0)
  66. const [contentWidth, setContentWidth] = useState(0)
  67. const containerRef = useRef<HTMLDivElement>(null)
  68. const contentRef = useRef<HTMLDivElement>(null)
  69. const getContainerWidth = () => {
  70. if (containerRef.current)
  71. setContainerWidth(containerRef.current?.clientWidth + 16)
  72. }
  73. useEffect(() => {
  74. getContainerWidth()
  75. }, [])
  76. const getContentWidth = () => {
  77. if (contentRef.current)
  78. setContentWidth(contentRef.current?.clientWidth)
  79. }
  80. useEffect(() => {
  81. if (!responding)
  82. getContentWidth()
  83. }, [responding])
  84. // Recalculate contentWidth when content changes (e.g., SVG preview/source toggle)
  85. useEffect(() => {
  86. if (!containerRef.current)
  87. return
  88. const resizeObserver = new ResizeObserver(() => {
  89. getContentWidth()
  90. })
  91. resizeObserver.observe(containerRef.current)
  92. return () => {
  93. resizeObserver.disconnect()
  94. }
  95. }, [])
  96. return (
  97. <div className='flex mb-2 last:mb-0'>
  98. <div className='shrink-0 relative w-10 h-10'>
  99. {answerIcon || <AnswerIcon />}
  100. {responding && (
  101. <div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'>
  102. <LoadingAnim type='avatar' />
  103. </div>
  104. )}
  105. </div>
  106. <div className='chat-answer-container group grow w-0 ml-4' ref={containerRef}>
  107. <div className={cn('group relative pr-10', chatAnswerContainerInner)}>
  108. <div
  109. ref={contentRef}
  110. className={cn('relative inline-block px-4 py-3 max-w-full bg-chat-bubble-bg rounded-2xl body-lg-regular text-text-primary', workflowProcess && 'w-full')}
  111. >
  112. {
  113. !responding && (
  114. <Operation
  115. hasWorkflowProcess={!!workflowProcess}
  116. maxSize={containerWidth - contentWidth - 4}
  117. contentWidth={contentWidth}
  118. item={item}
  119. question={question}
  120. index={index}
  121. showPromptLog={showPromptLog}
  122. noChatInput={noChatInput}
  123. />
  124. )
  125. }
  126. {/** Render the normal steps */}
  127. {
  128. workflowProcess && !hideProcessDetail && (
  129. <WorkflowProcess
  130. data={workflowProcess}
  131. item={item}
  132. hideProcessDetail={hideProcessDetail}
  133. />
  134. )
  135. }
  136. {/** Hide workflow steps by it's settings in siteInfo */}
  137. {
  138. workflowProcess && hideProcessDetail && appData && appData.site.show_workflow_steps && (
  139. <WorkflowProcess
  140. data={workflowProcess}
  141. item={item}
  142. hideProcessDetail={hideProcessDetail}
  143. />
  144. )
  145. }
  146. {
  147. responding && !content && !hasAgentThoughts && (
  148. <div className='flex items-center justify-center w-6 h-5'>
  149. <LoadingAnim type='text' />
  150. </div>
  151. )
  152. }
  153. {
  154. content && !hasAgentThoughts && (
  155. <BasicContent item={item} />
  156. )
  157. }
  158. {
  159. hasAgentThoughts && (
  160. <AgentContent
  161. item={item}
  162. responding={responding}
  163. />
  164. )
  165. }
  166. {
  167. !!allFiles?.length && (
  168. <FileList
  169. className='my-1'
  170. files={allFiles}
  171. showDeleteAction={false}
  172. showDownloadAction
  173. canPreview
  174. />
  175. )
  176. }
  177. {
  178. !!message_files?.length && (
  179. <FileList
  180. className='my-1'
  181. files={message_files}
  182. showDeleteAction={false}
  183. showDownloadAction
  184. canPreview
  185. />
  186. )
  187. }
  188. {
  189. annotation?.id && annotation.authorName && (
  190. <EditTitle
  191. className='mt-1'
  192. title={t('appAnnotation.editBy', { author: annotation.authorName })}
  193. />
  194. )
  195. }
  196. <SuggestedQuestions item={item} />
  197. {
  198. !!citation?.length && !responding && (
  199. <Citation data={citation} showHitInfo={config?.supportCitationHitInfo} />
  200. )
  201. }
  202. {item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="pt-3.5 flex justify-center items-center text-sm">
  203. <button
  204. className={`${item.prevSibling ? 'opacity-100' : 'opacity-65'}`}
  205. disabled={!item.prevSibling}
  206. onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
  207. >
  208. <ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-tertiary" />
  209. </button>
  210. <span className="px-2 text-xs text-text-quaternary">{item.siblingIndex + 1} / {item.siblingCount}</span>
  211. <button
  212. className={`${item.nextSibling ? 'opacity-100' : 'opacity-65'}`}
  213. disabled={!item.nextSibling}
  214. onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
  215. >
  216. <ChevronRight className="w-[14px] h-[14px] text-text-tertiary" />
  217. </button>
  218. </div>}
  219. </div>
  220. </div>
  221. <More more={more} />
  222. </div>
  223. </div>
  224. )
  225. }
  226. export default memo(Answer)