detail-modal.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. 'use client'
  2. import React, { useEffect, useState } from 'react'
  3. import { RiCloseLine } from '@remixicon/react'
  4. import Modal from '@/app/components/base/modal'
  5. import Button from '@/app/components/base/button'
  6. import {
  7. addIntent, addIntentKeyword, delBatchIntentKeyword, editIntent, editIntentKeyword, fetchIntentKeyword,
  8. fetchIntentType, getIntent,
  9. } from '@/service/common'
  10. import 'react-multi-email/dist/style.css'
  11. import Input from '@/app/components/base/input'
  12. import { SimpleSelect } from '@/app/components/base/select'
  13. import useSWR from 'swr'
  14. import Checkbox from '@/app/components/base/checkbox'
  15. import cn from '@/utils/classnames'
  16. import { useContext } from 'use-context-selector'
  17. import { ToastContext } from '@/app/components/base/toast'
  18. import Confirm from '@/app/components/base/confirm'
  19. import { Textarea } from '@/app/components/base/textarea'
  20. const DetailModal = ({
  21. transfer,
  22. onCancel,
  23. onSend,
  24. onRefresh,
  25. }: any) => {
  26. const { notify } = useContext(ToastContext)
  27. const [intentName, setIntentName] = useState<string>(transfer.row?.intentName || '')
  28. const [intentType, setIntentType] = useState<string>(transfer.row?.intentType || '')
  29. const [corpusList, setCorpusList] = useState<any>([])
  30. const [corpusFilter, setCorpusFilter] = useState<string>('')
  31. const [keywordsList, setKeywordsList] = useState<any>([])
  32. const { data: dataOptionsIntentType }: any = useSWR(
  33. {
  34. url: '/intentions/types',
  35. params: {
  36. page: 1,
  37. limit: 99999,
  38. },
  39. },
  40. fetchIntentType,
  41. )
  42. const optionsIntentType: any = dataOptionsIntentType?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
  43. useEffect(() => {
  44. if (transfer.row?.id) {
  45. getIntent({ url: `/intentions/${transfer.row.id}` }).then((res: any) => {
  46. setIntentType(res.type.id)
  47. setIntentName(res.name)
  48. setCorpusList(res.corpus)
  49. setKeywordsList(res.keywords)
  50. })
  51. }
  52. }, [])
  53. const [keyword, setKeyword] = useState<string>('')
  54. const [keywordFilter, setKeywordFilter] = useState<string>('')
  55. const refreshKeywords = async () => {
  56. const res = await fetchIntentKeyword({
  57. url: `/intentions/${transfer.row.id}/keywords`,
  58. params: {
  59. page: 1,
  60. limit: 99999,
  61. },
  62. })
  63. setKeywordsList(res)
  64. }
  65. const handleAddKeyword = async () => {
  66. if (!keyword)
  67. return
  68. if (keywordsList.some((v: any) => v.name === keyword)) {
  69. notify({ type: 'warning', message: '请勿新增重复数据!' })
  70. return
  71. }
  72. const { id }: any = await addIntentKeyword({
  73. url: `/intentions/${transfer.row.id}/keywords`,
  74. body: {
  75. name: keyword,
  76. },
  77. })
  78. if (id) {
  79. await refreshKeywords()
  80. setKeyword('')
  81. }
  82. }
  83. const [keywordSelectMap, setKeywordsSelectMap] = useState<any>(new Map())
  84. const addKeywordsSelectMap = (key: any, value: any) => {
  85. setKeywordsSelectMap((prevMap: any) => {
  86. const newMap = new Map(prevMap)
  87. newMap.set(key, value)
  88. return newMap
  89. })
  90. }
  91. const delKeywordsSelectMap = (key: any) => {
  92. setKeywordsSelectMap((prevMap: any) => {
  93. const newMap = new Map(prevMap)
  94. newMap.delete(key)
  95. return newMap
  96. })
  97. }
  98. const [keywordRow, setKeywordRow] = useState<any>({})
  99. const [showKeywordEdit, setShowKeywordEdit] = useState(false)
  100. const [editKeyword, setEditKeyword] = useState<string>('')
  101. const handleSaveKeyword = async () => {
  102. if (keywordsList.some((v: any) => v.name === editKeyword)) {
  103. notify({ type: 'warning', message: '请勿新增重复数据!' })
  104. return
  105. }
  106. const { id }: any = await editIntentKeyword({
  107. url: `/intentions/keywords/${keywordRow.id}`,
  108. body: {
  109. name: editKeyword,
  110. intention_id: keywordRow.intention_id,
  111. },
  112. })
  113. if (id) {
  114. await refreshKeywords()
  115. setShowKeywordEdit(false)
  116. }
  117. }
  118. const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  119. const [delBatch, setDelBatch] = useState(false)
  120. const handleDelKeyword = async () => {
  121. try {
  122. await delBatchIntentKeyword({
  123. url: '/intentions/keywords/batch',
  124. body: {
  125. method: 'delete',
  126. delete_data: delBatch ? Array.from(keywordSelectMap.keys()) : [keywordRow.id],
  127. },
  128. })
  129. setShowConfirmDelete(false)
  130. setKeywordsSelectMap(new Map())
  131. refreshKeywords()
  132. }
  133. catch (e) { }
  134. }
  135. const handleSave = async () => {
  136. try {
  137. let res
  138. if (transfer.mode === 'add') {
  139. res = await addIntent({
  140. url: '/intentions',
  141. body: { type_id: intentType, name: intentName },
  142. })
  143. }
  144. else {
  145. res = await editIntent({
  146. url: `/intentions/${transfer.row.id}`,
  147. body: { type_id: intentType, name: intentName },
  148. })
  149. }
  150. const { id }: any = res
  151. if (id) {
  152. if (transfer.mode === 'add')
  153. onRefresh(id)
  154. else
  155. onSend()
  156. }
  157. }
  158. catch (e) { }
  159. }
  160. const [similarityRow, setSimilarityRow] = useState<any>({})
  161. const [showSimilarityCorpus, setShowSimilarityCorpus] = useState(false)
  162. return (
  163. <div>
  164. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
  165. <div className='mb-2 flex justify-between'>
  166. <div className='text-xl font-semibold text-text-primary'>{transfer.mode === 'add' ? '新增' : '编辑'}意图</div>
  167. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
  168. </div>
  169. <div className="shrink-0 text-gray-500">
  170. <div className="flex flex-col gap-2">
  171. <div className="flex w-full items-center">
  172. <div className="w-[80px]">意图类型</div>
  173. <div className="flex flex-1">
  174. <SimpleSelect
  175. className="h-[32px] w-[200px]"
  176. defaultValue={intentType}
  177. onSelect={(i: any) => {
  178. setIntentType(i.value)
  179. }}
  180. items={optionsIntentType}
  181. allowSearch={false}
  182. placeholder="请选择意图类型"
  183. />
  184. </div>
  185. </div>
  186. <div className="flex w-full items-center">
  187. <div className="w-[80px]">意图名称</div>
  188. <div className="flex-1">
  189. <Input
  190. showClearIcon
  191. value={intentName}
  192. onChange={e => setIntentName(e.target.value)}
  193. onClear={() => setIntentName('')}
  194. />
  195. </div>
  196. </div>
  197. {
  198. transfer.mode === 'edit' && (
  199. <div className="flex w-full items-center">
  200. <div className="w-[80px]">关键词</div>
  201. <div className="flex-1">
  202. <Input
  203. showClearIcon
  204. value={keyword}
  205. onChange={e => setKeyword(e.target.value)}
  206. onClear={() => setKeyword('')}
  207. placeholder='输入后Enter以添加'
  208. onEnter={handleAddKeyword}
  209. />
  210. </div>
  211. </div>
  212. )
  213. }
  214. </div>
  215. {
  216. transfer.mode === 'edit' && (
  217. <div className="mt-3 flex flex-col">
  218. <div className="flex h-10 w-full items-center gap-2 border-2 border-[#F6F8FC] bg-[#F6F8FC] px-2">
  219. <div className='flex items-center' onClick={e => e.stopPropagation()}>
  220. <Checkbox
  221. className='mr-2 shrink-0'
  222. checked={keywordsList.every((v: any) => keywordSelectMap.has(v.id))}
  223. onCheck={() => {
  224. keywordsList.every((v: any) => keywordSelectMap.has(v.id))
  225. ? setKeywordsSelectMap(new Map())
  226. : keywordsList.forEach((v: any) => addKeywordsSelectMap(v.id, v))
  227. }}
  228. disabled={keywordsList.length === 0}
  229. />
  230. 全选
  231. </div>
  232. <div className="ml-auto w-[200px]">
  233. <Input
  234. showClearIcon
  235. value={keywordFilter}
  236. onChange={e => setKeywordFilter(e.target.value)}
  237. onClear={() => setKeywordFilter('')}
  238. placeholder='请输入关键词名称进行过滤'
  239. />
  240. </div>
  241. <Button variant='primary' className={cn('shrink-0')} onClick={() => {
  242. setDelBatch(true)
  243. setShowConfirmDelete(true)
  244. }}>
  245. 批量删除
  246. </Button>
  247. </div>
  248. <div className="flex h-[150px] flex-col gap-2 overflow-y-auto border-2 border-solid border-[#F6F8FC] p-2">
  249. {
  250. keywordsList.filter((v: any) => !keywordFilter || v.name.includes(keywordFilter)).map((item: any) => (
  251. <div key={item.id} className="flex items-center">
  252. <Checkbox
  253. className='mr-2 shrink-0'
  254. checked={keywordSelectMap.has(item.id)}
  255. onCheck={() => {
  256. keywordSelectMap.has(item.id)
  257. ? delKeywordsSelectMap(item.id)
  258. : addKeywordsSelectMap(item.id, item)
  259. }}
  260. disabled={keywordsList.length === 0}
  261. />
  262. <div className="flex-1">
  263. {item.name}
  264. </div>
  265. <Button variant='ghost-accent' size='small' className={cn('shrink-0')}
  266. onClick={() => {
  267. setKeywordRow(item)
  268. setEditKeyword(item.name)
  269. setShowKeywordEdit(true)
  270. }}>
  271. 编辑
  272. </Button>
  273. <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')}
  274. onClick={() => {
  275. setKeywordRow(item)
  276. setShowConfirmDelete(true)
  277. }}>
  278. 刪除
  279. </Button>
  280. </div>
  281. ))
  282. }
  283. </div>
  284. <div className="flex border-2 border-t-0 border-solid border-[#F6F8FC] p-2 text-xs">
  285. <div>共{keywordsList.length}条</div>
  286. <div className="ml-4">已选择{keywordSelectMap.size}条</div>
  287. </div>
  288. </div>
  289. )
  290. }
  291. {
  292. transfer.mode === 'edit' && (
  293. <div className="mt-3 flex flex-col">
  294. <div className="flex h-10 w-full items-center gap-2 border-2 border-[#F6F8FC] bg-[#F6F8FC] px-2">
  295. <div>语料列表</div>
  296. <div className="ml-auto w-[200px]">
  297. <Input
  298. showClearIcon
  299. value={corpusFilter}
  300. onChange={e => setCorpusFilter(e.target.value)}
  301. onClear={() => setCorpusFilter('')}
  302. placeholder='请输入语料名称进行过滤'
  303. />
  304. </div>
  305. </div>
  306. <div className="flex h-[150px] flex-col gap-2 overflow-y-auto border-2 border-solid border-[#F6F8FC] p-2">
  307. {
  308. corpusList.filter((v: any) => !corpusFilter || v.question.includes(corpusFilter)).map((item: any) => (
  309. <div key={item.id} className="flex items-center">
  310. <div className="flex-1">
  311. {item.question}
  312. </div>
  313. <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
  314. setSimilarityRow(item)
  315. setShowSimilarityCorpus(true)
  316. }}>
  317. 语料配置
  318. </Button>
  319. </div>
  320. ))
  321. }
  322. </div>
  323. <div className="flex border-2 border-t-0 border-solid border-[#F6F8FC] p-2 text-xs">
  324. <div>共{corpusList.length}条</div>
  325. </div>
  326. </div>
  327. )
  328. }
  329. <Button
  330. tabIndex={0}
  331. className='mt-2 w-full'
  332. onClick={handleSave}
  333. disabled={!intentType.length || !intentName.length}
  334. variant='primary'
  335. >
  336. 保存
  337. </Button>
  338. </div>
  339. </Modal>
  340. {
  341. showKeywordEdit && (
  342. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[400px]">
  343. <div className='mb-2 flex justify-between'>
  344. <div className='text-xl font-semibold text-text-primary'>编辑关键词</div>
  345. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowKeywordEdit(false)} />
  346. </div>
  347. <div>
  348. <div className={cn('flex flex-wrap items-center justify-between py-4')}>
  349. <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
  350. 关键词
  351. </div>
  352. <Input
  353. value={editKeyword}
  354. onChange={e => setEditKeyword(e.target.value)}
  355. className='h-9'
  356. placeholder='请输入关键词'
  357. />
  358. </div>
  359. <Button
  360. tabIndex={0}
  361. className='w-full'
  362. onClick={handleSaveKeyword}
  363. disabled={!editKeyword.length}
  364. variant='primary'
  365. >
  366. 保存
  367. </Button>
  368. </div>
  369. </Modal>
  370. )
  371. }
  372. {showConfirmDelete && (
  373. <Confirm
  374. title="删除确认"
  375. content={`请确认是否删除${delBatch ? `${keywordSelectMap.size}条关键词` : keywordRow.name}?`}
  376. isShow={showConfirmDelete}
  377. onConfirm={handleDelKeyword}
  378. onCancel={() => setShowConfirmDelete(false)}
  379. />
  380. )}
  381. {
  382. showSimilarityCorpus && (
  383. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] max-w-[800px]">
  384. <div className='mb-2 flex justify-between'>
  385. <div className='text-xl font-semibold text-text-primary'>训练语料配置</div>
  386. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowSimilarityCorpus(false)} />
  387. </div>
  388. <div>
  389. <div className={cn('flex flex-wrap items-center justify-between py-4')}>
  390. <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
  391. 当前标注问题:{similarityRow.question}
  392. </div>
  393. <Textarea
  394. value={similarityRow.question_config || ''}
  395. className='resize-none'
  396. rows={30}
  397. disabled={true}
  398. />
  399. </div>
  400. </div>
  401. </Modal>
  402. )
  403. }
  404. </div>
  405. )
  406. }
  407. export default DetailModal