index.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { useEffect, useMemo, useRef, useState } from 'react'
  2. import type { FC } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { RiArrowDownSLine } from '@remixicon/react'
  5. import type { CitationItem } from '../type'
  6. import Popup from './popup'
  7. export type Resources = {
  8. documentId: string
  9. documentName: string
  10. dataSourceType: string
  11. sources: CitationItem[]
  12. }
  13. type CitationProps = {
  14. data: CitationItem[]
  15. showHitInfo?: boolean
  16. containerClassName?: string
  17. }
  18. const Citation: FC<CitationProps> = ({
  19. data,
  20. showHitInfo,
  21. containerClassName = 'chat-answer-container',
  22. }) => {
  23. const { t } = useTranslation()
  24. const elesRef = useRef<HTMLDivElement[]>([])
  25. const [limitNumberInOneLine, setlimitNumberInOneLine] = useState(0)
  26. const [showMore, setShowMore] = useState(false)
  27. const resources = useMemo(() => data.reduce((prev: Resources[], next) => {
  28. const documentId = next.document_id
  29. const documentName = next.document_name
  30. const dataSourceType = next.data_source_type
  31. const documentIndex = prev.findIndex(i => i.documentId === documentId)
  32. if (documentIndex > -1) {
  33. prev[documentIndex].sources.push(next)
  34. }
  35. else {
  36. prev.push({
  37. documentId,
  38. documentName,
  39. dataSourceType,
  40. sources: [next],
  41. })
  42. }
  43. return prev
  44. }, []), [data])
  45. const handleAdjustResourcesLayout = () => {
  46. const containerWidth = document.querySelector(`.${containerClassName}`)!.clientWidth - 40
  47. let totalWidth = 0
  48. for (let i = 0; i < resources.length; i++) {
  49. totalWidth += elesRef.current[i].clientWidth
  50. if (totalWidth + i * 4 > containerWidth!) {
  51. totalWidth -= elesRef.current[i].clientWidth
  52. if (totalWidth + 34 > containerWidth!)
  53. setlimitNumberInOneLine(i - 1)
  54. else
  55. setlimitNumberInOneLine(i)
  56. break
  57. }
  58. else {
  59. setlimitNumberInOneLine(i + 1)
  60. }
  61. }
  62. }
  63. useEffect(() => {
  64. handleAdjustResourcesLayout()
  65. }, [])
  66. const resourcesLength = resources.length
  67. return (
  68. <div className='mt-3 -mb-1'>
  69. <div className='flex items-center mb-2 text-xs font-medium text-gray-500'>
  70. {t('common.chat.citation.title')}
  71. <div className='grow ml-2 h-[1px] bg-black/5' />
  72. </div>
  73. <div className='relative flex flex-wrap'>
  74. {
  75. resources.map((res, index) => (
  76. <div
  77. key={index}
  78. className='absolute top-0 left-0 w-auto mr-1 mb-1 pl-7 pr-2 max-w-[240px] h-7 text-xs whitespace-nowrap opacity-0 -z-10'
  79. ref={ele => (elesRef.current[index] = ele!)}
  80. >
  81. {res.documentName}
  82. </div>
  83. ))
  84. }
  85. {
  86. resources.slice(0, showMore ? resourcesLength : limitNumberInOneLine).map((res, index) => (
  87. <div key={index} className='mr-1 mb-1 cursor-pointer'>
  88. <Popup
  89. data={res}
  90. showHitInfo={showHitInfo}
  91. />
  92. </div>
  93. ))
  94. }
  95. {
  96. limitNumberInOneLine < resourcesLength && (
  97. <div
  98. className='flex items-center px-2 h-7 bg-white rounded-lg text-xs font-medium text-gray-500 cursor-pointer'
  99. onClick={() => setShowMore(v => !v)}
  100. >
  101. {
  102. !showMore
  103. ? `+ ${resourcesLength - limitNumberInOneLine}`
  104. : <RiArrowDownSLine className='w-4 h-4 text-gray-600 rotate-180' />
  105. }
  106. </div>
  107. )
  108. }
  109. </div>
  110. </div>
  111. )
  112. }
  113. export default Citation