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.

question.tsx 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import type {
  2. FC,
  3. ReactNode,
  4. } from 'react'
  5. import {
  6. memo,
  7. useCallback,
  8. useEffect,
  9. useRef,
  10. useState,
  11. } from 'react'
  12. import type { ChatItem } from '../types'
  13. import type { Theme } from '../embedded-chatbot/theme/theme-context'
  14. import { CssTransform } from '../embedded-chatbot/theme/utils'
  15. import ContentSwitch from './content-switch'
  16. import { User } from '@/app/components/base/icons/src/public/avatar'
  17. import { Markdown } from '@/app/components/base/markdown'
  18. import { FileList } from '@/app/components/base/file-uploader'
  19. import ActionButton from '../../action-button'
  20. import { RiClipboardLine, RiEditLine } from '@remixicon/react'
  21. import Toast from '../../toast'
  22. import copy from 'copy-to-clipboard'
  23. import { useTranslation } from 'react-i18next'
  24. import cn from '@/utils/classnames'
  25. import Textarea from 'react-textarea-autosize'
  26. import Button from '../../button'
  27. import { useChatContext } from './context'
  28. type QuestionProps = {
  29. item: ChatItem
  30. questionIcon?: ReactNode
  31. theme: Theme | null | undefined
  32. enableEdit?: boolean
  33. switchSibling?: (siblingMessageId: string) => void
  34. }
  35. const Question: FC<QuestionProps> = ({
  36. item,
  37. questionIcon,
  38. theme,
  39. enableEdit = true,
  40. switchSibling,
  41. }) => {
  42. const { t } = useTranslation()
  43. const {
  44. content,
  45. message_files,
  46. } = item
  47. const {
  48. onRegenerate,
  49. } = useChatContext()
  50. const [isEditing, setIsEditing] = useState(false)
  51. const [editedContent, setEditedContent] = useState(content)
  52. const [contentWidth, setContentWidth] = useState(0)
  53. const contentRef = useRef<HTMLDivElement>(null)
  54. const handleEdit = useCallback(() => {
  55. setIsEditing(true)
  56. setEditedContent(content)
  57. }, [content])
  58. const handleResend = useCallback(() => {
  59. setIsEditing(false)
  60. onRegenerate?.(item, { message: editedContent, files: message_files })
  61. }, [editedContent, message_files, item, onRegenerate])
  62. const handleCancelEditing = useCallback(() => {
  63. setIsEditing(false)
  64. setEditedContent(content)
  65. }, [content])
  66. const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
  67. if (direction === 'prev')
  68. item.prevSibling && switchSibling?.(item.prevSibling)
  69. else
  70. item.nextSibling && switchSibling?.(item.nextSibling)
  71. }, [switchSibling, item.prevSibling, item.nextSibling])
  72. const getContentWidth = () => {
  73. if (contentRef.current)
  74. setContentWidth(contentRef.current?.clientWidth)
  75. }
  76. useEffect(() => {
  77. if (!contentRef.current)
  78. return
  79. const resizeObserver = new ResizeObserver(() => {
  80. getContentWidth()
  81. })
  82. resizeObserver.observe(contentRef.current)
  83. return () => {
  84. resizeObserver.disconnect()
  85. }
  86. }, [])
  87. return (
  88. <div className='mb-2 flex justify-end last:mb-0'>
  89. <div className={cn('group relative mr-4 flex max-w-full items-start pl-14', isEditing && 'flex-1')}>
  90. <div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}>
  91. <div
  92. className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex"
  93. style={{ right: contentWidth + 8 }}
  94. >
  95. <ActionButton onClick={() => {
  96. copy(content)
  97. Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
  98. }}>
  99. <RiClipboardLine className='h-4 w-4' />
  100. </ActionButton>
  101. {enableEdit && <ActionButton onClick={handleEdit}>
  102. <RiEditLine className='h-4 w-4' />
  103. </ActionButton>}
  104. </div>
  105. </div>
  106. <div
  107. ref={contentRef}
  108. className='w-full rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 px-4 py-3 text-sm text-text-primary'
  109. style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
  110. >
  111. {
  112. !!message_files?.length && (
  113. <FileList
  114. className='mb-2'
  115. files={message_files}
  116. showDeleteAction={false}
  117. showDownloadAction={true}
  118. />
  119. )
  120. }
  121. { !isEditing
  122. ? <Markdown content={content} />
  123. : <div className="
  124. flex flex-col gap-2 rounded-xl
  125. border border-components-chat-input-border bg-components-panel-bg-blur p-[9px] shadow-md
  126. ">
  127. <div className="max-h-[158px] overflow-y-auto overflow-x-hidden">
  128. <Textarea
  129. className={cn(
  130. 'body-lg-regular w-full p-1 leading-6 text-text-tertiary outline-none',
  131. )}
  132. autoFocus
  133. minRows={1}
  134. value={editedContent}
  135. onChange={e => setEditedContent(e.target.value)}
  136. />
  137. </div>
  138. <div className="flex justify-end gap-2">
  139. <Button variant='ghost' onClick={handleCancelEditing}>{t('common.operation.cancel')}</Button>
  140. <Button variant='primary' onClick={handleResend}>{t('common.chat.resend')}</Button>
  141. </div>
  142. </div> }
  143. { !isEditing && <ContentSwitch
  144. count={item.siblingCount}
  145. currentIndex={item.siblingIndex}
  146. prevDisabled={!item.prevSibling}
  147. nextDisabled={!item.nextSibling}
  148. switchSibling={handleSwitchSibling}
  149. />}
  150. </div>
  151. <div className='mt-1 h-[18px]' />
  152. </div>
  153. <div className='h-10 w-10 shrink-0'>
  154. {
  155. questionIcon || (
  156. <div className='h-full w-full rounded-full border-[0.5px] border-black/5'>
  157. <User className='h-full w-full' />
  158. </div>
  159. )
  160. }
  161. </div>
  162. </div>
  163. )
  164. }
  165. export default memo(Question)