mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: 分段管理支持批量迁移,删除分段 #113,#103
This commit is contained in:
parent
8204d5ff44
commit
d4e742f7c6
|
|
@ -51,8 +51,15 @@ class UpdateProblemArgs:
|
|||
|
||||
|
||||
class UpdateEmbeddingDatasetIdArgs:
|
||||
def __init__(self, source_id_list: List[str], target_dataset_id: str):
|
||||
self.source_id_list = source_id_list
|
||||
def __init__(self, paragraph_id_list: List[str], target_dataset_id: str):
|
||||
self.paragraph_id_list = paragraph_id_list
|
||||
self.target_dataset_id = target_dataset_id
|
||||
|
||||
|
||||
class UpdateEmbeddingDocumentIdArgs:
|
||||
def __init__(self, paragraph_id_list: List[str], target_document_id: str, target_dataset_id: str):
|
||||
self.paragraph_id_list = paragraph_id_list
|
||||
self.target_document_id = target_document_id
|
||||
self.target_dataset_id = target_dataset_id
|
||||
|
||||
|
||||
|
|
@ -213,13 +220,23 @@ class ListenerManagement:
|
|||
|
||||
@staticmethod
|
||||
def update_embedding_dataset_id(args: UpdateEmbeddingDatasetIdArgs):
|
||||
VectorStore.get_embedding_vector().update_by_source_ids(args.source_id_list,
|
||||
{'dataset_id': args.target_dataset_id})
|
||||
VectorStore.get_embedding_vector().update_by_paragraph_ids(args.paragraph_id_list,
|
||||
{'dataset_id': args.target_dataset_id})
|
||||
|
||||
@staticmethod
|
||||
def update_embedding_document_id(args: UpdateEmbeddingDocumentIdArgs):
|
||||
VectorStore.get_embedding_vector().update_by_paragraph_ids(args.paragraph_id_list,
|
||||
{'document_id': args.target_document_id,
|
||||
'dataset_id': args.target_dataset_id})
|
||||
|
||||
@staticmethod
|
||||
def delete_embedding_by_source_ids(source_ids: List[str]):
|
||||
VectorStore.get_embedding_vector().delete_by_source_ids(source_ids, SourceType.PROBLEM)
|
||||
|
||||
@staticmethod
|
||||
def delete_embedding_by_paragraph_ids(paragraph_ids: List[str]):
|
||||
VectorStore.get_embedding_vector().delete_by_paragraph_ids(paragraph_ids)
|
||||
|
||||
@staticmethod
|
||||
def delete_embedding_by_dataset_id_list(source_ids: List[str]):
|
||||
VectorStore.get_embedding_vector().delete_by_dataset_id_list(source_ids)
|
||||
|
|
|
|||
|
|
@ -166,10 +166,12 @@ class DocumentSerializers(ApiMixin, serializers.Serializer):
|
|||
meta={})
|
||||
else:
|
||||
document_list.update(dataset_id=target_dataset_id)
|
||||
paragraph_list.update(dataset_id=target_dataset_id)
|
||||
# 修改向量信息
|
||||
ListenerManagement.update_embedding_dataset_id(UpdateEmbeddingDatasetIdArgs(
|
||||
[problem_paragraph_mapping.id for problem_paragraph_mapping in problem_paragraph_mapping_list],
|
||||
[paragraph.id for paragraph in paragraph_list],
|
||||
target_dataset_id))
|
||||
# 修改段落信息
|
||||
paragraph_list.update(dataset_id=target_dataset_id)
|
||||
|
||||
@staticmethod
|
||||
def get_target_dataset_problem(target_dataset_id: str,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ from drf_yasg import openapi
|
|||
from rest_framework import serializers
|
||||
|
||||
from common.db.search import page_search
|
||||
from common.event.listener_manage import ListenerManagement
|
||||
from common.event.listener_manage import ListenerManagement, UpdateEmbeddingDocumentIdArgs, UpdateEmbeddingDatasetIdArgs
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.mixins.api_mixin import ApiMixin
|
||||
from common.util.common import post
|
||||
from common.util.field_message import ErrMessage
|
||||
from dataset.models import Paragraph, Problem, Document, ProblemParagraphMapping
|
||||
from dataset.serializers.common_serializers import update_document_char_length
|
||||
from dataset.serializers.common_serializers import update_document_char_length, BatchSerializer
|
||||
from dataset.serializers.problem_serializers import ProblemInstanceSerializer, ProblemSerializer, ProblemSerializers
|
||||
from embedding.models import SourceType
|
||||
|
||||
|
|
@ -272,6 +272,167 @@ class ParagraphSerializers(ApiMixin, serializers.Serializer):
|
|||
description='问题id')
|
||||
]
|
||||
|
||||
class Batch(serializers.Serializer):
|
||||
dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id"))
|
||||
document_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("文档id"))
|
||||
|
||||
@transaction.atomic
|
||||
def batch_delete(self, instance: Dict, with_valid=True):
|
||||
if with_valid:
|
||||
BatchSerializer(data=instance).is_valid(model=Paragraph, raise_exception=True)
|
||||
self.is_valid(raise_exception=True)
|
||||
paragraph_id_list = instance.get("id_list")
|
||||
QuerySet(Paragraph).filter(id__in=paragraph_id_list).delete()
|
||||
QuerySet(ProblemParagraphMapping).filter(paragraph_id__in=paragraph_id_list).delete()
|
||||
# 删除向量库
|
||||
ListenerManagement.delete_embedding_by_paragraph_ids(paragraph_id_list)
|
||||
return True
|
||||
|
||||
class Migrate(ApiMixin, serializers.Serializer):
|
||||
dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("知识库id"))
|
||||
document_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("文档id"))
|
||||
target_dataset_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("目标知识库id"))
|
||||
target_document_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("目标文档id"))
|
||||
paragraph_id_list = serializers.ListField(required=True, error_messages=ErrMessage.char("段落列表"),
|
||||
child=serializers.UUIDField(required=True,
|
||||
error_messages=ErrMessage.uuid("段落id")))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
document_list = QuerySet(Document).filter(
|
||||
id__in=[self.data.get('document_id'), self.data.get('target_document_id')])
|
||||
document_id = self.data.get('document_id')
|
||||
target_document_id = self.data.get('target_document_id')
|
||||
if document_id == target_document_id:
|
||||
raise AppApiException(5000, "需要迁移的文档和目标文档一致")
|
||||
if len([document for document in document_list if str(document.id) == self.data.get('document_id')]) < 1:
|
||||
raise AppApiException(5000, f"文档id不存在【{self.data.get('document_id')}】")
|
||||
if len([document for document in document_list if
|
||||
str(document.id) == self.data.get('target_document_id')]) < 1:
|
||||
raise AppApiException(5000, f"目标文档id不存在【{self.data.get('target_document_id')}】")
|
||||
|
||||
@transaction.atomic
|
||||
def migrate(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
dataset_id = self.data.get('dataset_id')
|
||||
target_dataset_id = self.data.get('target_dataset_id')
|
||||
document_id = self.data.get('document_id')
|
||||
target_document_id = self.data.get('target_document_id')
|
||||
paragraph_id_list = self.data.get('paragraph_id_list')
|
||||
paragraph_list = QuerySet(Paragraph).filter(dataset_id=dataset_id, document_id=document_id,
|
||||
id__in=paragraph_id_list)
|
||||
problem_paragraph_mapping_list = QuerySet(ProblemParagraphMapping).filter(paragraph__in=paragraph_list)
|
||||
# 同数据集迁移
|
||||
if target_dataset_id == dataset_id:
|
||||
if len(problem_paragraph_mapping_list):
|
||||
problem_paragraph_mapping_list = [
|
||||
self.update_problem_paragraph_mapping(target_document_id,
|
||||
problem_paragraph_mapping) for problem_paragraph_mapping
|
||||
in
|
||||
problem_paragraph_mapping_list]
|
||||
# 修改mapping
|
||||
QuerySet(ProblemParagraphMapping).bulk_update(problem_paragraph_mapping_list,
|
||||
['document_id'])
|
||||
# 修改向量段落信息
|
||||
ListenerManagement.update_embedding_document_id(UpdateEmbeddingDocumentIdArgs(
|
||||
[paragraph.id for paragraph in paragraph_list],
|
||||
target_document_id, target_dataset_id))
|
||||
# 修改段落信息
|
||||
paragraph_list.update(document_id=target_document_id)
|
||||
# 不同数据集迁移
|
||||
else:
|
||||
problem_list = QuerySet(Problem).filter(
|
||||
id__in=[problem_paragraph_mapping.problem_id for problem_paragraph_mapping in
|
||||
problem_paragraph_mapping_list])
|
||||
# 目标数据集问题
|
||||
target_problem_list = list(
|
||||
QuerySet(Problem).filter(content__in=[problem.content for problem in problem_list],
|
||||
dataset_id=target_dataset_id))
|
||||
|
||||
target_handle_problem_list = [
|
||||
self.get_target_dataset_problem(target_dataset_id, target_document_id, problem_paragraph_mapping,
|
||||
problem_list, target_problem_list) for
|
||||
problem_paragraph_mapping
|
||||
in
|
||||
problem_paragraph_mapping_list]
|
||||
|
||||
create_problem_list = [problem for problem, is_create in target_handle_problem_list if
|
||||
is_create is not None and is_create]
|
||||
# 插入问题
|
||||
QuerySet(Problem).bulk_create(create_problem_list)
|
||||
# 修改mapping
|
||||
QuerySet(ProblemParagraphMapping).bulk_update(problem_paragraph_mapping_list,
|
||||
['problem_id', 'dataset_id', 'document_id'])
|
||||
# 修改向量段落信息
|
||||
ListenerManagement.update_embedding_document_id(UpdateEmbeddingDocumentIdArgs(
|
||||
[paragraph.id for paragraph in paragraph_list],
|
||||
target_document_id, target_dataset_id))
|
||||
# 修改段落信息
|
||||
paragraph_list.update(dataset_id=target_dataset_id, document_id=target_document_id)
|
||||
|
||||
@staticmethod
|
||||
def update_problem_paragraph_mapping(target_document_id: str, problem_paragraph_mapping):
|
||||
problem_paragraph_mapping.document_id = target_document_id
|
||||
return problem_paragraph_mapping
|
||||
|
||||
@staticmethod
|
||||
def get_target_dataset_problem(target_dataset_id: str,
|
||||
target_document_id: str,
|
||||
problem_paragraph_mapping,
|
||||
source_problem_list,
|
||||
target_problem_list):
|
||||
source_problem_list = [source_problem for source_problem in source_problem_list if
|
||||
source_problem.id == problem_paragraph_mapping.problem_id]
|
||||
problem_paragraph_mapping.dataset_id = target_dataset_id
|
||||
problem_paragraph_mapping.document_id = target_document_id
|
||||
if len(source_problem_list) > 0:
|
||||
problem_content = source_problem_list[-1].content
|
||||
problem_list = [problem for problem in target_problem_list if problem.content == problem_content]
|
||||
if len(problem_list) > 0:
|
||||
problem = problem_list[-1]
|
||||
problem_paragraph_mapping.problem_id = problem.id
|
||||
return problem, False
|
||||
else:
|
||||
problem = Problem(id=uuid.uuid1(), dataset_id=target_dataset_id, content=problem_content)
|
||||
target_problem_list.append(problem)
|
||||
problem_paragraph_mapping.problem_id = problem.id
|
||||
return problem, True
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
return [openapi.Parameter(name='dataset_id',
|
||||
in_=openapi.IN_PATH,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
description='文档id'),
|
||||
openapi.Parameter(name='document_id',
|
||||
in_=openapi.IN_PATH,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
description='文档id'),
|
||||
openapi.Parameter(name='target_dataset_id',
|
||||
in_=openapi.IN_PATH,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
description='目标知识库id'),
|
||||
openapi.Parameter(name='target_document_id',
|
||||
in_=openapi.IN_PATH,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
description='目标知识库id')
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||
title='段落id列表',
|
||||
description="段落id列表"
|
||||
)
|
||||
|
||||
class Operate(ApiMixin, serializers.Serializer):
|
||||
# 段落id
|
||||
paragraph_id = serializers.UUIDField(required=True, error_messages=ErrMessage.char(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ urlpatterns = [
|
|||
path('dataset/<str:dataset_id>/document/migrate/<str:target_dataset_id>', views.Document.Migrate.as_view()),
|
||||
path('dataset/<str:dataset_id>/document/<str:document_id>/refresh', views.Document.Refresh.as_view()),
|
||||
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph', views.Paragraph.as_view()),
|
||||
path(
|
||||
'dataset/<str:dataset_id>/document/<str:document_id>/paragraph/migrate/dataset/<str:target_dataset_id>/document/<str:target_document_id>',
|
||||
views.Paragraph.BatchMigrate.as_view()),
|
||||
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph/_batch', views.Paragraph.Batch.as_view()),
|
||||
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph/<int:current_page>/<int:page_size>',
|
||||
views.Paragraph.Page.as_view(), name='paragraph_page'),
|
||||
path('dataset/<str:dataset_id>/document/<str:document_id>/paragraph/<paragraph_id>',
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ from rest_framework.views import APIView
|
|||
from rest_framework.views import Request
|
||||
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import Permission, Group, Operate
|
||||
from common.constants.permission_constants import Permission, Group, Operate, CompareConstants
|
||||
from common.response import result
|
||||
from common.util.common import query_params_to_single_dict
|
||||
from dataset.serializers.common_serializers import BatchSerializer
|
||||
from dataset.serializers.paragraph_serializers import ParagraphSerializers
|
||||
|
||||
|
||||
|
|
@ -168,6 +169,50 @@ class Paragraph(APIView):
|
|||
o.is_valid(raise_exception=True)
|
||||
return result.success(o.delete())
|
||||
|
||||
class Batch(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['DELETE'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="批量删除段落",
|
||||
operation_id="批量删除段落",
|
||||
request_body=
|
||||
BatchSerializer.get_request_body_api(),
|
||||
manual_parameters=ParagraphSerializers.Create.get_request_params_api(),
|
||||
responses=result.get_default_response(),
|
||||
tags=["知识库/文档/段落"])
|
||||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')))
|
||||
def delete(self, request: Request, dataset_id: str, document_id: str):
|
||||
return result.success(ParagraphSerializers.Batch(
|
||||
data={"dataset_id": dataset_id, 'document_id': document_id}).batch_delete(request.data))
|
||||
|
||||
class BatchMigrate(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['PUT'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="批量迁移段落",
|
||||
operation_id="批量迁移段落",
|
||||
manual_parameters=ParagraphSerializers.Migrate.get_request_params_api(),
|
||||
request_body=ParagraphSerializers.Migrate.get_request_body_api(),
|
||||
responses=result.get_default_response(),
|
||||
tags=["知识库/文档/段落"]
|
||||
)
|
||||
@has_permissions(
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('dataset_id')),
|
||||
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
|
||||
dynamic_tag=k.get('target_dataset_id')),
|
||||
compare=CompareConstants.AND
|
||||
)
|
||||
def put(self, request: Request, dataset_id: str, target_dataset_id: str, document_id: str, target_document_id):
|
||||
return result.success(
|
||||
ParagraphSerializers.Migrate(
|
||||
data={'dataset_id': dataset_id, 'target_dataset_id': target_dataset_id,
|
||||
'document_id': document_id,
|
||||
'target_document_id': target_document_id,
|
||||
'paragraph_id_list': request.data}).migrate())
|
||||
|
||||
class Page(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class BaseVectorStore(ABC):
|
|||
return result[0]
|
||||
|
||||
@abstractmethod
|
||||
def query(self, query_text:str,query_embedding: List[float], dataset_id_list: list[str],
|
||||
def query(self, query_text: str, query_embedding: List[float], dataset_id_list: list[str],
|
||||
exclude_document_id_list: list[str],
|
||||
exclude_paragraph_list: list[str], is_active: bool, top_n: int, similarity: float,
|
||||
search_mode: SearchMode):
|
||||
|
|
@ -130,6 +130,10 @@ class BaseVectorStore(ABC):
|
|||
def update_by_paragraph_id(self, paragraph_id: str, instance: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_by_paragraph_ids(self, paragraph_ids: str, instance: Dict):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_by_source_id(self, source_id: str, instance: Dict):
|
||||
pass
|
||||
|
|
@ -173,3 +177,7 @@ class BaseVectorStore(ABC):
|
|||
@abstractmethod
|
||||
def delete_by_paragraph_id(self, paragraph_id: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_by_paragraph_ids(self, paragraph_ids: List[str]):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@ class PGVector(BaseVectorStore):
|
|||
def update_by_paragraph_id(self, paragraph_id: str, instance: Dict):
|
||||
QuerySet(Embedding).filter(paragraph_id=paragraph_id).update(**instance)
|
||||
|
||||
def update_by_paragraph_ids(self, paragraph_id: str, instance: Dict):
|
||||
QuerySet(Embedding).filter(paragraph_id__in=paragraph_id).update(**instance)
|
||||
|
||||
def delete_by_dataset_id(self, dataset_id: str):
|
||||
QuerySet(Embedding).filter(dataset_id=dataset_id).delete()
|
||||
|
||||
|
|
@ -139,6 +142,9 @@ class PGVector(BaseVectorStore):
|
|||
def delete_by_paragraph_id(self, paragraph_id: str):
|
||||
QuerySet(Embedding).filter(paragraph_id=paragraph_id).delete()
|
||||
|
||||
def delete_by_paragraph_ids(self, paragraph_ids: List[str]):
|
||||
QuerySet(Embedding).filter(paragraph_id__in=paragraph_ids).delete()
|
||||
|
||||
|
||||
class ISearch(ABC):
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -48,6 +48,24 @@ const delParagraph: (
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除段落
|
||||
* @param 参数 dataset_id, document_id
|
||||
*/
|
||||
const delMulParagraph: (
|
||||
dataset_id: string,
|
||||
document_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<boolean>> = (dataset_id, document_id, data, loading) => {
|
||||
return del(
|
||||
`${prefix}/${dataset_id}/document/${document_id}/paragraph/_batch`,
|
||||
undefined,
|
||||
{ id_list: data },
|
||||
loading
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建段落
|
||||
* @param 参数
|
||||
|
|
@ -104,6 +122,33 @@ const putParagraph: (
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量迁移段落
|
||||
* @param 参数 dataset_id,target_dataset_id,
|
||||
*/
|
||||
const putMigrateMulParagraph: (
|
||||
dataset_id: string,
|
||||
document_id: string,
|
||||
target_dataset_id: string,
|
||||
target_document_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<boolean>> = (
|
||||
dataset_id,
|
||||
document_id,
|
||||
target_dataset_id,
|
||||
target_document_id,
|
||||
data,
|
||||
loading
|
||||
) => {
|
||||
return put(
|
||||
`${prefix}/${dataset_id}/document/${document_id}/paragraph/migrate/dataset/${target_dataset_id}/document/${target_document_id}`,
|
||||
data,
|
||||
undefined,
|
||||
loading
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 问题列表
|
||||
* @param 参数 dataset_id,document_id,paragraph_id
|
||||
|
|
@ -189,5 +234,7 @@ export default {
|
|||
getProblem,
|
||||
postProblem,
|
||||
disassociationProblem,
|
||||
associationProblem
|
||||
associationProblem,
|
||||
delMulParagraph,
|
||||
putMigrateMulParagraph
|
||||
}
|
||||
|
|
|
|||
|
|
@ -573,3 +573,33 @@ h4 {
|
|||
color: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
// card 选中样式
|
||||
.selected {
|
||||
border: 1px solid var(--el-color-primary) !important;
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border: 14px solid var(--el-color-primary);
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 2px;
|
||||
border: 2px solid #fff;
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
transform: rotate(35deg);
|
||||
}
|
||||
&:hover {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,15 @@
|
|||
v-if="datasetDetail.type === '1'"
|
||||
>同步文档</el-button
|
||||
>
|
||||
<el-button @click="openDatasetDialog()" :disabled="multipleSelection.length === 0"
|
||||
>迁移</el-button
|
||||
>
|
||||
<el-button @click="openBatchEditDocument" :disabled="multipleSelection.length === 0"
|
||||
>设置</el-button
|
||||
>
|
||||
<el-button @click="deleteMulDocument" :disabled="multipleSelection.length === 0"
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button @click="openDatasetDialog()" :disabled="multipleSelection.length === 0">
|
||||
迁移
|
||||
</el-button>
|
||||
<el-button @click="openBatchEditDocument" :disabled="multipleSelection.length === 0">
|
||||
设置
|
||||
</el-button>
|
||||
<el-button @click="deleteMulDocument" :disabled="multipleSelection.length === 0">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
|
|
@ -327,9 +327,7 @@ const handleSelectionChange = (val: any[]) => {
|
|||
|
||||
function openBatchEditDocument() {
|
||||
const arr: string[] = multipleSelection.value.map((v) => v.id)
|
||||
if (batchEditDocumentDialogRef) {
|
||||
batchEditDocumentDialogRef?.value?.open(arr)
|
||||
}
|
||||
batchEditDocumentDialogRef?.value?.open(arr)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<el-dialog title="选择知识库/文档" v-model="dialogVisible" width="500">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
:rules="rules"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item label="选择知识库" prop="dataset_id">
|
||||
<el-select
|
||||
v-model="form.dataset_id"
|
||||
filterable
|
||||
placeholder="请选择知识库"
|
||||
:loading="optionLoading"
|
||||
@change="changeDataset"
|
||||
>
|
||||
<el-option v-for="item in datasetList" :key="item.id" :label="item.name" :value="item.id">
|
||||
<span class="flex align-center">
|
||||
<AppAvatar
|
||||
v-if="!item.dataset_id && item.type === '1'"
|
||||
class="mr-12 avatar-purple"
|
||||
shape="square"
|
||||
:size="24"
|
||||
>
|
||||
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="!item.dataset_id && item.type === '0'"
|
||||
class="mr-12"
|
||||
shape="square"
|
||||
:size="24"
|
||||
>
|
||||
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="保存至文档" prop="document_id">
|
||||
<el-select
|
||||
v-model="form.document_id"
|
||||
filterable
|
||||
placeholder="请选择文档"
|
||||
:loading="optionLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in documentList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)" :loading="loading"> 迁移 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import paragraphApi from '@/api/paragraph'
|
||||
import useStore from '@/stores'
|
||||
|
||||
const { dataset, document } = useStore()
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id, documentId }
|
||||
} = route as any
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
const formRef = ref()
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref<any>({
|
||||
dataset_id: '',
|
||||
document_id: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
dataset_id: [{ required: true, message: '请选择知识库', trigger: 'change' }],
|
||||
document_id: [{ required: true, message: '请选择文档', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const datasetList = ref<any[]>([])
|
||||
const documentList = ref<any[]>([])
|
||||
const optionLoading = ref(false)
|
||||
const paragraphList = ref<string[]>([])
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
dataset_id: '',
|
||||
document_id: ''
|
||||
}
|
||||
datasetList.value = []
|
||||
documentList.value = []
|
||||
paragraphList.value = []
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
function changeDataset(id: string) {
|
||||
form.value.document_id = ''
|
||||
getDocument(id)
|
||||
}
|
||||
|
||||
function getDocument(id: string) {
|
||||
document.asyncGetAllDocument(id, loading).then((res: any) => {
|
||||
documentList.value = res.data?.filter((v: any) => v.id !== documentId)
|
||||
})
|
||||
}
|
||||
|
||||
function getDataset() {
|
||||
dataset.asyncGetAllDataset(loading).then((res: any) => {
|
||||
datasetList.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const open = (list: any) => {
|
||||
paragraphList.value = list
|
||||
getDataset()
|
||||
formRef.value?.clearValidate()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
paragraphApi
|
||||
.putMigrateMulParagraph(
|
||||
id,
|
||||
documentId,
|
||||
form.value.dataset_id,
|
||||
form.value.document_id,
|
||||
paragraphList.value,
|
||||
loading
|
||||
)
|
||||
.then(() => {
|
||||
emit('refresh')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scope></style>
|
||||
|
|
@ -8,7 +8,20 @@
|
|||
>)</el-text
|
||||
>
|
||||
<div class="document-detail__header">
|
||||
<el-button @click="addParagraph" type="primary" :disabled="loading"> 添加分段 </el-button>
|
||||
<el-button @click="batchSelectedHandle(true)" v-if="isBatch === false">
|
||||
批量选择
|
||||
</el-button>
|
||||
<el-button @click="batchSelectedHandle(false)" v-if="isBatch === true">
|
||||
取消选择
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="addParagraph"
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
v-if="isBatch === false"
|
||||
>
|
||||
添加分段
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
|
|
@ -57,7 +70,28 @@
|
|||
:key="index"
|
||||
class="p-8"
|
||||
>
|
||||
<!-- 批量操作card -->
|
||||
<CardBox
|
||||
v-if="isBatch === true"
|
||||
shadow="hover"
|
||||
:title="item.title || '-'"
|
||||
:description="item.content"
|
||||
class="document-card cursor"
|
||||
:class="multipleSelection.includes(item.id) ? 'selected' : ''"
|
||||
:showIcon="false"
|
||||
@click="selectHandle(item.id)"
|
||||
>
|
||||
<div class="active-button" @click.stop></div>
|
||||
|
||||
<template #footer>
|
||||
<div class="footer-content flex-between">
|
||||
<span> {{ numberFormat(item?.content.length) || 0 }} 个 字符 </span>
|
||||
</div>
|
||||
</template>
|
||||
</CardBox>
|
||||
<!-- 非批量操作card -->
|
||||
<CardBox
|
||||
v-else
|
||||
shadow="hover"
|
||||
:title="item.title || '-'"
|
||||
:description="item.content"
|
||||
|
|
@ -77,11 +111,25 @@
|
|||
<template #footer>
|
||||
<div class="footer-content flex-between">
|
||||
<span> {{ numberFormat(item?.content.length) || 0 }} 个 字符 </span>
|
||||
<el-tooltip effect="dark" content="删除" placement="top">
|
||||
<el-button text @click.stop="deleteParagraph(item)" class="delete-button">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<span @click.stop>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button text>
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSelectDocumentDialog(item)">
|
||||
<AppIcon iconName="app-migrate"></AppIcon>
|
||||
迁移</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item icon="Delete" @click.stop="deleteParagraph(item)"
|
||||
>删除</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardBox>
|
||||
|
|
@ -90,8 +138,20 @@
|
|||
</InfiniteScroll>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="mul-operation border-t w-full" v-if="isBatch === true">
|
||||
<el-button :disabled="multipleSelection.length === 0" @click="openSelectDocumentDialog">
|
||||
迁移
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="multipleSelection.length === 0" @click="deleteMulParagraph">
|
||||
删除
|
||||
</el-button>
|
||||
<span class="ml-8"> 已选 {{ multipleSelection.length }} 项 </span>
|
||||
</div>
|
||||
</div>
|
||||
<ParagraphDialog ref="ParagraphDialogRef" :title="title" @refresh="refresh" />
|
||||
<SelectDocumentDialog ref="SelectDocumentDialogRef" @refresh="refreshMigrateParagraph" />
|
||||
</LayoutContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -100,6 +160,7 @@ import { useRoute } from 'vue-router'
|
|||
import documentApi from '@/api/document'
|
||||
import paragraphApi from '@/api/paragraph'
|
||||
import ParagraphDialog from './component/ParagraphDialog.vue'
|
||||
import SelectDocumentDialog from './component/SelectDocumentDialog.vue'
|
||||
import { numberFormat } from '@/utils/utils'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
import useStore from '@/stores'
|
||||
|
|
@ -109,6 +170,7 @@ const {
|
|||
params: { id, documentId }
|
||||
} = route as any
|
||||
|
||||
const SelectDocumentDialogRef = ref()
|
||||
const ParagraphDialogRef = ref()
|
||||
const loading = ref(false)
|
||||
const changeStateloading = ref(false)
|
||||
|
|
@ -118,12 +180,66 @@ const title = ref('')
|
|||
const search = ref('')
|
||||
const searchType = ref('title')
|
||||
|
||||
// 批量操作
|
||||
const isBatch = ref(false)
|
||||
const multipleSelection = ref<any[]>([])
|
||||
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
function refreshMigrateParagraph() {
|
||||
paragraphDetail.value = paragraphDetail.value.filter(
|
||||
(v) => !multipleSelection.value.includes(v.id)
|
||||
)
|
||||
multipleSelection.value = []
|
||||
MsgSuccess('迁移删除成功')
|
||||
}
|
||||
|
||||
function openSelectDocumentDialog(row?: any) {
|
||||
if (row) {
|
||||
multipleSelection.value = [row.id]
|
||||
}
|
||||
SelectDocumentDialogRef.value.open(multipleSelection.value)
|
||||
}
|
||||
function deleteMulParagraph() {
|
||||
MsgConfirm(
|
||||
`是否批量删除 ${multipleSelection.value.length} 个分段?`,
|
||||
`删除后无法恢复,请谨慎操作。`,
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
confirmButtonClass: 'danger'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
paragraphApi
|
||||
.delMulParagraph(id, documentId, multipleSelection.value, changeStateloading)
|
||||
.then(() => {
|
||||
paragraphDetail.value = paragraphDetail.value.filter(
|
||||
(v) => !multipleSelection.value.includes(v.id)
|
||||
)
|
||||
multipleSelection.value = []
|
||||
MsgSuccess('批量删除成功')
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function batchSelectedHandle(bool: boolean) {
|
||||
isBatch.value = bool
|
||||
multipleSelection.value = []
|
||||
}
|
||||
|
||||
function selectHandle(id: string) {
|
||||
if (multipleSelection.value.includes(id)) {
|
||||
multipleSelection.value.splice(multipleSelection.value.indexOf(id), 1)
|
||||
} else {
|
||||
multipleSelection.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
function searchHandle() {
|
||||
paginationConfig.current_page = 1
|
||||
paragraphDetail.value = []
|
||||
|
|
@ -219,6 +335,12 @@ onMounted(() => {
|
|||
height: 210px;
|
||||
background: var(--app-layout-bg-color);
|
||||
border: 1px solid var(--app-layout-bg-color);
|
||||
&.selected {
|
||||
background: #ffffff;
|
||||
&:hover {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--el-border-color);
|
||||
|
|
@ -243,5 +365,18 @@ onMounted(() => {
|
|||
top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
.mul-operation {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 16px 24px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
:title="item.title || '-'"
|
||||
:description="item.content"
|
||||
class="paragraph-card cursor mb-16"
|
||||
:class="isAssociation(item.id) ? 'active' : ''"
|
||||
:class="isAssociation(item.id) ? 'selected' : ''"
|
||||
:showIcon="false"
|
||||
@click="associationClick(item)"
|
||||
>
|
||||
|
|
@ -244,31 +244,6 @@ defineExpose({ open })
|
|||
<style lang="scss" scope>
|
||||
.paragraph-card {
|
||||
position: relative;
|
||||
&.active {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border: 14px solid var(--el-color-primary);
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 2px;
|
||||
border: 2px solid #fff;
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
transform: rotate(35deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.paragraph-badge {
|
||||
.el-badge__content {
|
||||
|
|
|
|||
Loading…
Reference in New Issue