index.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { Fragment, useEffect, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import {
  6. RiAddLine,
  7. RiMoreFill,
  8. } from '@remixicon/react'
  9. import { useContext } from 'use-context-selector'
  10. import {
  11. useCSVDownloader,
  12. } from 'react-papaparse'
  13. import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react'
  14. import Button from '../../../base/button'
  15. import AddAnnotationModal from '../add-annotation-modal'
  16. import type { AnnotationItemBasic } from '../type'
  17. import BatchAddModal from '../batch-add-annotation-modal'
  18. import cn from '@/utils/classnames'
  19. import CustomPopover from '@/app/components/base/popover'
  20. import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
  21. import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
  22. import I18n from '@/context/i18n'
  23. import { fetchExportAnnotationList } from '@/service/annotation'
  24. import { LanguagesSupported } from '@/i18n/language'
  25. const CSV_HEADER_QA_EN = ['Question', 'Answer']
  26. const CSV_HEADER_QA_CN = ['问题', '答案']
  27. type Props = {
  28. appId: string
  29. onAdd: (payload: AnnotationItemBasic) => void
  30. onAdded: () => void
  31. controlUpdateList: number
  32. }
  33. const HeaderOptions: FC<Props> = ({
  34. appId,
  35. onAdd,
  36. onAdded,
  37. controlUpdateList,
  38. }) => {
  39. const { t } = useTranslation()
  40. const { locale } = useContext(I18n)
  41. const { CSVDownloader, Type } = useCSVDownloader()
  42. const [list, setList] = useState<AnnotationItemBasic[]>([])
  43. const annotationUnavailable = list.length === 0
  44. const listTransformer = (list: AnnotationItemBasic[]) => list.map(
  45. (item: AnnotationItemBasic) => {
  46. const dataString = `{"messages": [{"role": "system", "content": ""}, {"role": "user", "content": ${JSON.stringify(item.question)}}, {"role": "assistant", "content": ${JSON.stringify(item.answer)}}]}`
  47. return dataString
  48. },
  49. )
  50. const JSONLOutput = () => {
  51. const a = document.createElement('a')
  52. const content = listTransformer(list).join('\n')
  53. const file = new Blob([content], { type: 'application/jsonl' })
  54. a.href = URL.createObjectURL(file)
  55. a.download = `annotations-${locale}.jsonl`
  56. a.click()
  57. }
  58. const fetchList = async () => {
  59. const { data }: any = await fetchExportAnnotationList(appId)
  60. setList(data as AnnotationItemBasic[])
  61. }
  62. useEffect(() => {
  63. fetchList()
  64. }, [])
  65. useEffect(() => {
  66. if (controlUpdateList)
  67. fetchList()
  68. }, [controlUpdateList])
  69. const [showBulkImportModal, setShowBulkImportModal] = useState(false)
  70. const Operations = () => {
  71. return (
  72. <div className="w-full py-1">
  73. <button className='mx-1 flex h-9 w-[calc(100%_-_8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50' onClick={() => {
  74. setShowBulkImportModal(true)
  75. }}>
  76. <FilePlus02 className='h-4 w-4 text-text-tertiary' />
  77. <span className='system-sm-regular grow text-left text-text-secondary'>{t('appAnnotation.table.header.bulkImport')}</span>
  78. </button>
  79. <Menu as="div" className="relative h-full w-full">
  80. <MenuButton className='mx-1 flex h-9 w-[calc(100%_-_8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50'>
  81. <FileDownload02 className='h-4 w-4 text-text-tertiary' />
  82. <span className='system-sm-regular grow text-left text-text-secondary'>{t('appAnnotation.table.header.bulkExport')}</span>
  83. <ChevronRight className='h-[14px] w-[14px] shrink-0 text-text-tertiary' />
  84. </MenuButton>
  85. <Transition
  86. as={Fragment}
  87. enter="transition ease-out duration-100"
  88. enterFrom="transform opacity-0 scale-95"
  89. enterTo="transform opacity-100 scale-100"
  90. leave="transition ease-in duration-75"
  91. leaveFrom="transform opacity-100 scale-100"
  92. leaveTo="transform opacity-0 scale-95"
  93. >
  94. <MenuItems
  95. className={cn(
  96. 'absolute left-1 top-[1px] z-10 min-w-[100px] origin-top-right -translate-x-full rounded-xl border-[0.5px] border-components-panel-on-panel-item-bg bg-components-panel-bg py-1 shadow-xs',
  97. )}
  98. >
  99. <CSVDownloader
  100. type={Type.Link}
  101. filename={`annotations-${locale}`}
  102. bom={true}
  103. data={[
  104. locale !== LanguagesSupported[1] ? CSV_HEADER_QA_EN : CSV_HEADER_QA_CN,
  105. ...list.map(item => [item.question, item.answer]),
  106. ]}
  107. >
  108. <button disabled={annotationUnavailable} className='mx-1 flex h-9 w-[calc(100%_-_8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50'>
  109. <span className='system-sm-regular grow text-left text-text-secondary'>CSV</span>
  110. </button>
  111. </CSVDownloader>
  112. <button disabled={annotationUnavailable} className={cn('mx-1 flex h-9 w-[calc(100%_-_8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50', '!border-0')} onClick={JSONLOutput}>
  113. <span className='system-sm-regular grow text-left text-text-secondary'>JSONL</span>
  114. </button>
  115. </MenuItems>
  116. </Transition>
  117. </Menu>
  118. </div>
  119. )
  120. }
  121. const [showAddModal, setShowAddModal] = React.useState(false)
  122. return (
  123. <div className='flex space-x-2'>
  124. <Button variant='primary' onClick={() => setShowAddModal(true)}>
  125. <RiAddLine className='mr-0.5 h-4 w-4' />
  126. <div>{t('appAnnotation.table.header.addAnnotation')}</div>
  127. </Button>
  128. <CustomPopover
  129. htmlContent={<Operations />}
  130. position="br"
  131. trigger="click"
  132. btnElement={
  133. <Button variant='secondary' className='w-8 p-0'>
  134. <RiMoreFill className='h-4 w-4' />
  135. </Button>
  136. }
  137. btnClassName='p-0 border-0'
  138. className={'!z-20 h-fit !w-[155px]'}
  139. popupClassName='!w-full !overflow-visible'
  140. manualClose
  141. />
  142. {showAddModal && (
  143. <AddAnnotationModal
  144. isShow={showAddModal}
  145. onHide={() => setShowAddModal(false)}
  146. onAdd={onAdd}
  147. />
  148. )}
  149. {
  150. showBulkImportModal && (
  151. <BatchAddModal
  152. appId={appId}
  153. isShow={showBulkImportModal}
  154. onCancel={() => setShowBulkImportModal(false)}
  155. onAdded={onAdded}
  156. />
  157. )
  158. }
  159. </div>
  160. )
  161. }
  162. export default React.memo(HeaderOptions)