detail-modal.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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. addCorpus, addCorpusQuestion, delBatchCorpusQuestion, editCorpus as editCorpusFunc, editCorpusQuestion, fetchCorpusQuestion,
  8. fetchIntent,
  9. getCorpus,
  10. } from '@/service/common'
  11. import 'react-multi-email/dist/style.css'
  12. import Input from '@/app/components/base/input'
  13. import Checkbox from '@/app/components/base/checkbox'
  14. import cn from '@/utils/classnames'
  15. import { useContext } from 'use-context-selector'
  16. import { ToastContext } from '@/app/components/base/toast'
  17. import Confirm from '@/app/components/base/confirm'
  18. import { Cascader as AntdCascader } from 'antd'
  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 [intentCascader, setIntentCascader] = useState<any>([])
  28. useEffect(() => {
  29. fetchIntent({
  30. url: '/intentions',
  31. params: {
  32. page: 1,
  33. limit: 99999,
  34. },
  35. }).then((res: any) => {
  36. const map = new Map()
  37. res.data.forEach((v: any) => {
  38. if (map.has(v.type_id)) {
  39. const parent = map.get(v.type_id)
  40. parent.children.push(v)
  41. map.set(v.type_id, parent)
  42. }
  43. else {
  44. map.set(v.type_id, {
  45. id: v.type_id,
  46. name: v.type_name,
  47. children: [v],
  48. })
  49. }
  50. })
  51. setIntentCascader(Array.from(map.values()))
  52. })
  53. }, [])
  54. const [question, setQuestion] = useState<string>('')
  55. const [intentValue, setIntentValue] = useState<any>([])
  56. const [questionCorpus, setQuestionCorpus] = useState<any>('')
  57. const [similarityList, setSimilarityList] = useState<any>([])
  58. const handleSave = async () => {
  59. try {
  60. let res
  61. if (transfer.mode === 'add') {
  62. res = await addCorpus({
  63. url: '/intentions/corpus',
  64. body: { question, intention_id: intentValue[intentValue.length - 1] },
  65. })
  66. }
  67. else {
  68. res = await editCorpusFunc({
  69. url: `/intentions/corpus/${transfer.row.id}`,
  70. body: { question, intention_id: intentValue[intentValue.length - 1], question_config: questionCorpus },
  71. })
  72. }
  73. const { id }: any = res
  74. if (id) {
  75. if (transfer.mode === 'add')
  76. onRefresh(id)
  77. else
  78. onSend()
  79. }
  80. }
  81. catch (e) { }
  82. }
  83. useEffect(() => {
  84. if (transfer.row?.id) {
  85. getCorpus({ url: `/intentions/corpus/${transfer.row.id}` }).then((res: any) => {
  86. setQuestion(res.question)
  87. setIntentValue([res.intention.type_id, res.intention.id])
  88. setQuestionCorpus(res.question_config || '')
  89. setSimilarityList(res.similarity_questions)
  90. })
  91. }
  92. }, [])
  93. const [similarityQuestion, setSimilarityQuestion] = useState<string>('')
  94. const [similarityFilter, setSimilarityFilter] = useState<string>('')
  95. const refreshSimilarity = async () => {
  96. const res = await fetchCorpusQuestion({
  97. url: `/intentions/corpus/${transfer.row.id}/similarity_questions`,
  98. params: {
  99. page: 1,
  100. limit: 99999,
  101. },
  102. })
  103. setSimilarityList(res)
  104. }
  105. const handleAddSimilarity = async () => {
  106. if (!similarityQuestion)
  107. return
  108. if (similarityList.some((v: any) => v.question === similarityQuestion)) {
  109. notify({ type: 'warning', message: '请勿新增重复数据!' })
  110. return
  111. }
  112. const { id }: any = await addCorpusQuestion({
  113. url: `/intentions/corpus/${transfer.row.id}/similarity_questions`,
  114. body: {
  115. question: similarityQuestion,
  116. },
  117. })
  118. if (id) {
  119. await refreshSimilarity()
  120. setSimilarityQuestion('')
  121. }
  122. }
  123. const [similaritySelectMap, setSimilaritySelectMap] = useState<any>(new Map())
  124. const addSimilaritySelectMap = (key: any, value: any) => {
  125. setSimilaritySelectMap((prevMap: any) => {
  126. const newMap = new Map(prevMap)
  127. newMap.set(key, value)
  128. return newMap
  129. })
  130. }
  131. const delSimilaritySelectMap = (key: any) => {
  132. setSimilaritySelectMap((prevMap: any) => {
  133. const newMap = new Map(prevMap)
  134. newMap.delete(key)
  135. return newMap
  136. })
  137. }
  138. const [similarityRow, setSimilarityRow] = useState<any>({})
  139. const [showSimilarityEdit, setShowSimilarityEdit] = useState(false)
  140. const [editSimilarity, setEditSimilarity] = useState<string>('')
  141. const [showSimilarityCorpus, setShowSimilarityCorpus] = useState(false)
  142. const [editSimilarityCorpus, setEditSimilarityCorpus] = useState('')
  143. const handleSaveSimilarity = async () => {
  144. if (similarityList.some((v: any) => v.name === editSimilarity)) {
  145. notify({ type: 'warning', message: '请勿新增重复数据!' })
  146. return
  147. }
  148. const params: any = {
  149. corpus_id: similarityRow.corpus_id,
  150. }
  151. if (showSimilarityEdit) {
  152. params.question = editSimilarity
  153. }
  154. else if (showSimilarityCorpus) {
  155. params.question = similarityRow.question
  156. params.question_config = editSimilarityCorpus
  157. }
  158. const { id }: any = await editCorpusQuestion({
  159. url: `/intentions/similarity_questions/${similarityRow.id}`,
  160. body: params,
  161. })
  162. if (id) {
  163. await refreshSimilarity()
  164. setShowSimilarityEdit(false)
  165. setShowSimilarityCorpus(false)
  166. }
  167. }
  168. const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  169. const [delBatch, setDelBatch] = useState(false)
  170. const handleDelSimilarity = async () => {
  171. try {
  172. await delBatchCorpusQuestion({
  173. url: '/intentions/similarity_questions/batch',
  174. body: {
  175. method: 'delete',
  176. data: delBatch ? Array.from(similaritySelectMap.keys()) : [similarityRow.id],
  177. },
  178. })
  179. setShowConfirmDelete(false)
  180. setSimilaritySelectMap(new Map())
  181. refreshSimilarity()
  182. }
  183. catch (e) { }
  184. }
  185. const [showCorpus, setShowCorpus] = useState(false)
  186. const [editCorpus, setEditCorpus] = useState('')
  187. return (
  188. <div>
  189. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
  190. <div className='mb-2 flex justify-between'>
  191. <div className='text-xl font-semibold text-text-primary'>{transfer.mode === 'add' ? '新增' : '编辑'}语料</div>
  192. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
  193. </div>
  194. <div className="shrink-0 text-gray-500">
  195. <div className="flex flex-col gap-2">
  196. <div className="flex w-full items-center">
  197. <div className="w-[80px]">标准问题</div>
  198. <div className="flex-1">
  199. <Input
  200. showClearIcon
  201. value={question}
  202. onChange={e => setQuestion(e.target.value)}
  203. onClear={() => setQuestion('')}
  204. />
  205. </div>
  206. {
  207. transfer.mode === 'edit' && (
  208. <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
  209. setEditCorpus(questionCorpus)
  210. setShowCorpus(true)
  211. }}>
  212. 语料配置
  213. </Button>
  214. )
  215. }
  216. </div>
  217. <div className="flex w-full items-center">
  218. <div className="w-[80px]">意图名称</div>
  219. <div className="flex flex-1">
  220. <AntdCascader
  221. value={intentValue}
  222. className="h-[32px] w-full"
  223. options={intentCascader}
  224. onChange={(val: any) => {
  225. setIntentValue(val)
  226. }}
  227. placeholder="请选择"
  228. fieldNames={{ label: 'name', value: 'id' }}
  229. showSearch={true}
  230. />
  231. </div>
  232. </div>
  233. {
  234. transfer.mode === 'edit' && (
  235. <div className="flex w-full items-center">
  236. <div className="w-[80px]">相似问题</div>
  237. <div className="flex-1">
  238. <Input
  239. showClearIcon
  240. value={similarityQuestion}
  241. onChange={e => setSimilarityQuestion(e.target.value)}
  242. onClear={() => setSimilarityQuestion('')}
  243. placeholder='输入后Enter以添加'
  244. onEnter={handleAddSimilarity}
  245. />
  246. </div>
  247. </div>
  248. )
  249. }
  250. </div>
  251. {
  252. transfer.mode === 'edit' && (
  253. <div className="mt-3 flex flex-col">
  254. <div className="flex h-10 w-full items-center gap-2 border-2 border-[#F6F8FC] bg-[#F6F8FC] px-2">
  255. <div className='flex items-center' onClick={e => e.stopPropagation()}>
  256. <Checkbox
  257. className='mr-2 shrink-0'
  258. checked={similarityList.every((v: any) => similaritySelectMap.has(v.id))}
  259. onCheck={() => {
  260. similarityList.every((v: any) => similaritySelectMap.has(v.id))
  261. ? setSimilaritySelectMap(new Map())
  262. : similarityList.forEach((v: any) => addSimilaritySelectMap(v.id, v))
  263. }}
  264. disabled={similarityList.length === 0}
  265. />
  266. 全选
  267. </div>
  268. <div className="ml-auto w-[200px]">
  269. <Input
  270. showClearIcon
  271. value={similarityFilter}
  272. onChange={e => setSimilarityFilter(e.target.value)}
  273. onClear={() => setSimilarityFilter('')}
  274. placeholder='请输入相似问题名称进行过滤'
  275. />
  276. </div>
  277. <Button variant='primary' className={cn('shrink-0')} onClick={() => {
  278. setDelBatch(true)
  279. setShowConfirmDelete(true)
  280. }}>
  281. 批量删除
  282. </Button>
  283. </div>
  284. <div className="flex h-[300px] flex-col gap-2 overflow-y-auto border-2 border-solid border-[#F6F8FC] p-2">
  285. {
  286. similarityList.filter((v: any) => !similarityFilter || v.question.includes(similarityFilter)).map((item: any) => (
  287. <div key={item.id} className="flex items-center">
  288. <Checkbox
  289. className='mr-2 shrink-0'
  290. checked={similaritySelectMap.has(item.id)}
  291. onCheck={() => {
  292. similaritySelectMap.has(item.id)
  293. ? delSimilaritySelectMap(item.id)
  294. : addSimilaritySelectMap(item.id, item)
  295. }}
  296. disabled={similarityList.length === 0}
  297. />
  298. <div className="flex-1">
  299. {item.question}
  300. </div>
  301. <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
  302. setSimilarityRow(item)
  303. setEditSimilarityCorpus(item.question_config || '')
  304. setShowSimilarityCorpus(true)
  305. }}>
  306. 语料配置
  307. </Button>
  308. <Button variant='ghost-accent' size='small' className={cn('shrink-0')}
  309. onClick={() => {
  310. setSimilarityRow(item)
  311. setEditSimilarity(item.question)
  312. setShowSimilarityEdit(true)
  313. }}>
  314. 编辑
  315. </Button>
  316. <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')}
  317. onClick={() => {
  318. setSimilarityRow(item)
  319. setShowConfirmDelete(true)
  320. }}>
  321. 刪除
  322. </Button>
  323. </div>
  324. ))
  325. }
  326. </div>
  327. <div className="flex border-2 border-t-0 border-solid border-[#F6F8FC] p-2 text-xs">
  328. <div>共{similarityList.length}条</div>
  329. <div className="ml-4">已选择{similaritySelectMap.size}条</div>
  330. </div>
  331. </div>
  332. )
  333. }
  334. <Button
  335. tabIndex={0}
  336. className='mt-4 w-full'
  337. onClick={handleSave}
  338. disabled={!question.length || !intentValue.length}
  339. variant='primary'
  340. >
  341. 保存
  342. </Button>
  343. </div>
  344. </Modal>
  345. {
  346. showSimilarityEdit && (
  347. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[400px]">
  348. <div className='mb-2 flex justify-between'>
  349. <div className='text-xl font-semibold text-text-primary'>编辑相似问题</div>
  350. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowSimilarityEdit(false)} />
  351. </div>
  352. <div>
  353. <div className={cn('flex flex-wrap items-center justify-between py-4')}>
  354. <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
  355. 相似问题
  356. </div>
  357. <Input
  358. value={editSimilarity}
  359. onChange={e => setEditSimilarity(e.target.value)}
  360. className='h-9'
  361. placeholder='请输入相似问题'
  362. />
  363. </div>
  364. <Button
  365. tabIndex={0}
  366. className='w-full'
  367. onClick={handleSaveSimilarity}
  368. disabled={!editSimilarity.length}
  369. variant='primary'
  370. >
  371. 保存
  372. </Button>
  373. </div>
  374. </Modal>
  375. )
  376. }
  377. {showConfirmDelete && (
  378. <Confirm
  379. title="删除确认"
  380. content={`请确认是否删除${delBatch ? `${similaritySelectMap.size}条相似问题` : similarityRow.question}?`}
  381. isShow={showConfirmDelete}
  382. onConfirm={handleDelSimilarity}
  383. onCancel={() => setShowConfirmDelete(false)}
  384. />
  385. )}
  386. {
  387. showCorpus && (
  388. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] max-w-[800px]">
  389. <div className='mb-2 flex justify-between'>
  390. <div className='text-xl font-semibold text-text-primary'>训练语料配置</div>
  391. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowCorpus(false)} />
  392. </div>
  393. <div>
  394. <div className={cn('flex flex-wrap items-center justify-between py-4')}>
  395. <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
  396. 当前标注问题:{question}
  397. </div>
  398. <Textarea
  399. value={editCorpus}
  400. onChange={e => setEditCorpus(e.target.value)}
  401. className='resize-none'
  402. placeholder='请输入语料配置'
  403. rows={30}
  404. />
  405. </div>
  406. <div className="flex gap-2">
  407. <Button
  408. className='w-full'
  409. onClick={() => setEditCorpus(questionCorpus)}
  410. variant='warning'
  411. >
  412. 重置
  413. </Button>
  414. <Button
  415. className='w-full'
  416. onClick={() => {
  417. setQuestionCorpus(editCorpus)
  418. setShowCorpus(false)
  419. }}
  420. disabled={!editCorpus.length}
  421. variant='primary'
  422. >
  423. 保存
  424. </Button>
  425. </div>
  426. </div>
  427. </Modal>
  428. )
  429. }
  430. {
  431. showSimilarityCorpus && (
  432. <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] max-w-[800px]">
  433. <div className='mb-2 flex justify-between'>
  434. <div className='text-xl font-semibold text-text-primary'>训练语料配置</div>
  435. <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowSimilarityCorpus(false)} />
  436. </div>
  437. <div>
  438. <div className={cn('flex flex-wrap items-center justify-between py-4')}>
  439. <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
  440. 当前标注问题:{similarityRow.question}
  441. </div>
  442. <Textarea
  443. value={editSimilarityCorpus}
  444. onChange={e => setEditSimilarityCorpus(e.target.value)}
  445. className='resize-none'
  446. placeholder='请输入语料配置'
  447. rows={30}
  448. />
  449. </div>
  450. <div className="flex gap-2">
  451. <Button
  452. className='w-full'
  453. onClick={() => setEditSimilarityCorpus(similarityRow.question_config || '')}
  454. variant='warning'
  455. >
  456. 重置
  457. </Button>
  458. <Button
  459. className='w-full'
  460. onClick={handleSaveSimilarity}
  461. disabled={!editSimilarityCorpus.length}
  462. variant='primary'
  463. >
  464. 保存
  465. </Button>
  466. </div>
  467. </div>
  468. </Modal>
  469. )
  470. }
  471. </div>
  472. )
  473. }
  474. export default DetailModal