Browse Source

意图识别

CzRger 2 months ago
parent
commit
410a700b6a

+ 180 - 125
web/app/components/skill/intent/detail-modal.tsx

@@ -1,14 +1,17 @@
 'use client'
-import React, { useCallback, useState } from 'react'
+import React, { useEffect, useState } from 'react'
 import { RiCloseLine } from '@remixicon/react'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
-import { delCorpusQuestion, fetchIntent, fetchIntentType } from '@/service/common'
+import {
+  addIntent, addIntentKeyword, delBatchIntentKeyword, editIntentKeyword,
+  editIntentType, fetchIntentKeyword,
+  fetchIntentType, getIntent,
+} from '@/service/common'
 import 'react-multi-email/dist/style.css'
 import Input from '@/app/components/base/input'
 import { SimpleSelect } from '@/app/components/base/select'
 import useSWR from 'swr'
-import { v4 as uuid4 } from 'uuid'
 import Checkbox from '@/app/components/base/checkbox'
 import cn from '@/utils/classnames'
 import { useContext } from 'use-context-selector'
@@ -19,15 +22,17 @@ const DetailModal = ({
   transfer,
   onCancel,
   onSend,
+  onRefresh,
 }: any) => {
   const { notify } = useContext(ToastContext)
-  const [questionRelation, setQuestionRelation] = useState<string>('')
-  const [questionFilter, setQuestionFilter] = useState<string>('') // the input value
-  const [question, setQuestion] = useState<string>(transfer.row?.question || '') // the input value
-  const [intentType, setIntentType] = useState<string>(transfer.row?.intentType || '') // the input value
+  const [intentName, setIntentName] = useState<string>(transfer.row?.intentName || '')
+  const [intentType, setIntentType] = useState<string>(transfer.row?.intentType || '')
+  const [corpusList, setCorpusList] = useState<any>([])
+  const [corpusFilter, setCorpusFilter] = useState<string>('')
+  const [keywordsList, setKeywordsList] = useState<any>([])
   const { data: dataOptionsIntentType }: any = useSWR(
     {
-      url: '/xxx',
+      url: '/intentions/types',
       params: {
         page: 1,
         limit: 1000,
@@ -36,109 +41,125 @@ const DetailModal = ({
     fetchIntentType,
   )
   const optionsIntentType: any = dataOptionsIntentType?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
-  const [intentName, setIntentName] = useState<string>(transfer.row?.intentName || '') // the input value
-  const { data: dataOptionsIntentName }: any = useSWR(
-    {
-      url: '/xxx',
+  useEffect(() => {
+    if (transfer.row?.id) {
+      getIntent({ url: `/intentions/${transfer.row.id}` }).then((res: any) => {
+        setIntentType(res.type.id)
+        setIntentName(res.name)
+        setCorpusList(res.corpus)
+        setKeywordsList(res.keywords)
+      })
+    }
+  }, [])
+  const [keyword, setKeyword] = useState<string>('')
+  const [keywordFilter, setKeywordFilter] = useState<string>('')
+  const refreshKeywords = async () => {
+    const res = await fetchIntentKeyword({
+      url: `/intentions/${transfer.row.id}/keywords`,
       params: {
         page: 1,
         limit: 1000,
-        intentType,
       },
-    },
-    fetchIntent,
-  )
-  const optionsIntentName: any = dataOptionsIntentName?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
-  const [questionList, setQuestionList] = useState<any>([{ id: uuid4(), name: '啊啊啊啊啊啊啊啊啊啊' }, { id: uuid4(), name: '啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊' }])
-  const handleAddQuestion = () => {
-    if (!questionRelation)
+    })
+    setKeywordsList(res)
+  }
+  const handleAddKeyword = async () => {
+    if (!keyword)
       return
-
-    if (questionList.some((v: any) => v.name === questionRelation)) {
+    if (keywordsList.some((v: any) => v.name === keyword)) {
       notify({ type: 'warning', message: '请勿新增重复数据!' })
       return
     }
-    setQuestionList([...questionList, { id: uuid4(), name: questionRelation }])
-    setQuestionRelation('')
+    const { id }: any = await addIntentKeyword({
+      url: `/intentions/${transfer.row.id}/keywords`,
+      body: {
+        name: keyword,
+      },
+    })
+    if (id) {
+      await refreshKeywords()
+      setKeyword('')
+    }
   }
-  const [questionSelectMap, setQuestionSelectMap] = useState<any>(new Map())
-  const addQuestionSelectMap = (key: any, value: any) => {
-    setQuestionSelectMap((prevMap: any) => {
+  const [keywordSelectMap, setKeywordsSelectMap] = useState<any>(new Map())
+  const addKeywordsSelectMap = (key: any, value: any) => {
+    setKeywordsSelectMap((prevMap: any) => {
       const newMap = new Map(prevMap)
       newMap.set(key, value)
       return newMap
     })
   }
-  const delQuestionSelectMap = (key: any) => {
-    setQuestionSelectMap((prevMap: any) => {
+  const delKeywordsSelectMap = (key: any) => {
+    setKeywordsSelectMap((prevMap: any) => {
       const newMap = new Map(prevMap)
       newMap.delete(key)
       return newMap
     })
   }
+  const [keywordRow, setKeywordRow] = useState<any>({})
+  const [showKeywordEdit, setShowKeywordEdit] = useState(false)
+  const [editKeyword, setEditKeyword] = useState<string>('')
+  const handleSaveKeyword = async () => {
+    if (!editKeyword)
+      return
+    if (keywordsList.some((v: any) => v.name === editKeyword)) {
+      notify({ type: 'warning', message: '请勿新增重复数据!' })
+      return
+    }
+    const { id }: any = await editIntentKeyword({
+      url: `intentions/keywords/${keywordRow.id}`,
+      body: {
+        name: editKeyword,
+        intention_id: keywordRow.intention_id,
+      },
+    })
+    if (id) {
+      await refreshKeywords()
+      setShowKeywordEdit(false)
+    }
+  }
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
-  const [row, setRow] = useState<any>({})
-  const handleDelQuestion = async () => {
+  const [delBatch, setDelBatch] = useState(false)
+  const handleDelKeyword = async () => {
     try {
-      await delCorpusQuestion({
-        url: `/xxx/${row.id}`,
-        body: {},
+      await delBatchIntentKeyword({
+        url: '/intentions/keywords/batch',
+        body: {
+          method: 'delete',
+          delete_data: delBatch ? Array.from(keywordSelectMap.keys()) : [keywordRow.id],
+        },
       })
       setShowConfirmDelete(false)
-      // mutate()
+      setKeywordsSelectMap(new Map())
+      refreshKeywords()
+    }
+    catch (e) { }
+  }
+  const handleSave = async () => {
+    try {
+      let res
+      if (transfer.mode === 'add') {
+        res = await addIntent({
+          url: '/intentions',
+          body: { type_id: intentType, name: intentName },
+        })
+      }
+      else {
+        res = await editIntentType({
+          url: `/intentions/${transfer.row.id}`,
+          body: { type_id: intentType, name: intentName },
+        })
+      }
+      const { id }: any = res
+      if (id) {
+        if (transfer.mode === 'add')
+          onRefresh(id)
+        else
+          onSend()
+      }
     }
     catch (e) { }
   }
-  const [showQuestionEdit, setShowQuestionEdit] = useState(false)
-  const [editQuestion, setEditQuestion] = useState<string>('')
-  const [corpusRow, setCorpusRow] = useState<any>({})
-  const [corpusRowConfig, setCorpusRowConfig] = useState<string>('')
-  const handleSave = useCallback(async () => {
-    // try {
-    //   let res
-    //   if (transfer.mode === 'add') {
-    //     res = await addCorpus({
-    //       url: '/xxx',
-    //       body: { name, type: 'knowledge_category' },
-    //     })
-    //   }
-    //   else {
-    //     res = await editCorpus({
-    //       url: '/xxx',
-    //       body: { name },
-    //     })
-    //   }
-    //   const { id }: any = res
-    //   if (id) {
-    //     onCancel()
-    //     onSend()
-    //   }
-    // }
-    // catch (e) { }
-  }, [name, onCancel, onSend, transfer])
-  const handleSaveQuestion = useCallback(async () => {
-    // try {
-    //   let res
-    //   if (transfer.mode === 'add') {
-    //     res = await addCorpus({
-    //       url: '/xxx',
-    //       body: { name, type: 'knowledge_category' },
-    //     })
-    //   }
-    //   else {
-    //     res = await editCorpus({
-    //       url: '/xxx',
-    //       body: { name },
-    //     })
-    //   }
-    //   const { id }: any = res
-    //   if (id) {
-    //     onCancel()
-    //     onSend()
-    //   }
-    // }
-    // catch (e) { }
-  }, [name, onCancel, onSend, transfer])
   return (
     <div>
       <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
@@ -156,7 +177,6 @@ const DetailModal = ({
                   defaultValue={intentType}
                   onSelect={(i: any) => {
                     setIntentType(i.value)
-                    setIntentName('')
                   }}
                   items={optionsIntentType}
                   allowSearch={false}
@@ -169,9 +189,9 @@ const DetailModal = ({
               <div className="flex-1">
                 <Input
                   showClearIcon
-                  value={question}
-                  onChange={e => setQuestion(e.target.value)}
-                  onClear={() => setQuestion('')}
+                  value={intentName}
+                  onChange={e => setIntentName(e.target.value)}
+                  onClear={() => setIntentName('')}
                 />
               </div>
             </div>
@@ -182,11 +202,11 @@ const DetailModal = ({
                   <div className="flex-1">
                     <Input
                       showClearIcon
-                      value={questionRelation}
-                      onChange={e => setQuestionRelation(e.target.value)}
-                      onClear={() => setQuestionRelation('')}
+                      value={keyword}
+                      onChange={e => setKeyword(e.target.value)}
+                      onClear={() => setKeyword('')}
                       placeholder='输入后Enter以添加'
-                      onEnter={handleAddQuestion}
+                      onEnter={handleAddKeyword}
                     />
                   </div>
                 </div>
@@ -200,57 +220,60 @@ const DetailModal = ({
                   <div className='flex items-center' onClick={e => e.stopPropagation()}>
                     <Checkbox
                       className='mr-2 shrink-0'
-                      checked={questionList.every((v: any) => questionSelectMap.has(v.id))}
+                      checked={keywordsList.every((v: any) => keywordSelectMap.has(v.id))}
                       onCheck={() => {
-                        questionList.every((v: any) => questionSelectMap.has(v.id))
-                          ? setQuestionSelectMap(new Map())
-                          : questionList.forEach((v: any) => addQuestionSelectMap(v.id, v))
+                        keywordsList.every((v: any) => keywordSelectMap.has(v.id))
+                          ? setKeywordsSelectMap(new Map())
+                          : keywordsList.forEach((v: any) => addKeywordsSelectMap(v.id, v))
                       }}
-                      disabled={questionList.length === 0}
+                      disabled={keywordsList.length === 0}
                     />
                     全选
                   </div>
                   <div className="ml-auto w-[200px]">
                     <Input
                       showClearIcon
-                      value={questionFilter}
-                      onChange={e => setQuestionFilter(e.target.value)}
-                      onClear={() => setQuestionFilter('')}
-                      placeholder='请输入相似问题名称进行过滤'
+                      value={keywordFilter}
+                      onChange={e => setKeywordFilter(e.target.value)}
+                      onClear={() => setKeywordFilter('')}
+                      placeholder='请输入关键词名称进行过滤'
                     />
                   </div>
-                  <Button variant='primary' className={cn('shrink-0')}>
+                  <Button variant='primary' className={cn('shrink-0')} onClick={() => {
+                    setDelBatch(true)
+                    setShowConfirmDelete(true)
+                  }}>
                     批量删除
                   </Button>
                 </div>
                 <div className="flex h-[150px] flex-col gap-2 overflow-y-auto border-2 border-solid border-[#F6F8FC] p-2">
                   {
-                    questionList.filter((v: any) => !questionFilter || v.name.includes(questionFilter)).map((item: any) => (
+                    keywordsList.filter((v: any) => !keywordFilter || v.name.includes(keywordFilter)).map((item: any) => (
                       <div key={item.id} className="flex items-center">
                         <Checkbox
                           className='mr-2 shrink-0'
-                          checked={questionSelectMap.has(item.id)}
+                          checked={keywordSelectMap.has(item.id)}
                           onCheck={() => {
-                            questionSelectMap.has(item.id)
-                              ? delQuestionSelectMap(item.id)
-                              : addQuestionSelectMap(item.id, item)
+                            keywordSelectMap.has(item.id)
+                              ? delKeywordsSelectMap(item.id)
+                              : addKeywordsSelectMap(item.id, item)
                           }}
-                          disabled={questionList.length === 0}
+                          disabled={keywordsList.length === 0}
                         />
                         <div className="flex-1">
                           {item.name}
                         </div>
                         <Button variant='ghost-accent' size='small' className={cn('shrink-0')}
                           onClick={() => {
-                            setRow(item)
-                            setEditQuestion(item.name)
-                            setShowQuestionEdit(true)
+                            setKeywordRow(item)
+                            setEditKeyword(item.name)
+                            setShowKeywordEdit(true)
                           }}>
                           编辑
                         </Button>
                         <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')}
                           onClick={() => {
-                            setRow(item)
+                            setKeywordRow(item)
                             setShowConfirmDelete(true)
                           }}>
                           刪除
@@ -260,17 +283,49 @@ const DetailModal = ({
                   }
                 </div>
                 <div className="flex border-2 border-t-0 border-solid border-[#F6F8FC] p-2 text-xs">
-                  <div>共{questionList.length}条</div>
-                  <div className="ml-4">已选择{questionSelectMap.size}条</div>
+                  <div>共{keywordsList.length}条</div>
+                  <div className="ml-4">已选择{keywordSelectMap.size}条</div>
+                </div>
+              </div>
+            )
+          }
+          {
+            transfer.mode === 'edit' && (
+              <div className="mt-3 flex flex-col">
+                <div className="flex h-10 w-full items-center gap-2 border-2 border-[#F6F8FC] bg-[#F6F8FC] px-2">
+                  <div>语料列表</div>
+                  <div className="ml-auto w-[200px]">
+                    <Input
+                      showClearIcon
+                      value={corpusFilter}
+                      onChange={e => setCorpusFilter(e.target.value)}
+                      onClear={() => setCorpusFilter('')}
+                      placeholder='请输入语料名称进行过滤'
+                    />
+                  </div>
+                </div>
+                <div className="flex h-[150px] flex-col gap-2 overflow-y-auto border-2 border-solid border-[#F6F8FC] p-2">
+                  {
+                    corpusList.filter((v: any) => !corpusFilter || v.name.includes(corpusFilter)).map((item: any) => (
+                      <div key={item.id} className="flex items-center">
+                        <div className="flex-1">
+                          {item.name}
+                        </div>
+                      </div>
+                    ))
+                  }
+                </div>
+                <div className="flex border-2 border-t-0 border-solid border-[#F6F8FC] p-2 text-xs">
+                  <div>共{corpusList.length}条</div>
                 </div>
               </div>
             )
           }
           <Button
             tabIndex={0}
-            className='w-full'
+            className='mt-2 w-full'
             onClick={handleSave}
-            disabled={!question.length || !intentType.length || !intentName.length}
+            disabled={!intentType.length || !intentName.length}
             variant='primary'
           >
             保存
@@ -278,11 +333,11 @@ const DetailModal = ({
         </div>
       </Modal>
       {
-        showQuestionEdit && (
+        showKeywordEdit && (
           <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[400px]">
             <div className='mb-2 flex justify-between'>
               <div className='text-xl font-semibold text-text-primary'>编辑关键词</div>
-              <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowQuestionEdit(false)} />
+              <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={() => setShowKeywordEdit(false)} />
             </div>
             <div>
               <div className={cn('flex flex-wrap items-center justify-between py-4')}>
@@ -290,8 +345,8 @@ const DetailModal = ({
                   关键词
                 </div>
                 <Input
-                  value={editQuestion}
-                  onChange={e => setEditQuestion(e.target.value)}
+                  value={editKeyword}
+                  onChange={e => setEditKeyword(e.target.value)}
                   className='h-9'
                   placeholder='请输入关键词'
                 />
@@ -299,8 +354,8 @@ const DetailModal = ({
               <Button
                 tabIndex={0}
                 className='w-full'
-                onClick={handleSaveQuestion}
-                disabled={!editQuestion.length}
+                onClick={handleSaveKeyword}
+                disabled={!editKeyword.length}
                 variant='primary'
               >
                 保存
@@ -312,9 +367,9 @@ const DetailModal = ({
       {showConfirmDelete && (
         <Confirm
           title="删除确认"
-          content={`请确认是否删除${row.name}?`}
+          content={`请确认是否删除${delBatch ? `${keywordSelectMap.size}条关键词` : keywordRow.name}?`}
           isShow={showConfirmDelete}
-          onConfirm={handleDelQuestion}
+          onConfirm={handleDelKeyword}
           onCancel={() => setShowConfirmDelete(false)}
         />
       )}

+ 10 - 1
web/app/components/skill/intent/index.tsx

@@ -153,11 +153,20 @@ const CorpusIndex = () => {
         detailModalVisible && (
           <DetailModal
             transfer={transfer}
-            onCancel={() => setDetailModalVisible(false)}
+            onCancel={() => {
+              setDetailModalVisible(false)
+              handleSearch()
+            }}
             onSend={() => {
               setDetailModalVisible(false)
               handleSearch()
             }}
+            onRefresh={(id: string) => {
+              setTransfer({
+                mode: 'edit',
+                row: { id },
+              })
+            }}
           />
         )
       }

+ 7 - 4
web/app/components/skill/intent/list.tsx

@@ -4,7 +4,7 @@ import type { FC } from 'react'
 import { useState } from 'react'
 import Button from '@/app/components/base/button'
 import cn from '@/utils/classnames'
-import { delCorpus } from '@/service/common'
+import { delIntent } from '@/service/common'
 import Confirm from '@/app/components/base/confirm'
 import DetailModal from './detail-modal'
 import useTimestamp from '@/hooks/use-timestamp'
@@ -27,8 +27,8 @@ const IntentPageList: FC<PageListProps> = ({
   const [row, setRow] = useState<any>({})
   const handleDel = async () => {
     try {
-      await delCorpus({
-        url: `/tags/${row.id}`,
+      await delIntent({
+        url: `/intentions/${row.id}`,
         body: {},
       })
       setShowConfirmDelete(false)
@@ -111,7 +111,10 @@ const IntentPageList: FC<PageListProps> = ({
         detailModalVisible && (
           <DetailModal
             transfer={transfer}
-            onCancel={() => setDetailModalVisible(false)}
+            onCancel={() => {
+              setDetailModalVisible(false)
+              onUpdate()
+            }}
             onSend={() => {
               setDetailModalVisible(false)
               onUpdate()

+ 25 - 48
web/service/common.ts

@@ -104,64 +104,16 @@ export const fetchMembers: Fetcher<{ accounts: Member[] | null }, { url: string;
 export const fetchTypes = ({ url, params }: any) => {
   console.log('查询类型列表')
   return get(url, { params })
-  // return new Promise((resolve) => {
-  //   setTimeout(() => {
-  //     const arr: any = []
-  //     for (let i = 1; i < 10; i++) {
-  //       arr.push({
-  //         id: i,
-  //         name: `类型${i}`,
-  //         relation: i % 2,
-  //       })
-  //     }
-  //     resolve({
-  //       data: arr,
-  //     })
-  //   }, 1000)
-  // })
 }
 
 export const fetchKnowledges = ({ url, params }: any) => {
   console.log('查询知识服务', params, url)
   return get(url, { params })
-  // return new Promise((resolve) => {
-  //   setTimeout(() => {
-  //     const arr: any = []
-  //     for (let i = 1; i < 10; i++) {
-  //       arr.push({
-  //         id: i,
-  //         serviceType: i % 3 + 1,
-  //         serviceName: `深圳口岸服务网${i}`,
-  //         url: '74.10.28.118',
-  //         status: i % 2,
-  //         time: '2021-09-22 14:22:22',
-  //       })
-  //     }
-  //     resolve({
-  //       data: arr,
-  //     })
-  //   }, 1000)
-  // })
 }
 
 export const fetchMoulds = ({ url, params }: any) => {
   console.log('查询知识库模板', params, url)
   return get<{ accounts: Member[] | null }>(url, { params })
-  // return new Promise((resolve) => {
-  //   setTimeout(() => {
-  //     const arr: any = []
-  //     for (let i = 1; i < 2; i++) {
-  //       // /files/7be8a2ca-a633-473c-8bcf-b73ee9fd7738/file-preview?timestamp=1744947616&nonce=a4107f3e163bbb0da255914e2ccabaf3&sign=H82Wk2VbWjXXEyzRUhGyNQlR-PG8NEGy5ijCEF0IKgo=
-  //       arr.push({
-  //         id: '7be8a2ca-a633-473c-8bcf-b73ee9fd7738',
-  //         fileName: `文件${i}.doc`,
-  //       })
-  //     }
-  //     resolve({
-  //       data: arr,
-  //     })
-  //   }, 1000)
-  // })
 }
 
 export const fetchProviders: Fetcher<Provider[] | null, { url: string; params: Record<string, any> }> = ({ url, params }) => {
@@ -536,6 +488,7 @@ export const fetchIntent = ({ url, params }: any) => {
   console.log('查询意图', url, params)
   return get(url, { params })
 }
+
 export const addIntent = ({ url, body }: any) => {
   console.log('新增意图', url, body)
   return post(url, { body })
@@ -551,6 +504,30 @@ export const delIntent = ({ url, body }: any) => {
   return del(url, { body })
 }
 
+export const getIntent = ({ url, body }: any) => {
+  console.log('详情意图', url, body)
+  return get(url, { body })
+}
+
+export const fetchIntentKeyword = ({ url, params }: any) => {
+  console.log('查询意图关键词', url, params)
+  return get(url, { params })
+}
+
+export const addIntentKeyword = ({ url, body }: any) => {
+  console.log('新增意图关键词', url, body)
+  return post(url, { body })
+}
+
+export const editIntentKeyword = ({ url, body }: any) => {
+  console.log('编辑意图关键词', url, body)
+  return patch(url, { body })
+}
+
+export const delBatchIntentKeyword = ({ url, body }: any) => {
+  console.log('批量删除意图关键词', url, body)
+  return post(url, { body })
+}
 export const fetchCorpus = ({ url, params }: any) => {
   console.log('查询训练语料列表', url, params)
   // return get(url, { params })