Explorar el Código

Merge branch '1.1.3-master_web' of http://8.130.72.63:18081/shenzhen/tjdify into 1.1.3-master

CzRger hace 3 meses
padre
commit
95044ad439
Se han modificado 30 ficheros con 2607 adiciones y 686 borrados
  1. 31 3
      web/app/(commonLayout)/apps/Apps.tsx
  2. 29 3
      web/app/(commonLayout)/datasets/Container.tsx
  3. 6 1
      web/app/(commonLayout)/datasets/Datasets.tsx
  4. 1 1
      web/app/components/datasets/documents/mould/index.tsx
  5. 84 3
      web/app/components/datasets/rename-modal/index.tsx
  6. 54 2
      web/app/components/explore/create-app-modal/index.tsx
  7. 4 0
      web/app/components/header/account-setting/dept-page/detail-modal/index.module.css
  8. 106 0
      web/app/components/header/account-setting/dept-page/detail-modal/index.tsx
  9. 230 0
      web/app/components/header/account-setting/dept-page/index.tsx
  10. 237 0
      web/app/components/header/account-setting/dept-page/user-modal.tsx
  11. 9 1
      web/app/components/header/account-setting/index.tsx
  12. 1 1
      web/app/components/header/account-setting/knowledges-page/index.tsx
  13. 1 1
      web/app/components/header/account-setting/types-page/index.tsx
  14. 19 8
      web/app/components/header/index.tsx
  15. 249 195
      web/app/components/skill/corpus/detail-modal.tsx
  16. 88 77
      web/app/components/skill/corpus/index.tsx
  17. 15 8
      web/app/components/skill/corpus/list.tsx
  18. 211 127
      web/app/components/skill/intent/detail-modal.tsx
  19. 60 59
      web/app/components/skill/intent/index.tsx
  20. 16 9
      web/app/components/skill/intent/list.tsx
  21. 52 44
      web/app/components/skill/intent/type-modal.tsx
  22. 0 25
      web/app/components/workflow/hooks/use-workflow.ts
  23. 17 3
      web/app/layout.tsx
  24. 4 0
      web/app/styles/cus.scss
  25. 1 1
      web/app/styles/globals.css
  26. 2 0
      web/models/datasets.ts
  27. 1 0
      web/package.json
  28. 911 0
      web/pnpm-lock.yaml
  29. 148 114
      web/service/common.ts
  30. 20 0
      web/utils/fix.ts

+ 31 - 3
web/app/(commonLayout)/apps/Apps.tsx

@@ -29,6 +29,8 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
 import TagManagementModal from '@/app/components/base/tag-management'
 import TagFilter from '@/app/components/base/tag-management/filter'
 import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
+import { fetchDepts } from '@/service/common'
+import { TreeSelect as AntdTreeSelect } from 'antd'
 
 const getKey = (
   pageIndex: number,
@@ -37,6 +39,7 @@ const getKey = (
   isCreatedByMe: boolean,
   tags: string[],
   keywords: string,
+  dept: string,
 ) => {
   if (!pageIndex || previousPageData.has_more) {
     const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords, is_created_by_me: isCreatedByMe } }
@@ -48,7 +51,8 @@ const getKey = (
 
     if (tags.length)
       params.params.tag_ids = tags
-
+    if (dept)
+      params.params.dept = dept
     return params
   }
   return null
@@ -72,9 +76,21 @@ const Apps = () => {
   const setTagIDs = useCallback((tagIDs: string[]) => {
     setQuery(prev => ({ ...prev, tagIDs }))
   }, [setQuery])
-
+  const [dept, setDept] = useState<any>()
+  const [optionsDept, setOptionsDept] = useState<any>([])
+  useEffect(() => {
+    fetchDepts({
+      url: '/xxx',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    }).then((res: any) => {
+      setOptionsDept(res.data || [])
+    })
+  }, [])
   const { data, isLoading, setSize, mutate } = useSWRInfinite(
-    (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
+    (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords, dept),
     fetchAppList,
     { revalidateFirstPage: true },
   )
@@ -146,6 +162,18 @@ const Apps = () => {
           options={options}
         />
         <div className='flex items-center gap-2'>
+          <AntdTreeSelect
+            showSearch
+            style={{ width: '200px' }}
+            value={dept}
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            placeholder="请选择部门"
+            allowClear
+            treeDefaultExpandAll
+            onChange={v => setDept(v || '')}
+            treeData={optionsDept}
+            fieldNames={{ label: 'name', value: 'id' }}
+          />
           <CheckboxWithLabel
             className='mr-2'
             label={t('app.showMyCreatedAppsOnly')}

+ 29 - 3
web/app/(commonLayout)/datasets/Container.tsx

@@ -30,8 +30,9 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
 import { useAppContext } from '@/context/app-context'
 import { useExternalApiPanel } from '@/context/external-api-panel-context'
 import { SimpleSelect } from '@/app/components/base/select'
-import { fetchTypes } from '@/service/common'
+import { fetchDepts, fetchTypes } from '@/service/common'
 import Statistic from '@/app/(commonLayout)/datasets/Statistic'
+import { TreeSelect as AntdTreeSelect } from 'antd'
 
 const Container = () => {
   const { t } = useTranslation()
@@ -96,13 +97,26 @@ const Container = () => {
       url: '/tags/page',
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
         tag_type: 'knowledge_category',
       },
     }).then((res: any) => {
       setOptionsType(res.data.map((v: any) => ({ name: v.name, value: v.id })) || [])
     })
   }, [])
+  const [dept, setDept] = useState<any>()
+  const [optionsDept, setOptionsDept] = useState<any>([])
+  useEffect(() => {
+    fetchDepts({
+      url: '/xxx',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    }).then((res: any) => {
+      setOptionsDept(res.data || [])
+    })
+  }, [])
   return (
     <div ref={containerRef} className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'>
       <div className='sticky top-0 z-10 flex flex-wrap justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
@@ -113,6 +127,18 @@ const Container = () => {
         />
         {activeTab === 'dataset' && (
           <div className='flex items-center justify-center gap-2'>
+            <AntdTreeSelect
+              showSearch
+              style={{ width: '200px' }}
+              value={dept}
+              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+              placeholder="请选择部门"
+              allowClear
+              treeDefaultExpandAll
+              onChange={v => setDept(v || '')}
+              treeData={optionsDept}
+              fieldNames={{ label: 'name', value: 'id' }}
+            />
             <SimpleSelect
               wrapperClassName="h-[32px] w-[200px]"
               defaultValue={type}
@@ -154,7 +180,7 @@ const Container = () => {
       </div>
       {activeTab === 'dataset' && (
         <>
-          <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} type={searchType} />
+          <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} type={searchType} dept={dept} />
           <DatasetFooter />
           {showTagManagementModal && (
             <TagManagementModal type='knowledge' show={showTagManagementModal} />

+ 6 - 1
web/app/(commonLayout)/datasets/Datasets.tsx

@@ -17,6 +17,7 @@ const getKey = (
   keyword: string,
   includeAll: boolean,
   type: string,
+  dept: string,
 ) => {
   if (!pageIndex || previousPageData.has_more) {
     const params: FetchDatasetsParams = {
@@ -33,6 +34,8 @@ const getKey = (
       params.params.keyword = keyword
     if (type)
       params.params.category_ids = [type]
+    if (dept)
+      params.params.dept = dept
     return params
   }
   return null
@@ -44,6 +47,7 @@ type Props = {
   keywords: string
   includeAll: boolean,
   type: string,
+  dept: string,
 }
 
 const Datasets = ({
@@ -52,10 +56,11 @@ const Datasets = ({
   keywords,
   includeAll,
   type,
+  dept,
 }: Props) => {
   const { isCurrentWorkspaceEditor } = useAppContext()
   const { data, isLoading, setSize, mutate } = useSWRInfinite(
-    (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll, type),
+    (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll, type, dept),
     fetchDatasets,
     { revalidateFirstPage: false, revalidateAll: true },
   )

+ 1 - 1
web/app/components/datasets/documents/mould/index.tsx

@@ -71,7 +71,7 @@ const DetailModel = ({
       url: `/datasets/${datasetId}/templates`,
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
       },
     },
     fetchMoulds,

+ 84 - 3
web/app/components/datasets/rename-modal/index.tsx

@@ -1,6 +1,7 @@
 'use client'
 
 import type { MouseEventHandler } from 'react'
+import React from 'react'
 import { useEffect } from 'react'
 import { useState } from 'react'
 import { RiCloseLine } from '@remixicon/react'
@@ -16,7 +17,8 @@ import { ToastContext } from '@/app/components/base/toast'
 import type { DataSet } from '@/models/datasets'
 import { tagBindingsCreate, tagBindingsRemove, updateDatasetSetting } from '@/service/datasets'
 import { useModalContext } from '@/context/modal-context'
-import { fetchTypes } from '@/service/common'
+import { fetchDeptUsers, fetchTypes } from '@/service/common'
+import { TreeSelect as AntdTreeSelect } from 'antd'
 
 type RenameDatasetModalProps = {
   show: boolean
@@ -40,7 +42,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
       url: '/tags/page',
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
         tag_type: 'knowledge_category',
       },
     }).then((res: any) => {
@@ -98,7 +100,37 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
     }
   }
   const { setShowAccountSettingModal } = useModalContext()
-
+  const optionsEditAuth = [
+    { name: '本账号', value: 1 },
+    { name: '本部门', value: 2 },
+  ]
+  const [editAuth, setEditAuth] = useState()
+  const [editUserIds, setEditUserIds] = useState([])
+  const [lookUserIds, setLookUserIds] = useState([])
+  const [optionsDeptUser, setOptionsDeptUser] = useState<any>([])
+  const [optionsDeptUserEdit, setOptionsDeptUserEdit] = useState<any>([])
+  useEffect(() => {
+    fetchDeptUsers({
+      url: '/xxx',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    }).then((res: any) => {
+      setOptionsDeptUser(res.data || [])
+    })
+  }, [])
+  useEffect(() => {
+    fetchDeptUsers({
+      url: '/xxx',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    }).then((res: any) => {
+      setOptionsDeptUserEdit(res.data || [])
+    })
+  }, [])
   return (
     <Modal
       className='w-[520px] max-w-[520px] rounded-xl px-8 py-6'
@@ -151,6 +183,55 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
             />
           </div>
         </div>
+        <div className='pt-2'>
+          <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑权限</div>
+          <div className="h-[32px]">
+            <SimpleSelect
+              className="h-[32px]"
+              defaultValue={editAuth}
+              onSelect={(i) => {
+                setEditAuth(i.value)
+              }}
+              items={optionsEditAuth}
+              allowSearch={false}
+              placeholder="请选择编辑权限"
+            />
+          </div>
+        </div>
+        <div className='pt-2'>
+          <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑授权</div>
+          <AntdTreeSelect
+            showSearch
+            style={{ width: '100%' }}
+            value={editUserIds}
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            placeholder="请选择编辑授权"
+            allowClear
+            treeDefaultExpandAll
+            onChange={v => setEditUserIds(v)}
+            treeData={optionsDeptUserEdit}
+            fieldNames={{ label: 'name', value: 'id' }}
+            multiple={true}
+            treeCheckable={true}
+          />
+        </div>
+        <div className='pt-2'>
+          <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>可见授权</div>
+          <AntdTreeSelect
+            showSearch
+            style={{ width: '100%' }}
+            value={lookUserIds}
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            placeholder="请选择可见授权"
+            allowClear
+            treeDefaultExpandAll
+            onChange={v => setLookUserIds(v)}
+            treeData={optionsDeptUser}
+            fieldNames={{ label: 'name', value: 'id' }}
+            multiple={true}
+            treeCheckable={true}
+          />
+        </div>
       </div>
       <div className='flex justify-end pt-6'>
         <Button className='mr-2' onClick={onClose}>{t('common.operation.cancel')}</Button>

+ 54 - 2
web/app/components/explore/create-app-modal/index.tsx

@@ -1,5 +1,5 @@
 'use client'
-import React, { useState } from 'react'
+import React, { useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { RiCloseLine } from '@remixicon/react'
 import AppIconPicker from '../../base/app-icon-picker'
@@ -13,6 +13,9 @@ import AppIcon from '@/app/components/base/app-icon'
 import { useProviderContext } from '@/context/provider-context'
 import AppsFull from '@/app/components/billing/apps-full-in-dialog'
 import type { AppIconType } from '@/types/app'
+import { SimpleSelect } from '@/app/components/base/select'
+import { TreeSelect as AntdTreeSelect } from 'antd'
+import { fetchDeptUsers } from '@/service/common'
 
 export type CreateAppModalProps = {
   show: boolean
@@ -80,7 +83,24 @@ const CreateAppModal = ({
     })
     onHide()
   }
-
+  const optionsEditAuth = [
+    { name: '本账号', value: 1 },
+    { name: '本部门', value: 2 },
+  ]
+  const [editAuth, setEditAuth] = useState()
+  const [lookUserIds, setLookUserIds] = useState([])
+  const [optionsDeptUser, setOptionsDeptUser] = useState<any>([])
+  useEffect(() => {
+    fetchDeptUsers({
+      url: '/xxx',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    }).then((res: any) => {
+      setOptionsDeptUser(res.data || [])
+    })
+  }, [])
   return (
     <>
       <Modal
@@ -143,6 +163,38 @@ const CreateAppModal = ({
             </div>
           )}
           {!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
+          <div className='pt-2'>
+            <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑权限</div>
+            <div className="h-[32px]">
+              <SimpleSelect
+                className="h-[32px]"
+                defaultValue={editAuth}
+                onSelect={(i) => {
+                  setEditAuth(i.value)
+                }}
+                items={optionsEditAuth}
+                allowSearch={false}
+                placeholder="请选择编辑权限"
+              />
+            </div>
+          </div>
+          <div className='pt-2'>
+            <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>可见授权</div>
+            <AntdTreeSelect
+              showSearch
+              style={{ width: '100%' }}
+              value={lookUserIds}
+              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+              placeholder="请选择可见授权"
+              allowClear
+              treeDefaultExpandAll
+              onChange={v => setLookUserIds(v)}
+              treeData={optionsDeptUser}
+              fieldNames={{ label: 'name', value: 'id' }}
+              multiple={true}
+              treeCheckable={true}
+            />
+          </div>
         </div>
         <div className='flex flex-row-reverse'>
           <Button disabled={!isEditModal && isAppsFull} className='ml-2 w-24' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>

+ 4 - 0
web/app/components/header/account-setting/dept-page/detail-modal/index.module.css

@@ -0,0 +1,4 @@
+.modal {
+  padding: 24px 32px !important;
+  width: 400px !important;
+}

+ 106 - 0
web/app/components/header/account-setting/dept-page/detail-modal/index.tsx

@@ -0,0 +1,106 @@
+'use client'
+import { useCallback, useEffect, useState } from 'react'
+import { RiCloseLine } from '@remixicon/react'
+import s from './index.module.css'
+import cn from '@/utils/classnames'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import { addKnowledge, editKnowledge, fetchDepts } from '@/service/common'
+import 'react-multi-email/dist/style.css'
+import Input from '@/app/components/base/input'
+import { TreeSelect as AntdTreeSelect } from 'antd'
+
+const InviteModal = ({
+  transfer,
+  onCancel,
+  onSend,
+}: any) => {
+  const [name, setName] = useState<string>(transfer.row?.name || '')
+  const [options, setOptions] = useState<any>([])
+  useEffect(() => {
+    fetchDepts({
+      url: '/xxx',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    }).then((res: any) => {
+      setOptions(res.data || [])
+    })
+  }, [])
+  const handleSave = useCallback(async () => {
+    try {
+      let res: any = () => {}
+      if (transfer.mode === 'add') {
+        res = await addKnowledge({
+          url: '/123',
+          body: { name, status: false },
+        })
+      }
+      else {
+        res = await editKnowledge({
+          url: `/123/${transfer.row.id}`,
+          body: { name },
+        })
+      }
+      const { id }: any = res
+      if (id) {
+        onCancel()
+        onSend()
+      }
+    }
+    catch (e) { }
+  }, [name, onCancel, onSend, transfer])
+
+  return (
+    <div className={cn(s.wrap)}>
+      <Modal overflowVisible isShow onClose={() => { }} className={cn(s.modal)}>
+        <div className='mb-2 flex justify-between'>
+          <div className='text-xl font-semibold text-text-primary'>{transfer.mode === 'add' ? '新增' : '编辑'}部门</div>
+          <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
+        </div>
+        <div>
+          <div className={cn('flex flex-wrap items-center justify-between')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              上级部门
+            </div>
+            <AntdTreeSelect
+              showSearch
+              style={{ width: '100%' }}
+              value={name}
+              dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+              placeholder="请选择上级部门"
+              allowClear
+              treeDefaultExpandAll
+              onChange={v => setName(v || '')}
+              treeData={options}
+              fieldNames={{ label: 'name', value: 'id' }}
+            />
+          </div>
+          <div className={cn('flex flex-wrap items-center justify-between')}>
+            <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+              部门名称
+            </div>
+            <Input
+              value={name}
+              onChange={e => setName(e.target.value)}
+              className='h-9'
+              placeholder='请输入部门名称'
+            />
+          </div>
+          <Button
+            tabIndex={0}
+            className='mt-4 w-full'
+            onClick={handleSave}
+            disabled={!name.length}
+            variant='primary'
+          >
+            保存
+          </Button>
+        </div>
+      </Modal>
+    </div>
+  )
+}
+
+export default InviteModal

+ 230 - 0
web/app/components/header/account-setting/dept-page/index.tsx

@@ -0,0 +1,230 @@
+'use client'
+import { useState } from 'react'
+import useSWR from 'swr'
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import DetailModal from './detail-modal'
+import UserModal from './user-modal'
+import { useContext } from 'use-context-selector'
+import { RiAddLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { delKnowledge, fetchDepts } from '@/service/common'
+import I18n from '@/context/i18n'
+import { useAppContext } from '@/context/app-context'
+import LogoEmbeddedChatHeader from '@/app/components/base/logo/logo-embedded-chat-header'
+import { useProviderContext } from '@/context/provider-context'
+import { Plan } from '@/app/components/billing/type'
+import Button from '@/app/components/base/button'
+import UpgradeBtn from '@/app/components/billing/upgrade-btn'
+import { NUM_INFINITE } from '@/app/components/billing/config'
+import { LanguagesSupported } from '@/i18n/language'
+import cn from '@/utils/classnames'
+import Confirm from '@/app/components/base/confirm'
+import useTimestamp from '@/hooks/use-timestamp'
+import { Column as AntdColumn, Space as AntdSpace, Table as AntdTable } from 'antd'
+dayjs.extend(relativeTime)
+
+const DeptsPage = () => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const { formatTime } = useTimestamp()
+  const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
+  const { data, mutate }: any = useSWR(
+    {
+      url: '/external_applications',
+      params: {
+        page: 1,
+        limit: 99999,
+      },
+    },
+    fetchDepts,
+  )
+  const [detailModalVisible, setDetailModalVisible] = useState(false)
+  const [userModalVisible, setUserModalVisible] = useState(false)
+  const [transfer, setTransfer] = useState<any>({
+    mode: 'add',
+    row: null,
+  })
+  const deptList = data?.data || []
+  const { plan, enableBilling } = useProviderContext()
+  const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
+  const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && deptList.length >= plan.total.teamMembers
+
+  const [showConfirmDelete, setShowConfirmDelete] = useState(false)
+  const [row, setRow] = useState<any>({})
+  const handleDel = async () => {
+    try {
+      await delKnowledge({
+        url: `/external_applications/${row.id}`,
+        body: {},
+      })
+      setShowConfirmDelete(false)
+      mutate()
+    }
+    catch (e) { }
+  }
+  return (
+    <>
+      <div className='flex flex-col'>
+        <div className='mb-4 flex items-center gap-3 rounded-xl border-l-[0.5px] border-t-[0.5px] border-divider-subtle bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-3 pr-5'>
+          <LogoEmbeddedChatHeader className='!h-12 !w-12' />
+          <div className='grow'>
+            <div className='system-md-semibold text-text-secondary'>{currentWorkspace?.name}</div>
+            {enableBilling && (
+              <div className='system-xs-medium mt-1 text-text-tertiary'>
+                {isNotUnlimitedMemberPlan
+                  ? (
+                    <div className='flex space-x-1'>
+                      <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && deptList.length > 1 && 's'}</div>
+                      <div className=''>{deptList.length}</div>
+                      <div>/</div>
+                      <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div>
+                    </div>
+                  )
+                  : (
+                    <div className='flex space-x-1'>
+                      <div>{deptList.length}</div>
+                      <div>{t('billing.plansCommon.memberAfter')}{locale !== LanguagesSupported[1] && deptList.length > 1 && 's'}</div>
+                    </div>
+                  )}
+              </div>
+            )}
+          </div>
+          {isMemberFull && (
+            <UpgradeBtn className='mr-2' loc='member-invite' />
+          )}
+          <Button variant='primary' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull}
+            onClick={() => {
+              setTransfer({ mode: 'add', row: null })
+              setDetailModalVisible(true)
+            }}>
+            <RiAddLine className='mr-1 h-4 w-4' />
+            新增
+          </Button>
+        </div>
+        <div className='overflow-visible lg:overflow-visible'>
+          <AntdTable
+            rowKey="id"
+            dataSource={deptList}
+            pagination={false}
+          >
+            <AntdColumn title="部门名称" dataIndex="name" key="name"/>
+            <AntdColumn title="关联用户数量" dataIndex="relation" key="relation" align="center" width={120}/>
+            <AntdColumn title="操作" key="action" align="center" width={100} render={(_: any, record: any) => (
+              <AntdSpace size="middle">
+                {!(record.children?.length > 0) && (
+                  <Button variant='ghost-accent' size='small' className={cn('shrink-0')}
+                    onClick={() => {
+                      setTransfer({
+                        mode: 'user',
+                        row: JSON.parse(JSON.stringify(record)),
+                      })
+                      setUserModalVisible(true)
+                    }}>
+                    关联用户
+                  </Button>
+                )}
+                <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull}
+                  onClick={() => {
+                    setTransfer({
+                      mode: 'edit',
+                      row: JSON.parse(JSON.stringify(record)),
+                    })
+                    setDetailModalVisible(true)
+                  }}>
+                  编辑
+                </Button>
+                {!(record.children?.length > 0) && !(record.relation > 0) && (
+                  <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} disabled={!isCurrentWorkspaceManager || isMemberFull} onClick={() => {
+                    setRow(record)
+                    setShowConfirmDelete(true)
+                  }}>
+                    刪除
+                  </Button>
+                )}
+              </AntdSpace>
+            )}/>
+          </AntdTable>
+          {/* <div className='flex min-w-[480px] items-center border-b border-divider-regular py-[7px]'> */}
+          {/*  <div className='system-xs-medium-uppercase shrink-0 grow text-text-tertiary'>部门名称</div> */}
+          {/*  <div className='system-xs-medium-uppercase shrink-0 w-[200px] text-center text-text-tertiary'>关联用户数量</div> */}
+          {/*  <div className='system-xs-medium-uppercase w-[200px] shrink-0 px-3 text-center text-text-tertiary'>操作</div> */}
+          {/* </div> */}
+          {/* <div className='relative min-w-[480px]'> */}
+          {/*  { */}
+          {/*    deptList.map((dept: any) => ( */}
+          {/*      <div key={dept.id} className='flex justify-between border-b border-divider-subtle'> */}
+          {/*        <div className='system-sm-regular shrink-0 grow py-2 text-text-secondary'>{dept.name}</div> */}
+          {/*        <div className='system-sm-regular shrink-0 w-[200px] py-2 text-center text-text-secondary'>0</div> */}
+          {/*        <div className='flex w-[200px] shrink-0 items-center justify-center'> */}
+          {/*          <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull} */}
+          {/*            onClick={() => { */}
+          {/*              setTransfer({ */}
+          {/*                mode: 'user', */}
+          {/*                row: JSON.parse(JSON.stringify(dept)), */}
+          {/*              }) */}
+          {/*              setUserModalVisible(true) */}
+          {/*            }}> */}
+          {/*            关联用户 */}
+          {/*          </Button> */}
+          {/*          <Button variant='ghost-accent' size='small' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull} */}
+          {/*            onClick={() => { */}
+          {/*              setTransfer({ */}
+          {/*                mode: 'edit', */}
+          {/*                row: JSON.parse(JSON.stringify(dept)), */}
+          {/*              }) */}
+          {/*              setDetailModalVisible(true) */}
+          {/*            }}> */}
+          {/*            编辑 */}
+          {/*          </Button> */}
+          {/*          <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} disabled={!isCurrentWorkspaceManager || isMemberFull} onClick={() => { */}
+          {/*            setRow(dept) */}
+          {/*            setShowConfirmDelete(true) */}
+          {/*          }}> */}
+          {/*            刪除 */}
+          {/*          </Button> */}
+          {/*        </div> */}
+          {/*      </div> */}
+          {/*    )) */}
+          {/*  } */}
+          {/* </div> */}
+        </div>
+      </div>
+      {
+        detailModalVisible && (
+          <DetailModal
+            transfer={transfer}
+            onCancel={() => setDetailModalVisible(false)}
+            onSend={() => {
+              mutate()
+            }}
+          />
+        )
+      }
+      {
+        userModalVisible && (
+          <UserModal
+            transfer={transfer}
+            onCancel={() => {
+              setUserModalVisible(false)
+              mutate()
+            }}
+            onSend={() => mutate()}
+          />
+        )
+      }
+      {showConfirmDelete && (
+        <Confirm
+          title="删除确认"
+          content={`请确认是否删除${row.name}?`}
+          isShow={showConfirmDelete}
+          onConfirm={handleDel}
+          onCancel={() => setShowConfirmDelete(false)}
+        />
+      )}
+    </>
+  )
+}
+
+export default DeptsPage

+ 237 - 0
web/app/components/header/account-setting/dept-page/user-modal.tsx

@@ -0,0 +1,237 @@
+'use client'
+import React, { useCallback, useEffect, useState } from 'react'
+import { RiAddLine, RiCloseLine, RiRefreshLine, RiSearchLine } from '@remixicon/react'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import { delCorpus, fetchIntentType, fetchTypes } from '@/service/common'
+import 'react-multi-email/dist/style.css'
+import Input from '@/app/components/base/input'
+import useSWR from 'swr'
+import cn from '@/utils/classnames'
+import Confirm from '@/app/components/base/confirm'
+import Pagination from '@/app/components/base/pagination'
+import { SimpleSelect } from '@/app/components/base/select'
+
+const TypeModal = ({
+  onCancel,
+  onSend,
+}: any) => {
+  const [page, setPage] = React.useState<number>(0)
+  const [limit, setLimit] = useState<number>(10)
+  const [name, setName] = useState('')
+  const [query, setQuery] = useState<any>({})
+  const { data, mutate }: any = useSWR(
+    {
+      url: '/xxx',
+      params: {
+        page: page + 1,
+        limit,
+        ...query,
+      },
+    },
+    fetchIntentType,
+  )
+  const list: any = data?.data || []
+  const total = data?.total || 0
+  const handleSearch = (reset = false) => {
+    if (reset)
+      setIntentType('')
+
+    const params: any = {}
+    if (intentType)
+      params.intentType = intentType
+    setQuery(params)
+    setPage(0)
+  }
+  useEffect(() => {
+    mutate()
+  }, [page, limit])
+  const [showConfirmDelete, setShowConfirmDelete] = useState(false)
+  const [row, setRow] = useState<any>({})
+  const handleDel = async () => {
+    try {
+      await delCorpus({
+        url: `/tags/${row.id}`,
+        body: {},
+      })
+      setShowConfirmDelete(false)
+    }
+    catch (e) { }
+  }
+  const [showDetail, setShowDetail] = useState(false)
+  const [mode, setMode] = useState<any>('add')
+  const [editIntentType, setEditIntentType] = useState('')
+  const [selectUser, setSelectUser] = useState<any>('')
+  const [userOptions, setUserOptions] = useState<any>([])
+  useEffect(() => {
+    fetchTypes({
+      url: '/tags/page',
+      params: {
+        page: 1,
+        limit: 99999,
+        tag_type: 'knowledge_category',
+      },
+    }).then((res: any) => {
+      setUserOptions(res.data.map((v: any) => ({ name: v.name, value: v.id })) || [])
+    })
+  }, [])
+  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) { }
+  }, [mode, editIntentType, onCancel, onSend])
+  return (
+    <div>
+      <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
+        <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={onCancel} />
+        </div>
+        <div className='flex h-[600px] flex-col'>
+          <div className="flex items-center gap-2">
+            <div className="flex shrink-0 items-center text-gray-500">
+              用户名称
+              <Input
+                className="ml-2"
+                showClearIcon
+                wrapperClassName='!w-[200px]'
+                value={name}
+                onChange={e => setName(e.target.value)}
+                onClear={() => setName('')}
+              />
+            </div>
+            <Button variant='primary' className={cn('ml-auto shrink-0')} onClick={() => {
+              handleSearch(false)
+            }}>
+              <RiSearchLine className='mr-1 h-4 w-4' />
+              搜索
+            </Button>
+            <Button variant='primary' className={cn('shrink-0')} onClick={() => {
+              handleSearch(true)
+            }}>
+              <RiRefreshLine className='mr-1 h-4 w-4' />
+              重置
+            </Button>
+          </div>
+          <div className="mt-2">
+            <Button variant='primary' className={cn('shrink-0')}
+              onClick={() => {
+                setMode('add')
+                setEditIntentType('')
+                setShowDetail(true)
+              }}>
+              <RiAddLine className='mr-1 h-4 w-4' />
+              添加用户
+            </Button>
+          </div>
+          <div className="flex-1">
+            <div className='relative flex h-full w-full flex-col'>
+              <div className='relative grow overflow-x-auto'>
+                <table className={'mt-3 w-full min-w-[700px] max-w-full border-collapse border-0 text-sm'}>
+                  <thead className="h-8 border-b border-divider-subtle text-xs font-medium uppercase leading-8 text-text-tertiary">
+                    <tr>
+                      <td>用户名称</td>
+                      <td className="w-[120px] text-center">操作</td>
+                    </tr>
+                  </thead>
+                  <tbody className="text-text-secondary">
+                    {list.map((item: any) => (
+                      <tr
+                        key={item.id}
+                        className={'h-8 border-b border-divider-subtle hover:bg-background-default-hover'}
+                      >
+                        <td>{item.name}</td>
+                        <td className="flex justify-center gap-2">
+                          <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} onClick={() => {
+                            setRow(item)
+                            setShowConfirmDelete(true)
+                          }}>
+                            解除关联
+                          </Button>
+                        </td>
+                      </tr>
+                    ))}
+                  </tbody>
+                </table>
+              </div>
+              {/* Show Pagination only if the total is more than the limit */}
+              {total && (
+                <Pagination
+                  total={total}
+                  limit={limit}
+                  onLimitChange={setLimit}
+                  current={page}
+                  onChange={setPage}
+                  className='w-full shrink-0 px-0 pb-0'
+                />
+              )}
+            </div>
+          </div>
+        </div>
+      </Modal>
+      {showConfirmDelete && (
+        <Confirm
+          title="删除确认"
+          content={`请确认是否删除${row.name}?`}
+          isShow={showConfirmDelete}
+          onConfirm={handleDel}
+          onCancel={() => setShowConfirmDelete(false)}
+        />
+      )}
+      {
+        showDetail && (
+          <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={() => setShowDetail(false)} />
+            </div>
+            <div>
+              <div className={cn('flex flex-wrap items-center justify-between py-4')}>
+                <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+                  选择用户
+                </div>
+                <div className='w-full'>
+                  <SimpleSelect
+                    defaultValue={selectUser}
+                    onSelect={(i) => { setSelectUser(i.value) }}
+                    items={userOptions}
+                    allowSearch={false}
+                  />
+                </div>
+              </div>
+              <Button
+                tabIndex={0}
+                className='w-full'
+                onClick={handleSave}
+                disabled={!editIntentType.length}
+                variant='primary'
+              >
+                保存
+              </Button>
+            </div>
+          </Modal>
+        )
+      }
+    </div>
+  )
+}
+
+export default TypeModal

+ 9 - 1
web/app/components/header/account-setting/index.tsx

@@ -5,7 +5,7 @@ import {
   RiBook3Fill,
   RiBook3Line,
   RiBrain2Fill,
-  RiBrain2Line,
+  RiBrain2Line, RiBuildingFill, RiBuildingLine,
   RiCloseLine,
   RiColorFilterFill,
   RiColorFilterLine,
@@ -37,6 +37,7 @@ import { useProviderContext } from '@/context/provider-context'
 import { useAppContext } from '@/context/app-context'
 import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
 import Input from '@/app/components/base/input'
+import DeptsPage from './dept-page'
 
 const iconClassName = `
   w-5 h-5 mr-2
@@ -118,6 +119,12 @@ export default function AccountSetting({
         icon: <RiBook3Line className={iconClassName} />,
         activeIcon: <RiBook3Fill className={iconClassName} />,
       },
+      {
+        key: 'dept',
+        name: '部门',
+        icon: <RiBuildingLine className={iconClassName} />,
+        activeIcon: <RiBuildingFill className={iconClassName} />,
+      },
     ].filter(item => !!item.key) as GroupItem[]
   })()
 
@@ -240,6 +247,7 @@ export default function AccountSetting({
               {activeMenu === 'language' && <LanguagePage />}
               {activeMenu === 'type' && <TypesPage />}
               {activeMenu === 'knowledge' && <KnowledgesPage />}
+              {activeMenu === 'dept' && <DeptsPage />}
             </div>
           </div>
         </div>

+ 1 - 1
web/app/components/header/account-setting/knowledges-page/index.tsx

@@ -43,7 +43,7 @@ const KnowledgesPage = () => {
       url: '/external_applications',
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
       },
     },
     fetchKnowledges,

+ 1 - 1
web/app/components/header/account-setting/types-page/index.tsx

@@ -32,7 +32,7 @@ const TypesPage = () => {
       url: '/tags/page',
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
         tag_type: 'knowledge_category',
       },
     },

+ 19 - 8
web/app/components/header/index.tsx

@@ -21,6 +21,7 @@ import { useModalContext } from '@/context/modal-context'
 import PlanBadge from './plan-badge'
 import LicenseNav from './license-env'
 import { Plan } from '../billing/type'
+import SkillNav from './skill-nav'
 
 const navClassName = `
   flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@@ -85,11 +86,16 @@ const Header = () => {
       {
         !isMobile && (
           <div className='flex items-center'>
-            {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
-            {!isCurrentWorkspaceDatasetOperator && <AppNav />}
-            {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
-            {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
+            {/* {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} */}
+            {/* {!isCurrentWorkspaceDatasetOperator && <AppNav />} */}
+            {/* {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} */}
+            {/* {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} */}
             {/* {!isCurrentWorkspaceDatasetOperator && <SkillNav className={navClassName} />} */}
+            <ExploreNav className={navClassName} />
+            <AppNav />
+            <DatasetNav />
+            <ToolsNav className={navClassName} />
+            <SkillNav className={navClassName} />
           </div>
         )
       }
@@ -103,11 +109,16 @@ const Header = () => {
       {
         (isMobile && isShowNavMenu) && (
           <div className='flex w-full flex-col gap-y-1 p-2'>
-            {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
-            {!isCurrentWorkspaceDatasetOperator && <AppNav />}
-            {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
-            {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
+            {/* {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} */}
+            {/* {!isCurrentWorkspaceDatasetOperator && <AppNav />} */}
+            {/* {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} */}
+            {/* {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} */}
             {/* {!isCurrentWorkspaceDatasetOperator && <SkillNav className={navClassName} />} */}
+            <ExploreNav className={navClassName} />
+            <AppNav />
+            <DatasetNav />
+            <ToolsNav className={navClassName} />
+            <SkillNav className={navClassName} />
           </div>
         )
       }

+ 249 - 195
web/app/components/skill/corpus/detail-modal.tsx

@@ -1,169 +1,191 @@
 '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, fetchIntentName, fetchIntentType } from '@/service/common'
+import {
+  addCorpus, addCorpusQuestion, delBatchCorpusQuestion, editCorpus as editCorpusFunc, editCorpusQuestion, fetchCorpusQuestion,
+  fetchIntent,
+  getCorpus,
+} 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'
 import { ToastContext } from '@/app/components/base/toast'
 import Confirm from '@/app/components/base/confirm'
-import Textarea from '@/app/components/base/textarea'
+import { Cascader as AntdCascader } from 'antd'
+import { Textarea } from '@/app/components/base/textarea'
 
 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 { data: dataOptionsIntentType }: any = useSWR(
-    {
-      url: '/xxx',
+  const [intentCascader, setIntentCascader] = useState<any>([])
+  useEffect(() => {
+    fetchIntent({
+      url: '/intentions',
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
       },
-    },
-    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',
+    }).then((res: any) => {
+      const map = new Map()
+      res.data.forEach((v: any) => {
+        if (map.has(v.type_id)) {
+          const parent = map.get(v.type_id)
+          parent.children.push(v)
+          map.set(v.type_id, parent)
+        }
+        else {
+          map.set(v.type_id, {
+            id: v.type_id,
+            name: v.type_name,
+            children: [v],
+          })
+        }
+      })
+      setIntentCascader(Array.from(map.values()))
+    })
+  }, [])
+  const [question, setQuestion] = useState<string>('')
+  const [intentValue, setIntentValue] = useState<any>([])
+  const [questionCorpus, setQuestionCorpus] = useState<any>('')
+  const [similarityList, setSimilarityList] = useState<any>([])
+  const handleSave = async () => {
+    try {
+      let res
+      if (transfer.mode === 'add') {
+        res = await addCorpus({
+          url: '/intentions/corpus',
+          body: { question, intention_id: intentValue[intentValue.length - 1] },
+        })
+      }
+      else {
+        res = await editCorpusFunc({
+          url: `/intentions/corpus/${transfer.row.id}`,
+          body: { question, intention_id: intentValue[intentValue.length - 1], question_config: questionCorpus },
+        })
+      }
+      const { id }: any = res
+      if (id) {
+        if (transfer.mode === 'add')
+          onRefresh(id)
+        else
+          onSend()
+      }
+    }
+    catch (e) { }
+  }
+  useEffect(() => {
+    if (transfer.row?.id) {
+      getCorpus({ url: `/intentions/corpus/${transfer.row.id}` }).then((res: any) => {
+        setQuestion(res.question)
+        setIntentValue([res.intention.type_id, res.intention.id])
+        setQuestionCorpus(res.question_config || '')
+        setSimilarityList(res.similarity_questions)
+      })
+    }
+  }, [])
+  const [similarityQuestion, setSimilarityQuestion] = useState<string>('')
+  const [similarityFilter, setSimilarityFilter] = useState<string>('')
+  const refreshSimilarity = async () => {
+    const res = await fetchCorpusQuestion({
+      url: `/intentions/corpus/${transfer.row.id}/similarity_questions`,
       params: {
         page: 1,
-        limit: 1000,
-        intentType,
+        limit: 99999,
       },
-    },
-    fetchIntentName,
-  )
-  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)
+    })
+    setSimilarityList(res)
+  }
+  const handleAddSimilarity = async () => {
+    if (!similarityQuestion)
       return
-
-    if (questionList.some((v: any) => v.name === questionRelation)) {
+    if (similarityList.some((v: any) => v.question === similarityQuestion)) {
       notify({ type: 'warning', message: '请勿新增重复数据!' })
       return
     }
-    setQuestionList([...questionList, { id: uuid4(), name: questionRelation }])
-    setQuestionRelation('')
+    const { id }: any = await addCorpusQuestion({
+      url: `/intentions/corpus/${transfer.row.id}/similarity_questions`,
+      body: {
+        question: similarityQuestion,
+      },
+    })
+    if (id) {
+      await refreshSimilarity()
+      setSimilarityQuestion('')
+    }
   }
-  const [questionSelectMap, setQuestionSelectMap] = useState<any>(new Map())
-  const addQuestionSelectMap = (key: any, value: any) => {
-    setQuestionSelectMap((prevMap: any) => {
+  const [similaritySelectMap, setSimilaritySelectMap] = useState<any>(new Map())
+  const addSimilaritySelectMap = (key: any, value: any) => {
+    setSimilaritySelectMap((prevMap: any) => {
       const newMap = new Map(prevMap)
       newMap.set(key, value)
       return newMap
     })
   }
-  const delQuestionSelectMap = (key: any) => {
-    setQuestionSelectMap((prevMap: any) => {
+  const delSimilaritySelectMap = (key: any) => {
+    setSimilaritySelectMap((prevMap: any) => {
       const newMap = new Map(prevMap)
       newMap.delete(key)
       return newMap
     })
   }
+  const [similarityRow, setSimilarityRow] = useState<any>({})
+  const [showSimilarityEdit, setShowSimilarityEdit] = useState(false)
+  const [editSimilarity, setEditSimilarity] = useState<string>('')
+  const [showSimilarityCorpus, setShowSimilarityCorpus] = useState(false)
+  const [editSimilarityCorpus, setEditSimilarityCorpus] = useState('')
+  const handleSaveSimilarity = async () => {
+    if (similarityList.some((v: any) => v.name === editSimilarity)) {
+      notify({ type: 'warning', message: '请勿新增重复数据!' })
+      return
+    }
+    const params: any = {
+      corpus_id: similarityRow.corpus_id,
+    }
+    if (showSimilarityEdit) {
+      params.question = editSimilarity
+    }
+    else if (showSimilarityCorpus) {
+      params.question = similarityRow.question
+      params.question_config = editSimilarityCorpus
+    }
+
+    const { id }: any = await editCorpusQuestion({
+      url: `/intentions/similarity_questions/${similarityRow.id}`,
+      body: params,
+    })
+    if (id) {
+      await refreshSimilarity()
+      setShowSimilarityEdit(false)
+      setShowSimilarityCorpus(false)
+    }
+  }
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
-  const [row, setRow] = useState<any>({})
-  const handleDelQuestion = async () => {
+  const [delBatch, setDelBatch] = useState(false)
+  const handleDelSimilarity = async () => {
     try {
-      await delCorpusQuestion({
-        url: `/xxx/${row.id}`,
-        body: {},
+      await delBatchCorpusQuestion({
+        url: '/intentions/similarity_questions/batch',
+        body: {
+          method: 'delete',
+          data: delBatch ? Array.from(similaritySelectMap.keys()) : [similarityRow.id],
+        },
       })
       setShowConfirmDelete(false)
-      // mutate()
+      setSimilaritySelectMap(new Map())
+      refreshSimilarity()
     }
     catch (e) { }
   }
-  const [showQuestionEdit, setShowQuestionEdit] = useState(false)
-  const [editQuestion, setEditQuestion] = useState<string>('')
   const [showCorpus, setShowCorpus] = useState(false)
-  const [corpusRow, setCorpusRow] = useState<any>({})
-  const [corpusInput, setCorpusInput] = 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])
-  const handleSaveCorpus = 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 [editCorpus, setEditCorpus] = useState('')
   return (
     <div>
       <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
@@ -186,11 +208,7 @@ const DetailModal = ({
               {
                 transfer.mode === 'edit' && (
                   <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
-                    setRow({
-                      type: 'main',
-                      name: question,
-                    })
-                    setCorpusInput(question)
+                    setEditCorpus(questionCorpus)
                     setShowCorpus(true)
                   }}>
                     语料配置
@@ -201,27 +219,16 @@ const DetailModal = ({
             <div className="flex w-full items-center">
               <div className="w-[80px]">意图名称</div>
               <div className="flex flex-1">
-                <SimpleSelect
-                  className="h-[32px] w-[200px]"
-                  defaultValue={intentType}
-                  onSelect={(i: any) => {
-                    setIntentType(i.value)
-                    setIntentName('')
-                  }}
-                  items={optionsIntentType}
-                  allowSearch={false}
-                  placeholder="请选择意图类型"
-                />
-                <SimpleSelect
-                  className="h-[32px] w-[200px]"
-                  defaultValue={intentName}
-                  onSelect={(i: any) => {
-                    setIntentName(i.value)
+                <AntdCascader
+                  value={intentValue}
+                  className="h-[32px] w-full"
+                  options={intentCascader}
+                  onChange={(val: any) => {
+                    setIntentValue(val)
                   }}
-                  items={optionsIntentName}
-                  allowSearch={false}
-                  placeholder="请选择意图名称"
-                  disabled={!intentType}
+                  placeholder="请选择"
+                  fieldNames={{ label: 'name', value: 'id' }}
+                  showSearch={true}
                 />
               </div>
             </div>
@@ -232,11 +239,11 @@ const DetailModal = ({
                   <div className="flex-1">
                     <Input
                       showClearIcon
-                      value={questionRelation}
-                      onChange={e => setQuestionRelation(e.target.value)}
-                      onClear={() => setQuestionRelation('')}
+                      value={similarityQuestion}
+                      onChange={e => setSimilarityQuestion(e.target.value)}
+                      onClear={() => setSimilarityQuestion('')}
                       placeholder='输入后Enter以添加'
-                      onEnter={handleAddQuestion}
+                      onEnter={handleAddSimilarity}
                     />
                   </div>
                 </div>
@@ -250,64 +257,67 @@ 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={similarityList.every((v: any) => similaritySelectMap.has(v.id))}
                       onCheck={() => {
-                        questionList.every((v: any) => questionSelectMap.has(v.id))
-                          ? setQuestionSelectMap(new Map())
-                          : questionList.forEach((v: any) => addQuestionSelectMap(v.id, v))
+                        similarityList.every((v: any) => similaritySelectMap.has(v.id))
+                          ? setSimilaritySelectMap(new Map())
+                          : similarityList.forEach((v: any) => addSimilaritySelectMap(v.id, v))
                       }}
-                      disabled={questionList.length === 0}
+                      disabled={similarityList.length === 0}
                     />
                     全选
                   </div>
                   <div className="ml-auto w-[200px]">
                     <Input
                       showClearIcon
-                      value={questionFilter}
-                      onChange={e => setQuestionFilter(e.target.value)}
-                      onClear={() => setQuestionFilter('')}
+                      value={similarityFilter}
+                      onChange={e => setSimilarityFilter(e.target.value)}
+                      onClear={() => setSimilarityFilter('')}
                       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-[300px] 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) => (
+                    similarityList.filter((v: any) => !similarityFilter || v.question.includes(similarityFilter)).map((item: any) => (
                       <div key={item.id} className="flex items-center">
                         <Checkbox
                           className='mr-2 shrink-0'
-                          checked={questionSelectMap.has(item.id)}
+                          checked={similaritySelectMap.has(item.id)}
                           onCheck={() => {
-                            questionSelectMap.has(item.id)
-                              ? delQuestionSelectMap(item.id)
-                              : addQuestionSelectMap(item.id, item)
+                            similaritySelectMap.has(item.id)
+                              ? delSimilaritySelectMap(item.id)
+                              : addSimilaritySelectMap(item.id, item)
                           }}
-                          disabled={questionList.length === 0}
+                          disabled={similarityList.length === 0}
                         />
                         <div className="flex-1">
-                          {item.name}
+                          {item.question}
                         </div>
                         <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
-                          setRow(item)
-                          setCorpusInput(item.name)
-                          setShowCorpus(true)
+                          setSimilarityRow(item)
+                          setEditSimilarityCorpus(item.question_config || '')
+                          setShowSimilarityCorpus(true)
                         }}>
                           语料配置
                         </Button>
                         <Button variant='ghost-accent' size='small' className={cn('shrink-0')}
                           onClick={() => {
-                            setRow(item)
-                            setEditQuestion(item.name)
-                            setShowQuestionEdit(true)
+                            setSimilarityRow(item)
+                            setEditSimilarity(item.question)
+                            setShowSimilarityEdit(true)
                           }}>
                           编辑
                         </Button>
                         <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')}
                           onClick={() => {
-                            setRow(item)
+                            setSimilarityRow(item)
                             setShowConfirmDelete(true)
                           }}>
                           刪除
@@ -317,8 +327,8 @@ 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>共{similarityList.length}条</div>
+                  <div className="ml-4">已选择{similaritySelectMap.size}条</div>
                 </div>
               </div>
             )
@@ -327,7 +337,7 @@ const DetailModal = ({
             tabIndex={0}
             className='mt-4 w-full'
             onClick={handleSave}
-            disabled={!question.length || !intentType.length || !intentName.length}
+            disabled={!question.length || !intentValue.length}
             variant='primary'
           >
             保存
@@ -335,11 +345,11 @@ const DetailModal = ({
         </div>
       </Modal>
       {
-        showQuestionEdit && (
+        showSimilarityEdit && (
           <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={() => setShowSimilarityEdit(false)} />
             </div>
             <div>
               <div className={cn('flex flex-wrap items-center justify-between py-4')}>
@@ -347,8 +357,8 @@ const DetailModal = ({
                   相似问题
                 </div>
                 <Input
-                  value={editQuestion}
-                  onChange={e => setEditQuestion(e.target.value)}
+                  value={editSimilarity}
+                  onChange={e => setEditSimilarity(e.target.value)}
                   className='h-9'
                   placeholder='请输入相似问题'
                 />
@@ -356,8 +366,8 @@ const DetailModal = ({
               <Button
                 tabIndex={0}
                 className='w-full'
-                onClick={handleSaveQuestion}
-                disabled={!editQuestion.length}
+                onClick={handleSaveSimilarity}
+                disabled={!editSimilarity.length}
                 variant='primary'
               >
                 保存
@@ -366,6 +376,15 @@ const DetailModal = ({
           </Modal>
         )
       }
+      {showConfirmDelete && (
+        <Confirm
+          title="删除确认"
+          content={`请确认是否删除${delBatch ? `${similaritySelectMap.size}条相似问题` : similarityRow.question}?`}
+          isShow={showConfirmDelete}
+          onConfirm={handleDelSimilarity}
+          onCancel={() => setShowConfirmDelete(false)}
+        />
+      )}
       {
         showCorpus && (
           <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px]  max-w-[800px]">
@@ -376,11 +395,55 @@ const DetailModal = ({
             <div>
               <div className={cn('flex flex-wrap items-center justify-between py-4')}>
                 <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
-                  当前标注问题:{row.name}
+                  当前标注问题:{question}
+                </div>
+                <Textarea
+                  value={editCorpus}
+                  onChange={e => setEditCorpus(e.target.value)}
+                  className='resize-none'
+                  placeholder='请输入语料配置'
+                  rows={30}
+                />
+              </div>
+              <div className="flex gap-2">
+                <Button
+                  className='w-full'
+                  onClick={() => setEditCorpus(questionCorpus)}
+                  variant='warning'
+                >
+                  重置
+                </Button>
+                <Button
+                  className='w-full'
+                  onClick={() => {
+                    setQuestionCorpus(editCorpus)
+                    setShowCorpus(false)
+                  }}
+                  disabled={!editCorpus.length}
+                  variant='primary'
+                >
+                  保存
+                </Button>
+              </div>
+            </div>
+          </Modal>
+        )
+      }
+      {
+        showSimilarityCorpus && (
+          <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px]  max-w-[800px]">
+            <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={() => setShowSimilarityCorpus(false)} />
+            </div>
+            <div>
+              <div className={cn('flex flex-wrap items-center justify-between py-4')}>
+                <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+                  当前标注问题:{similarityRow.question}
                 </div>
                 <Textarea
-                  value={corpusInput}
-                  onChange={e => setCorpusInput(e.target.value)}
+                  value={editSimilarityCorpus}
+                  onChange={e => setEditSimilarityCorpus(e.target.value)}
                   className='resize-none'
                   placeholder='请输入语料配置'
                   rows={30}
@@ -389,15 +452,15 @@ const DetailModal = ({
               <div className="flex gap-2">
                 <Button
                   className='w-full'
-                  onClick={() => setCorpusInput(row.name)}
+                  onClick={() => setEditSimilarityCorpus(similarityRow.question_config || '')}
                   variant='warning'
                 >
                   重置
                 </Button>
                 <Button
                   className='w-full'
-                  onClick={handleSaveCorpus}
-                  disabled={!corpusInput.length}
+                  onClick={handleSaveSimilarity}
+                  disabled={!editSimilarityCorpus.length}
                   variant='primary'
                 >
                   保存
@@ -407,15 +470,6 @@ const DetailModal = ({
           </Modal>
         )
       }
-      {showConfirmDelete && (
-        <Confirm
-          title="删除确认"
-          content={`请确认是否删除${row.name}?`}
-          isShow={showConfirmDelete}
-          onConfirm={handleDelQuestion}
-          onCancel={() => setShowConfirmDelete(false)}
-        />
-      )}
     </div>
   )
 }

+ 88 - 77
web/app/components/skill/corpus/index.tsx

@@ -1,77 +1,89 @@
 'use client'
 
 import List from './list'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
 import Input from '@/app/components/base/input'
 import cn from '@/utils/classnames'
 import { RiAddLine, RiRefreshLine, RiSearchLine } from '@remixicon/react'
 import Button from '@/app/components/base/button'
-import useSWR from 'swr'
-import { fetchCorpus, fetchIntentName, fetchIntentType } from '@/service/common'
-import { SimpleSelect } from '@/app/components/base/select'
+import { fetchCorpus, fetchIntent } from '@/service/common'
 import DetailModal from './detail-modal'
+import { Cascader as AntdCascader } from 'antd'
 
 const CorpusIndex = () => {
-  const [page, setPage] = React.useState<number>(0)
-  const [limit, setLimit] = useState<number>(10)
-  const [question, setQuestion] = useState<string>('') // the input value
-  const [intentType, setIntentType] = useState<string>('') // the input value
-  const { data: dataOptionsIntentType }: any = useSWR(
-    {
-      url: '/xxx',
-      params: {
-        page: 1,
-        limit: 1000,
-      },
-    },
-    fetchIntentType,
-  )
-  const optionsIntentType: any = dataOptionsIntentType?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
-  const [intentName, setIntentName] = useState<string>('') // the input value
-  const { data: dataOptionsIntentName }: any = useSWR(
-    {
-      url: '/xxx',
+  const [intentCascader, setIntentCascader] = useState<any>([])
+  useEffect(() => {
+    fetchIntent({
+      url: '/intentions',
       params: {
         page: 1,
-        limit: 1000,
-        intentType,
+        limit: 99999,
       },
-    },
-    fetchIntentName,
-  )
-  const optionsIntentName: any = dataOptionsIntentName?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
-  const [query, setQuery] = useState<any>({})
-  const { data, mutate }: any = useSWR(
-    {
-      url: '/xxx',
-      params: {
-        page: page + 1,
-        limit,
-        ...query,
-      },
-    },
-    fetchCorpus,
-  )
-  const list: any = data?.data || []
-  const total = data?.total || 0
+    }).then((res: any) => {
+      const map = new Map()
+      res.data.forEach((v: any) => {
+        if (map.has(v.type_id)) {
+          const parent = map.get(v.type_id)
+          parent.children.push(v)
+          map.set(v.type_id, parent)
+        }
+        else {
+          map.set(v.type_id, {
+            id: v.type_id,
+            name: v.type_name,
+            children: [v],
+          })
+        }
+      })
+      setIntentCascader(Array.from(map.values()))
+    })
+  }, [])
+  const [page, setPage] = useState<number>(0)
+  const [limit, setLimit] = useState<number>(10)
+  const [question, setQuestion] = useState<string>('')
+  const [intentName, setIntentName] = useState<string>('')
+  const [intentValue, setIntentValue] = useState<any>([])
+  const query = useRef<any>({})
+  const [list, setList] = useState<any>([])
+  const [total, setTotal] = useState(0)
+  const handlePage = () => {
+    const params: any = {
+      page: page + 1,
+      limit,
+    }
+    if (query.current.question)
+      params.question_search = query.current.question
+    if (query.current.intentName)
+      params.intention_id = query.current.intentName
+    fetchCorpus({
+      url: '/intentions/corpus',
+      params,
+    }).then((res: any) => {
+      setList(res.data)
+      setTotal(res.total)
+    })
+  }
+  const [refresh, setRefresh] = useState<boolean>(false)
   const handleSearch = (reset = false) => {
+    setRefresh(true)
+    setPage(0)
     if (reset) {
       setQuestion('')
       setIntentName('')
-      setIntentType('')
+      setIntentValue([])
+      query.current = {}
     }
-    const params: any = {}
-    if (question)
-      params.question = question
-    if (intentType)
-      params.intentType = intentType
-    if (intentName)
-      params.intentName = intentName
-    setQuery(params)
-    setPage(0)
+    else {
+      query.current = {
+        question, intentName,
+      }
+    }
+    handlePage()
+    setRefresh(false)
   }
   useEffect(() => {
-    mutate()
+    if (!refresh)
+      handlePage()
   }, [page, limit])
 
   const [detailModalVisible, setDetailModalVisible] = useState(false)
@@ -94,30 +106,20 @@ const CorpusIndex = () => {
               onClear={() => setQuestion('')}
             />
           </div>
-          <div className="flex shrink-0 items-center text-gray-500">
+          <div className="ml-2 flex shrink-0 items-center text-gray-500">
             意图名称
             <div className="ml-2 flex h-[32px]">
-              <SimpleSelect
+              <AntdCascader
+                value={intentValue}
                 className="h-[32px] w-[200px]"
-                defaultValue={intentType}
-                onSelect={(i: any) => {
-                  setIntentType(i.value)
-                  setIntentName('')
+                options={intentCascader}
+                onChange={(val: any) => {
+                  setIntentValue(val)
+                  setIntentName(val?.[val.length - 1] || '')
                 }}
-                items={optionsIntentType}
-                allowSearch={false}
-                placeholder="请选择意图类型"
-              />
-              <SimpleSelect
-                className="h-[32px] w-[200px]"
-                defaultValue={intentName}
-                onSelect={(i: any) => {
-                  setIntentName(i.value)
-                }}
-                items={optionsIntentName}
-                allowSearch={false}
-                placeholder="请选择意图名称"
-                disabled={!intentType}
+                placeholder="请选择"
+                fieldNames={{ label: 'name', value: 'id' }}
+                showSearch={true}
               />
             </div>
           </div>
@@ -147,7 +149,7 @@ const CorpusIndex = () => {
         <div className="flex-1">
           <List
             list={list || []}
-            onUpdate={mutate}
+            onUpdate={() => handleSearch(false)}
             pagination={{
               total,
               limit,
@@ -162,10 +164,19 @@ const CorpusIndex = () => {
         detailModalVisible && (
           <DetailModal
             transfer={transfer}
-            onCancel={() => setDetailModalVisible(false)}
+            onCancel={() => {
+              setDetailModalVisible(false)
+              handleSearch()
+            }}
             onSend={() => {
               setDetailModalVisible(false)
-              mutate()
+              handleSearch()
+            }}
+            onRefresh={(id: string) => {
+              setTransfer({
+                mode: 'edit',
+                row: { id },
+              })
             }}
           />
         )

+ 15 - 8
web/app/components/skill/corpus/list.tsx

@@ -7,6 +7,8 @@ import cn from '@/utils/classnames'
 import { delCorpus } from '@/service/common'
 import Confirm from '@/app/components/base/confirm'
 import DetailModal from './detail-modal'
+import { useTranslation } from 'react-i18next'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type PageListProps = {
   list: []
@@ -19,12 +21,14 @@ const CorpusPageList: FC<PageListProps> = ({
   pagination,
   onUpdate,
 }) => {
+  const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
   const [row, setRow] = useState<any>({})
   const handleDel = async () => {
     try {
       await delCorpus({
-        url: `/tags/${row.id}`,
+        url: `/intentions/corpus/${row.id}`,
         body: {},
       })
       setShowConfirmDelete(false)
@@ -59,11 +63,11 @@ const CorpusPageList: FC<PageListProps> = ({
                   key={item.id}
                   className={'h-8 border-b border-divider-subtle hover:bg-background-default-hover'}
                 >
-                  <td>1</td>
-                  <td>2</td>
-                  <td>3</td>
-                  <td>4</td>
-                  <td>2025-02-02 22:44:33</td>
+                  <td>{item.question}</td>
+                  <td>{item.similarity_questions.length}</td>
+                  <td>{item.intention.type_name}</td>
+                  <td>{item.intention.name}</td>
+                  <td>{formatTime(item.created_at, t('datasetHitTesting.dateTimeFormat') as string)}</td>
                   <td className="flex justify-center gap-2">
                     <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
                       setTransfer({
@@ -97,7 +101,7 @@ const CorpusPageList: FC<PageListProps> = ({
       {showConfirmDelete && (
         <Confirm
           title="删除确认"
-          content={`请确认是否删除${row.name}?`}
+          content={`请确认是否删除${row.question}?`}
           isShow={showConfirmDelete}
           onConfirm={handleDel}
           onCancel={() => setShowConfirmDelete(false)}
@@ -107,7 +111,10 @@ const CorpusPageList: FC<PageListProps> = ({
         detailModalVisible && (
           <DetailModal
             transfer={transfer}
-            onCancel={() => setDetailModalVisible(false)}
+            onCancel={() => {
+              setDetailModalVisible(false)
+              onUpdate()
+            }}
             onSend={() => {
               setDetailModalVisible(false)
               onUpdate()

+ 211 - 127
web/app/components/skill/intent/detail-modal.tsx

@@ -1,144 +1,165 @@
 '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, fetchIntentName, fetchIntentType } from '@/service/common'
+import {
+  addIntent, addIntentKeyword, delBatchIntentKeyword, editIntent, editIntentKeyword, 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'
 import { ToastContext } from '@/app/components/base/toast'
 import Confirm from '@/app/components/base/confirm'
+import { Textarea } from '@/app/components/base/textarea'
 
 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,
+        limit: 99999,
       },
     },
     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,
+        limit: 99999,
       },
-    },
-    fetchIntentName,
-  )
-  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 (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 editIntent({
+          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])
+  const [similarityRow, setSimilarityRow] = useState<any>({})
+  const [showSimilarityCorpus, setShowSimilarityCorpus] = useState(false)
   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,55 @@ 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.question.includes(corpusFilter)).map((item: any) => (
+                      <div key={item.id} className="flex items-center">
+                        <div className="flex-1">
+                          {item.question}
+                        </div>
+                        <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
+                          setSimilarityRow(item)
+                          setShowSimilarityCorpus(true)
+                        }}>
+                          语料配置
+                        </Button>
+                      </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 +339,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 +351,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 +360,8 @@ const DetailModal = ({
               <Button
                 tabIndex={0}
                 className='w-full'
-                onClick={handleSaveQuestion}
-                disabled={!editQuestion.length}
+                onClick={handleSaveKeyword}
+                disabled={!editKeyword.length}
                 variant='primary'
               >
                 保存
@@ -312,12 +373,35 @@ const DetailModal = ({
       {showConfirmDelete && (
         <Confirm
           title="删除确认"
-          content={`请确认是否删除${row.name}?`}
+          content={`请确认是否删除${delBatch ? `${keywordSelectMap.size}条关键词` : keywordRow.name}?`}
           isShow={showConfirmDelete}
-          onConfirm={handleDelQuestion}
+          onConfirm={handleDelKeyword}
           onCancel={() => setShowConfirmDelete(false)}
         />
       )}
+      {
+        showSimilarityCorpus && (
+          <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px]  max-w-[800px]">
+            <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={() => setShowSimilarityCorpus(false)} />
+            </div>
+            <div>
+              <div className={cn('flex flex-wrap items-center justify-between py-4')}>
+                <div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-text-primary'>
+                  当前标注问题:{similarityRow.question}
+                </div>
+                <Textarea
+                  value={similarityRow.question_config || ''}
+                  className='resize-none'
+                  rows={30}
+                  disabled={true}
+                />
+              </div>
+            </div>
+          </Modal>
+        )
+      }
     </div>
   )
 }

+ 60 - 59
web/app/components/skill/intent/index.tsx

@@ -1,86 +1,80 @@
 'use client'
 
 import List from './list'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
 import Input from '@/app/components/base/input'
 import cn from '@/utils/classnames'
 import { RiAddLine, RiPriceTagLine, RiRefreshLine, RiSearchLine } from '@remixicon/react'
 import Button from '@/app/components/base/button'
 import useSWR from 'swr'
-import { fetchCorpus, fetchIntentName, fetchIntentType } from '@/service/common'
+import { fetchIntent, fetchIntentType } from '@/service/common'
 import { SimpleSelect } from '@/app/components/base/select'
 import DetailModal from './detail-modal'
 import TypeModal from './type-modal'
 
 const CorpusIndex = () => {
-  const [page, setPage] = React.useState<number>(0)
+  const [page, setPage] = useState<number>(0)
   const [limit, setLimit] = useState<number>(10)
-  const [question, setQuestion] = useState<string>('') // the input value
+  const [intentName, setIntentName] = useState<string>('') // the input value
   const [intentType, setIntentType] = useState<string>('') // the input value
-  const { data: dataOptionsIntentType }: any = useSWR(
+  const { data: dataOptionsIntentType, mutate: mutateOptionsIntentType }: any = useSWR(
     {
-      url: '/xxx',
+      url: '/intentions/types',
       params: {
         page: 1,
-        limit: 1000,
+        limit: 99999,
       },
     },
     fetchIntentType,
   )
   const optionsIntentType: any = dataOptionsIntentType?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
-  const [intentName, setIntentName] = useState<string>('') // the input value
-  const { data: dataOptionsIntentName }: any = useSWR(
-    {
-      url: '/xxx',
-      params: {
-        page: 1,
-        limit: 1000,
-        intentType,
-      },
-    },
-    fetchIntentName,
-  )
-  const optionsIntentName: any = dataOptionsIntentName?.data.map((v: any) => ({ name: v.name, value: v.id })) || []
-  const [query, setQuery] = useState<any>({})
-  const { data, mutate }: any = useSWR(
-    {
-      url: '/xxx',
-      params: {
-        page: page + 1,
-        limit,
-        ...query,
-      },
-    },
-    fetchCorpus,
-  )
-  const list: any = data?.data || []
-  const total = data?.total || 0
+  const query = useRef<any>({})
+  const [list, setList] = useState<any>([])
+  const [total, setTotal] = useState(0)
+  const handlePage = () => {
+    const params: any = {
+      page: page + 1,
+      limit,
+    }
+    if (query.current.intentType)
+      params.type_id = query.current.intentType
+    if (query.current.intentName)
+      params.name_search = query.current.intentName
+    fetchIntent({
+      url: '/intentions',
+      params,
+    }).then((res: any) => {
+      setList(res.data)
+      setTotal(res.total)
+    })
+  }
+  const [refresh, setRefresh] = useState<boolean>(false)
   const handleSearch = (reset = false) => {
+    setRefresh(true)
+    setPage(0)
     if (reset) {
-      setQuestion('')
       setIntentName('')
       setIntentType('')
+      query.current = {}
     }
-    const params: any = {}
-    if (question)
-      params.question = question
-    if (intentType)
-      params.intentType = intentType
-    if (intentName)
-      params.intentName = intentName
-    setQuery(params)
-    setPage(0)
+    else {
+      query.current = {
+        intentType, intentName,
+      }
+    }
+    handlePage()
+    setRefresh(false)
   }
   useEffect(() => {
-    mutate()
+    if (!refresh)
+      handlePage()
   }, [page, limit])
-
   const [detailModalVisible, setDetailModalVisible] = useState(false)
   const [transfer, setTransfer] = useState<any>({
     mode: 'add',
     row: {},
   })
-  const [showIntentTypeModalVisible, setShowIntentTypeModalVisible] = useState(true)
+  const [showIntentTypeModalVisible, setShowIntentTypeModalVisible] = useState(false)
   return (
     <>
       <div className='flex h-full w-full flex-col bg-background-default-subtle p-6'>
@@ -106,9 +100,9 @@ const CorpusIndex = () => {
               className="ml-2"
               showClearIcon
               wrapperClassName='!w-[200px]'
-              value={question}
-              onChange={e => setQuestion(e.target.value)}
-              onClear={() => setQuestion('')}
+              value={intentName}
+              onChange={e => setIntentName(e.target.value)}
+              onClear={() => setIntentName('')}
             />
           </div>
           <Button variant='primary' className={cn('ml-auto shrink-0')} onClick={() => {
@@ -135,8 +129,7 @@ const CorpusIndex = () => {
           </Button>
           <Button variant='primary' className={cn('ml-2 shrink-0')}
             onClick={() => {
-              setTransfer({ mode: 'add', row: null })
-              setDetailModalVisible(true)
+              setShowIntentTypeModalVisible(true)
             }}>
             <RiPriceTagLine className='mr-1 h-4 w-4' />
             意图类型管理
@@ -144,8 +137,8 @@ const CorpusIndex = () => {
         </div>
         <div className="flex-1">
           <List
-            list={list || []}
-            onUpdate={mutate}
+            list={list}
+            onUpdate={() => handleSearch(false)}
             pagination={{
               total,
               limit,
@@ -160,10 +153,19 @@ const CorpusIndex = () => {
         detailModalVisible && (
           <DetailModal
             transfer={transfer}
-            onCancel={() => setDetailModalVisible(false)}
+            onCancel={() => {
+              setDetailModalVisible(false)
+              handleSearch()
+            }}
             onSend={() => {
               setDetailModalVisible(false)
-              mutate()
+              handleSearch()
+            }}
+            onRefresh={(id: string) => {
+              setTransfer({
+                mode: 'edit',
+                row: { id },
+              })
             }}
           />
         )
@@ -171,10 +173,9 @@ const CorpusIndex = () => {
       {
         showIntentTypeModalVisible && (
           <TypeModal
-            onCancel={() => setShowIntentTypeModalVisible(false)}
-            onSend={() => {
+            onCancel={() => {
+              mutateOptionsIntentType()
               setShowIntentTypeModalVisible(false)
-              // mutate()
             }}
           />
         )

+ 16 - 9
web/app/components/skill/intent/list.tsx

@@ -4,9 +4,11 @@ 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'
+import { useTranslation } from 'react-i18next'
 
 type PageListProps = {
   list: []
@@ -19,12 +21,14 @@ const IntentPageList: FC<PageListProps> = ({
   pagination,
   onUpdate,
 }) => {
+  const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
   const [row, setRow] = useState<any>({})
   const handleDel = async () => {
     try {
-      await delCorpus({
-        url: `/tags/${row.id}`,
+      await delIntent({
+        url: `/intentions/${row.id}`,
         body: {},
       })
       setShowConfirmDelete(false)
@@ -59,11 +63,11 @@ const IntentPageList: FC<PageListProps> = ({
                   key={item.id}
                   className={'h-8 border-b border-divider-subtle hover:bg-background-default-hover'}
                 >
-                  <td>1</td>
-                  <td>2</td>
-                  <td>3</td>
-                  <td>4</td>
-                  <td>2025-02-02 22:44:33</td>
+                  <td>{item.type_name}</td>
+                  <td>{item.name}</td>
+                  <td>{item.corpus_count}</td>
+                  <td>{item.keywords_count}</td>
+                  <td>{formatTime(item.created_at, t('datasetHitTesting.dateTimeFormat') as string)}</td>
                   <td className="flex justify-center gap-2">
                     <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
                       setTransfer({
@@ -107,7 +111,10 @@ const IntentPageList: FC<PageListProps> = ({
         detailModalVisible && (
           <DetailModal
             transfer={transfer}
-            onCancel={() => setDetailModalVisible(false)}
+            onCancel={() => {
+              setDetailModalVisible(false)
+              onUpdate()
+            }}
             onSend={() => {
               setDetailModalVisible(false)
               onUpdate()

+ 52 - 44
web/app/components/skill/intent/type-modal.tsx

@@ -1,9 +1,9 @@
 'use client'
-import React, { useCallback, useEffect, useState } from 'react'
+import React, { useEffect, useState } from 'react'
 import { RiAddLine, RiCloseLine, RiRefreshLine, RiSearchLine } from '@remixicon/react'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
-import { delCorpus, fetchIntentType } from '@/service/common'
+import { addIntentType, delIntentType, editIntentType, fetchIntentType } from '@/service/common'
 import 'react-multi-email/dist/style.css'
 import Input from '@/app/components/base/input'
 import useSWR from 'swr'
@@ -13,7 +13,6 @@ import Confirm from '@/app/components/base/confirm'
 
 const TypeModal = ({
   onCancel,
-  onSend,
 }: any) => {
   const [page, setPage] = React.useState<number>(0)
   const [limit, setLimit] = useState<number>(10)
@@ -21,7 +20,7 @@ const TypeModal = ({
   const [query, setQuery] = useState<any>({})
   const { data, mutate }: any = useSWR(
     {
-      url: '/xxx',
+      url: '/intentions/types',
       params: {
         page: page + 1,
         limit,
@@ -32,57 +31,65 @@ const TypeModal = ({
   )
   const list: any = data?.data || []
   const total = data?.total || 0
+  const [refresh, setRefresh] = useState<boolean>(false)
   const handleSearch = (reset = false) => {
-    if (reset)
-      setIntentType('')
-
+    setRefresh(true)
+    setPage(0)
     const params: any = {}
-    if (intentType)
-      params.intentType = intentType
+    if (reset) {
+      setIntentType('')
+    }
+    else {
+      if (intentType)
+        params.search = intentType
+    }
     setQuery(params)
-    setPage(0)
+    mutate()
+    setRefresh(false)
   }
   useEffect(() => {
-    mutate()
+    if (!refresh)
+      mutate()
   }, [page, limit])
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
   const [row, setRow] = useState<any>({})
   const handleDel = async () => {
     try {
-      await delCorpus({
-        url: `/tags/${row.id}`,
+      await delIntentType({
+        url: `/intentions/types/${row.id}`,
         body: {},
       })
       setShowConfirmDelete(false)
+      handleSearch(false)
     }
     catch (e) { }
   }
   const [showDetail, setShowDetail] = useState(false)
   const [mode, setMode] = useState<any>('add')
-  const [editIntentType, setEditIntentType] = useState('')
-  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) { }
-  }, [mode, editIntentType, onCancel, onSend])
+  const [editIntentTypeName, setEditIntentTypeName] = useState('')
+  const handleSave = async () => {
+    try {
+      let res
+      if (mode === 'add') {
+        res = await addIntentType({
+          url: '/intentions/types',
+          body: { name: editIntentTypeName },
+        })
+      }
+      else {
+        res = await editIntentType({
+          url: `/intentions/types/${row.id}`,
+          body: { name: editIntentTypeName },
+        })
+      }
+      const { id }: any = res
+      if (id) {
+        setShowDetail(false)
+        handleSearch(false)
+      }
+    }
+    catch (e) { }
+  }
   return (
     <div>
       <Modal overflowVisible isShow onClose={() => { }} className="p-[24px 32px] w-[800px] max-w-[800px]">
@@ -120,7 +127,7 @@ const TypeModal = ({
             <Button variant='primary' className={cn('shrink-0')}
               onClick={() => {
                 setMode('add')
-                setEditIntentType('')
+                setEditIntentTypeName('')
                 setShowDetail(true)
               }}>
               <RiAddLine className='mr-1 h-4 w-4' />
@@ -145,16 +152,17 @@ const TypeModal = ({
                         className={'h-8 border-b border-divider-subtle hover:bg-background-default-hover'}
                       >
                         <td>{item.name}</td>
-                        <td>4</td>
+                        <td>{item.intention_count}</td>
                         <td className="flex justify-center gap-2">
                           <Button variant='ghost-accent' size='small' className={cn('shrink-0')} onClick={() => {
                             setMode('edit')
-                            setEditIntentType(item.name)
+                            setRow(item)
+                            setEditIntentTypeName(item.name)
                             setShowDetail(true)
                           }}>
                             编辑
                           </Button>
-                          <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} onClick={() => {
+                          <Button variant='ghost' size='small' className={cn('shrink-0 text-red-600')} disabled={item.intention_count > 0} onClick={() => {
                             setRow(item)
                             setShowConfirmDelete(true)
                           }}>
@@ -203,8 +211,8 @@ const TypeModal = ({
                   意图类型
                 </div>
                 <Input
-                  value={editIntentType}
-                  onChange={e => setEditIntentType(e.target.value)}
+                  value={editIntentTypeName}
+                  onChange={e => setEditIntentTypeName(e.target.value)}
                   className='h-9'
                   placeholder='请输入意图类型'
                 />
@@ -213,7 +221,7 @@ const TypeModal = ({
                 tabIndex={0}
                 className='w-full'
                 onClick={handleSave}
-                disabled={!editIntentType.length}
+                disabled={!editIntentTypeName.length}
                 variant='primary'
               >
                 保存

+ 0 - 25
web/app/components/workflow/hooks/use-workflow.ts

@@ -537,31 +537,6 @@ export const useWorkflowInit = () => {
   const handleFetchPreloadData = useCallback(async () => {
     try {
       const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
-      nodesDefaultConfigsData.push({
-        type: 'intent-recon-train',
-        config: {
-          method: 'get',
-          authorization: {
-            type: 'no-auth',
-          },
-          body: {
-            type: 'none',
-          },
-          timeout: {
-            connect: 10,
-            read: 60,
-            write: 20,
-            max_connect_timeout: 10,
-            max_read_timeout: 60,
-            max_write_timeout: 20,
-          },
-        },
-        retry_config: {
-          max_retries: 3,
-          retry_interval: 2,
-          retry_enabled: true,
-        },
-      })
       const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
       workflowStore.setState({
         nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => {

+ 17 - 3
web/app/layout.tsx

@@ -7,6 +7,9 @@ import { TanstackQueryIniter } from '@/context/query-client'
 import { ThemeProvider } from 'next-themes'
 import './styles/globals.css'
 import './styles/markdown.scss'
+import './styles/cus.scss'
+import Fix from '@/utils/fix'
+import { ConfigProvider } from 'antd'
 
 export const metadata = {
   title: 'Dify',
@@ -63,9 +66,20 @@ const LocaleLayout = async ({
                 enableSystem
                 disableTransitionOnChange
               >
-                <I18nServer>
-                  {children}
-                </I18nServer>
+                <ConfigProvider theme={{
+                  components: {
+                    // Select: {
+                    //   colorBgBase: '#c8ceda40',
+                    //   colorBorder: '#c8ceda40',
+                    //   algorithm: true, // 启用算法
+                    // },
+                  },
+                }}>
+                  <I18nServer>
+                    <Fix/>
+                    {children}
+                  </I18nServer>
+                </ConfigProvider>
               </ThemeProvider>
             </TanstackQueryIniter>
           </SentryInitor>

+ 4 - 0
web/app/styles/cus.scss

@@ -0,0 +1,4 @@
+.ant-select-selector {
+  background-color: var(--color-components-input-bg-normal) !important;
+  border-color: transparent !important;
+}

+ 1 - 1
web/app/styles/globals.css

@@ -697,4 +697,4 @@ button:focus-within {
     -ms-overflow-style: none;
     scrollbar-width: none;
   }
-}
+}

+ 2 - 0
web/models/datasets.ts

@@ -158,6 +158,8 @@ export type FetchDatasetsParams = {
     include_all?: boolean
     keyword?: string,
     category_ids?: string[],
+    type?: string,
+    dept?: string,
   }
 }
 

+ 1 - 0
web/package.json

@@ -55,6 +55,7 @@
     "@tanstack/react-query": "^5.60.5",
     "@tanstack/react-query-devtools": "^5.60.5",
     "ahooks": "^3.8.4",
+    "antd": "^5.24.9",
     "class-variance-authority": "^0.7.0",
     "classnames": "^2.5.1",
     "copy-to-clipboard": "^3.3.3",

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 911 - 0
web/pnpm-lock.yaml


+ 148 - 114
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 }) => {
@@ -502,10 +454,10 @@ export const fetchLog = ({ url, params }: any) => {
         })
       }
       resolve({
-        data: arr,
+        data: [],
         has_more: false,
         limit: 10,
-        total: 100,
+        total: 0,
         page: 1,
       })
     }, 1000)
@@ -514,73 +466,72 @@ export const fetchLog = ({ url, params }: any) => {
 
 export const fetchIntentType = ({ url, params }: any) => {
   console.log('查询意图类型', url, params)
-  // 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}`,
-          binding_count: i,
-        })
-      }
-      resolve({
-        data: arr,
-        has_more: false,
-        limit: 10,
-        total: 100,
-        page: 1,
-      })
-    }, 1000)
-  })
+  return get(url, { params })
 }
 
-export const fetchIntentName = ({ url, params }: any) => {
-  console.log('查询意图名称', url, params)
-  // 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}`,
-        })
-      }
-      resolve({
-        data: arr,
-        has_more: false,
-        limit: 10,
-        total: 100,
-        page: 1,
-      })
-    }, 1000)
-  })
+export const addIntentType = ({ url, body }: any) => {
+  console.log('新增意图类型', url, body)
+  return post(url, { body })
+}
+
+export const editIntentType = ({ url, body }: any) => {
+  console.log('编辑意图类型', url, body)
+  return patch(url, { body })
+}
+
+export const delIntentType = ({ url, body }: any) => {
+  console.log('删除意图类型', url, body)
+  return del(url, { body })
+}
+
+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 })
+}
+
+export const editIntent = ({ url, body }: any) => {
+  console.log('编辑意图', url, body)
+  return patch(url, { body })
+}
+
+export const delIntent = ({ url, body }: any) => {
+  console.log('删除意图', url, body)
+  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 })
-  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,
-        has_more: false,
-        limit: 10,
-        total: 100,
-        page: 1,
-      })
-    }, 1000)
-  })
+  return get(url, { params })
 }
 
 export const addCorpus = ({ url, body }: any) => {
@@ -598,6 +549,16 @@ export const delCorpus = ({ url, body }: any) => {
   return del(url, { body })
 }
 
+export const getCorpus = ({ url, params }: any) => {
+  console.log('查询训练语料', url, params)
+  return get(url, { params })
+}
+
+export const fetchCorpusQuestion = ({ url, params }: any) => {
+  console.log('查询训练语料-相似问题', url, params)
+  return get(url, { params })
+}
+
 export const addCorpusQuestion = ({ url, body }: any) => {
   console.log('新增训练语料-相似问题', url, body)
   return post(url, { body })
@@ -608,7 +569,80 @@ export const editCorpusQuestion = ({ url, body }: any) => {
   return patch(url, { body })
 }
 
-export const delCorpusQuestion = ({ url, body }: any) => {
-  console.log('删除训练语料-相似问题', url, body)
-  return del(url, { body })
+export const delBatchCorpusQuestion = ({ url, body }: any) => {
+  console.log('批量删除训练语料-相似问题', url, body)
+  return post(url, { body })
+}
+
+export const fetchDepts = ({ url, params }: any) => {
+  console.log('查询部门列表', params, url)
+  // return get(url, { params })
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const arr: any = []
+      for (let i = 1; i < 3; i++) {
+        const dept1: any = {
+          id: `${i}`,
+          name: `部门_${i}`,
+          children: [],
+        }
+        for (let j = 1; j < 4; j++) {
+          const dept2: any = {
+            id: `${i}_${j}`,
+            name: `部门_${i}-${j}`,
+            children: [],
+          }
+          for (let k = 1; k < 5; k++) {
+            const dept3: any = {
+              id: `${i}_${j}_${k}`,
+              name: `部门_${i}-${j}-${k}`,
+              relation: k % 2,
+            }
+            dept2.children.push(dept3)
+          }
+          dept1.children.push(dept2)
+        }
+        arr.push(dept1)
+      }
+      resolve({
+        data: arr,
+      })
+    }, 1000)
+  })
+}
+export const fetchDeptUsers = ({ url, params }: any) => {
+  console.log('查询部门用户列表', params, url)
+  // return get(url, { params })
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      const arr: any = []
+      for (let i = 1; i < 3; i++) {
+        const dept1: any = {
+          id: `${i}`,
+          name: `部门_${i}`,
+          children: [],
+        }
+        for (let j = 1; j < 4; j++) {
+          const dept2: any = {
+            id: `${i}_${j}`,
+            name: `部门_${i}-${j}`,
+            children: [],
+          }
+          for (let k = 1; k < 5; k++) {
+            const dept3: any = {
+              id: `${i}_${j}_${k}`,
+              name: `用户_${i}-${j}-${k}`,
+              relation: k % 2,
+            }
+            dept2.children.push(dept3)
+          }
+          dept1.children.push(dept2)
+        }
+        arr.push(dept1)
+      }
+      resolve({
+        data: arr,
+      })
+    }, 1000)
+  })
 }

+ 20 - 0
web/utils/fix.ts

@@ -0,0 +1,20 @@
+'use client'
+import { unstableSetRender } from 'antd'
+import { createRoot } from 'react-dom/client'
+
+unstableSetRender((node, container: any) => {
+  container._reactRoot ||= createRoot(container)
+  const root: ReturnType<typeof createRoot> = container._reactRoot
+  root.render(node)
+
+  return () =>
+    new Promise<void>((resolve) => {
+      setTimeout(() => {
+        root.unmount()
+        resolve()
+      }, 0)
+    })
+})
+export default function Fix() {
+  return null
+}