mirror of
https://github.com/langgenius/dify.git
synced 2024-11-15 19:22:36 +08:00
Knowledge optimization (#3755)
Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
parent
3cd8e6f5c6
commit
f257f2c396
|
@ -53,5 +53,8 @@ from .explore import (
|
|||
workflow,
|
||||
)
|
||||
|
||||
# Import tag controllers
|
||||
from .tag import tags
|
||||
|
||||
# Import workspace controllers
|
||||
from .workspace import account, members, model_providers, models, tool_providers, workspace
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
import json
|
||||
import uuid
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, inputs, marshal_with, reqparse
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from core.tools.tool_manager import ToolManager
|
||||
from core.tools.utils.configuration import ToolParameterConfigurationManager
|
||||
from fields.app_fields import (
|
||||
app_detail_fields,
|
||||
app_detail_fields_with_site,
|
||||
app_pagination_fields,
|
||||
)
|
||||
from libs.login import login_required
|
||||
from models.model import App, AppMode, AppModelConfig
|
||||
from services.app_service import AppService
|
||||
from services.tag_service import TagService
|
||||
|
||||
ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion']
|
||||
|
||||
|
@ -22,21 +29,29 @@ class AppListApi(Resource):
|
|||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(app_pagination_fields)
|
||||
def get(self):
|
||||
"""Get app list"""
|
||||
def uuid_list(value):
|
||||
try:
|
||||
return [str(uuid.UUID(v)) for v in value.split(',')]
|
||||
except ValueError:
|
||||
abort(400, message="Invalid UUID format in tag_ids.")
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('page', type=inputs.int_range(1, 99999), required=False, default=1, location='args')
|
||||
parser.add_argument('limit', type=inputs.int_range(1, 100), required=False, default=20, location='args')
|
||||
parser.add_argument('mode', type=str, choices=['chat', 'workflow', 'agent-chat', 'channel', 'all'], default='all', location='args', required=False)
|
||||
parser.add_argument('name', type=str, location='args', required=False)
|
||||
parser.add_argument('tag_ids', type=uuid_list, location='args', required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# get app list
|
||||
app_service = AppService()
|
||||
app_pagination = app_service.get_paginate_apps(current_user.current_tenant_id, args)
|
||||
if not app_pagination:
|
||||
return {'data': [], 'total': 0, 'page': 1, 'limit': 20, 'has_more': False}
|
||||
|
||||
return app_pagination
|
||||
return marshal(app_pagination, app_pagination_fields)
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
|
|
|
@ -48,11 +48,14 @@ class DatasetListApi(Resource):
|
|||
limit = request.args.get('limit', default=20, type=int)
|
||||
ids = request.args.getlist('ids')
|
||||
provider = request.args.get('provider', default="vendor")
|
||||
search = request.args.get('keyword', default=None, type=str)
|
||||
tag_ids = request.args.getlist('tag_ids')
|
||||
|
||||
if ids:
|
||||
datasets, total = DatasetService.get_datasets_by_ids(ids, current_user.current_tenant_id)
|
||||
else:
|
||||
datasets, total = DatasetService.get_datasets(page, limit, provider,
|
||||
current_user.current_tenant_id, current_user)
|
||||
current_user.current_tenant_id, current_user, search, tag_ids)
|
||||
|
||||
# check embedding setting
|
||||
provider_manager = ProviderManager()
|
||||
|
@ -184,6 +187,10 @@ class DatasetApi(Resource):
|
|||
help='Invalid indexing technique.')
|
||||
parser.add_argument('permission', type=str, location='json', choices=(
|
||||
'only_me', 'all_team_members'), help='Invalid permission.')
|
||||
parser.add_argument('embedding_model', type=str,
|
||||
location='json', help='Invalid embedding model.')
|
||||
parser.add_argument('embedding_model_provider', type=str,
|
||||
location='json', help='Invalid embedding model provider.')
|
||||
parser.add_argument('retrieval_model', type=dict, location='json', help='Invalid retrieval model.')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
@ -506,10 +513,27 @@ class DatasetRetrievalSettingMockApi(Resource):
|
|||
else:
|
||||
raise ValueError("Unsupported vector db type.")
|
||||
|
||||
class DatasetErrorDocs(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
results = DocumentService.get_error_documents_by_dataset_id(dataset_id_str)
|
||||
|
||||
return {
|
||||
'data': [marshal(item, document_status_fields) for item in results],
|
||||
'total': len(results)
|
||||
}, 200
|
||||
|
||||
|
||||
api.add_resource(DatasetListApi, '/datasets')
|
||||
api.add_resource(DatasetApi, '/datasets/<uuid:dataset_id>')
|
||||
api.add_resource(DatasetQueryApi, '/datasets/<uuid:dataset_id>/queries')
|
||||
api.add_resource(DatasetErrorDocs, '/datasets/<uuid:dataset_id>/error-docs')
|
||||
api.add_resource(DatasetIndexingEstimateApi, '/datasets/indexing-estimate')
|
||||
api.add_resource(DatasetRelatedAppListApi, '/datasets/<uuid:dataset_id>/related-apps')
|
||||
api.add_resource(DatasetIndexingStatusApi, '/datasets/<uuid:dataset_id>/indexing-status')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import request
|
||||
|
@ -233,7 +234,7 @@ class DatasetDocumentListApi(Resource):
|
|||
location='json')
|
||||
parser.add_argument('data_source', type=dict, required=False, location='json')
|
||||
parser.add_argument('process_rule', type=dict, required=False, location='json')
|
||||
parser.add_argument('duplicate', type=bool, nullable=False, location='json')
|
||||
parser.add_argument('duplicate', type=bool, default=True, nullable=False, location='json')
|
||||
parser.add_argument('original_document_id', type=str, required=False, location='json')
|
||||
parser.add_argument('doc_form', type=str, default='text_model', required=False, nullable=False, location='json')
|
||||
parser.add_argument('doc_language', type=str, default='English', required=False, nullable=False,
|
||||
|
@ -883,6 +884,49 @@ class DocumentRecoverApi(DocumentResource):
|
|||
return {'result': 'success'}, 204
|
||||
|
||||
|
||||
class DocumentRetryApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self, dataset_id):
|
||||
"""retry document."""
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('document_ids', type=list, required=True, nullable=False,
|
||||
location='json')
|
||||
args = parser.parse_args()
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
retry_documents = []
|
||||
if not dataset:
|
||||
raise NotFound('Dataset not found.')
|
||||
for document_id in args['document_ids']:
|
||||
try:
|
||||
document_id = str(document_id)
|
||||
|
||||
document = DocumentService.get_document(dataset.id, document_id)
|
||||
|
||||
# 404 if document not found
|
||||
if document is None:
|
||||
raise NotFound("Document Not Exists.")
|
||||
|
||||
# 403 if document is archived
|
||||
if DocumentService.check_archived(document):
|
||||
raise ArchivedDocumentImmutableError()
|
||||
|
||||
# 400 if document is completed
|
||||
if document.indexing_status == 'completed':
|
||||
raise DocumentAlreadyFinishedError()
|
||||
retry_documents.append(document)
|
||||
except Exception as e:
|
||||
logging.error(f"Document {document_id} retry failed: {str(e)}")
|
||||
continue
|
||||
# retry document
|
||||
DocumentService.retry_document(dataset_id, retry_documents)
|
||||
|
||||
return {'result': 'success'}, 204
|
||||
|
||||
|
||||
api.add_resource(GetProcessRuleApi, '/datasets/process-rule')
|
||||
api.add_resource(DatasetDocumentListApi,
|
||||
'/datasets/<uuid:dataset_id>/documents')
|
||||
|
@ -908,3 +952,4 @@ api.add_resource(DocumentStatusApi,
|
|||
'/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/status/<string:action>')
|
||||
api.add_resource(DocumentPauseApi, '/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause')
|
||||
api.add_resource(DocumentRecoverApi, '/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume')
|
||||
api.add_resource(DocumentRetryApi, '/datasets/<uuid:dataset_id>/retry')
|
||||
|
|
159
api/controllers/console/tag/tags.py
Normal file
159
api/controllers/console/tag/tags.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
from flask import request
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from fields.tag_fields import tag_fields
|
||||
from libs.login import login_required
|
||||
from models.model import Tag
|
||||
from services.tag_service import TagService
|
||||
|
||||
|
||||
def _validate_name(name):
|
||||
if not name or len(name) < 1 or len(name) > 40:
|
||||
raise ValueError('Name must be between 1 to 50 characters.')
|
||||
return name
|
||||
|
||||
|
||||
class TagListApi(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(tag_fields)
|
||||
def get(self):
|
||||
tag_type = request.args.get('type', type=str)
|
||||
keyword = request.args.get('keyword', default=None, type=str)
|
||||
tags = TagService.get_tags(tag_type, current_user.current_tenant_id, keyword)
|
||||
|
||||
return tags, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('name', nullable=False, required=True,
|
||||
help='Name must be between 1 to 50 characters.',
|
||||
type=_validate_name)
|
||||
parser.add_argument('type', type=str, location='json',
|
||||
choices=Tag.TAG_TYPE_LIST,
|
||||
nullable=True,
|
||||
help='Invalid tag type.')
|
||||
args = parser.parse_args()
|
||||
tag = TagService.save_tags(args)
|
||||
|
||||
response = {
|
||||
'id': tag.id,
|
||||
'name': tag.name,
|
||||
'type': tag.type,
|
||||
'binding_count': 0
|
||||
}
|
||||
|
||||
return response, 200
|
||||
|
||||
|
||||
class TagUpdateDeleteApi(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def patch(self, tag_id):
|
||||
tag_id = str(tag_id)
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('name', nullable=False, required=True,
|
||||
help='Name must be between 1 to 50 characters.',
|
||||
type=_validate_name)
|
||||
args = parser.parse_args()
|
||||
tag = TagService.update_tags(args, tag_id)
|
||||
|
||||
binding_count = TagService.get_tag_binding_count(tag_id)
|
||||
|
||||
response = {
|
||||
'id': tag.id,
|
||||
'name': tag.name,
|
||||
'type': tag.type,
|
||||
'binding_count': binding_count
|
||||
}
|
||||
|
||||
return response, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def delete(self, tag_id):
|
||||
tag_id = str(tag_id)
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
TagService.delete_tag(tag_id)
|
||||
|
||||
return 200
|
||||
|
||||
|
||||
class TagBindingCreateApi(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('tag_ids', type=list, nullable=False, required=True, location='json',
|
||||
help='Tag IDs is required.')
|
||||
parser.add_argument('target_id', type=str, nullable=False, required=True, location='json',
|
||||
help='Target ID is required.')
|
||||
parser.add_argument('type', type=str, location='json',
|
||||
choices=Tag.TAG_TYPE_LIST,
|
||||
nullable=True,
|
||||
help='Invalid tag type.')
|
||||
args = parser.parse_args()
|
||||
TagService.save_tag_binding(args)
|
||||
|
||||
return 200
|
||||
|
||||
|
||||
class TagBindingDeleteApi(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if not current_user.is_admin_or_owner:
|
||||
raise Forbidden()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('tag_id', type=str, nullable=False, required=True,
|
||||
help='Tag ID is required.')
|
||||
parser.add_argument('target_id', type=str, nullable=False, required=True,
|
||||
help='Target ID is required.')
|
||||
parser.add_argument('type', type=str, location='json',
|
||||
choices=Tag.TAG_TYPE_LIST,
|
||||
nullable=True,
|
||||
help='Invalid tag type.')
|
||||
args = parser.parse_args()
|
||||
TagService.delete_tag_binding(args)
|
||||
|
||||
return 200
|
||||
|
||||
|
||||
api.add_resource(TagListApi, '/tags')
|
||||
api.add_resource(TagUpdateDeleteApi, '/tags/<uuid:tag_id>')
|
||||
api.add_resource(TagBindingCreateApi, '/tag-bindings/create')
|
||||
api.add_resource(TagBindingDeleteApi, '/tag-bindings/remove')
|
|
@ -26,8 +26,11 @@ class DatasetApi(DatasetApiResource):
|
|||
page = request.args.get('page', default=1, type=int)
|
||||
limit = request.args.get('limit', default=20, type=int)
|
||||
provider = request.args.get('provider', default="vendor")
|
||||
search = request.args.get('keyword', default=None, type=str)
|
||||
tag_ids = request.args.getlist('tag_ids')
|
||||
|
||||
datasets, total = DatasetService.get_datasets(page, limit, provider,
|
||||
tenant_id, current_user)
|
||||
tenant_id, current_user, search, tag_ids)
|
||||
# check embedding setting
|
||||
provider_manager = ProviderManager()
|
||||
configurations = provider_manager.get_configurations(
|
||||
|
|
|
@ -110,19 +110,37 @@ class MilvusVector(BaseVector):
|
|||
return None
|
||||
|
||||
def delete_by_metadata_field(self, key: str, value: str):
|
||||
alias = uuid4().hex
|
||||
if self._client_config.secure:
|
||||
uri = "https://" + str(self._client_config.host) + ":" + str(self._client_config.port)
|
||||
else:
|
||||
uri = "http://" + str(self._client_config.host) + ":" + str(self._client_config.port)
|
||||
connections.connect(alias=alias, uri=uri, user=self._client_config.user, password=self._client_config.password)
|
||||
|
||||
ids = self.get_ids_by_metadata_field(key, value)
|
||||
if ids:
|
||||
self._client.delete(collection_name=self._collection_name, pks=ids)
|
||||
from pymilvus import utility
|
||||
if utility.has_collection(self._collection_name, using=alias):
|
||||
|
||||
ids = self.get_ids_by_metadata_field(key, value)
|
||||
if ids:
|
||||
self._client.delete(collection_name=self._collection_name, pks=ids)
|
||||
|
||||
def delete_by_ids(self, doc_ids: list[str]) -> None:
|
||||
alias = uuid4().hex
|
||||
if self._client_config.secure:
|
||||
uri = "https://" + str(self._client_config.host) + ":" + str(self._client_config.port)
|
||||
else:
|
||||
uri = "http://" + str(self._client_config.host) + ":" + str(self._client_config.port)
|
||||
connections.connect(alias=alias, uri=uri, user=self._client_config.user, password=self._client_config.password)
|
||||
|
||||
result = self._client.query(collection_name=self._collection_name,
|
||||
filter=f'metadata["doc_id"] in {doc_ids}',
|
||||
output_fields=["id"])
|
||||
if result:
|
||||
ids = [item["id"] for item in result]
|
||||
self._client.delete(collection_name=self._collection_name, pks=ids)
|
||||
from pymilvus import utility
|
||||
if utility.has_collection(self._collection_name, using=alias):
|
||||
|
||||
result = self._client.query(collection_name=self._collection_name,
|
||||
filter=f'metadata["doc_id"] in {doc_ids}',
|
||||
output_fields=["id"])
|
||||
if result:
|
||||
ids = [item["id"] for item in result]
|
||||
self._client.delete(collection_name=self._collection_name, pks=ids)
|
||||
|
||||
def delete(self) -> None:
|
||||
alias = uuid4().hex
|
||||
|
|
|
@ -217,29 +217,38 @@ class QdrantVector(BaseVector):
|
|||
def delete_by_metadata_field(self, key: str, value: str):
|
||||
|
||||
from qdrant_client.http import models
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
|
||||
filter = models.Filter(
|
||||
must=[
|
||||
models.FieldCondition(
|
||||
key=f"metadata.{key}",
|
||||
match=models.MatchValue(value=value),
|
||||
try:
|
||||
filter = models.Filter(
|
||||
must=[
|
||||
models.FieldCondition(
|
||||
key=f"metadata.{key}",
|
||||
match=models.MatchValue(value=value),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
self._reload_if_needed()
|
||||
|
||||
self._client.delete(
|
||||
collection_name=self._collection_name,
|
||||
points_selector=FilterSelector(
|
||||
filter=filter
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
self._reload_if_needed()
|
||||
|
||||
self._client.delete(
|
||||
collection_name=self._collection_name,
|
||||
points_selector=FilterSelector(
|
||||
filter=filter
|
||||
),
|
||||
)
|
||||
)
|
||||
except UnexpectedResponse as e:
|
||||
# Collection does not exist, so return
|
||||
if e.status_code == 404:
|
||||
return
|
||||
# Some other error occurred, so re-raise the exception
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete(self):
|
||||
from qdrant_client.http import models
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
|
||||
|
||||
try:
|
||||
filter = models.Filter(
|
||||
must=[
|
||||
|
@ -257,29 +266,40 @@ class QdrantVector(BaseVector):
|
|||
)
|
||||
except UnexpectedResponse as e:
|
||||
# Collection does not exist, so return
|
||||
if e.status_code == 404:
|
||||
if e.status_code == 404:
|
||||
return
|
||||
# Some other error occurred, so re-raise the exception
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete_by_ids(self, ids: list[str]) -> None:
|
||||
|
||||
from qdrant_client.http import models
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
|
||||
for node_id in ids:
|
||||
filter = models.Filter(
|
||||
must=[
|
||||
models.FieldCondition(
|
||||
key="metadata.doc_id",
|
||||
match=models.MatchValue(value=node_id),
|
||||
try:
|
||||
filter = models.Filter(
|
||||
must=[
|
||||
models.FieldCondition(
|
||||
key="metadata.doc_id",
|
||||
match=models.MatchValue(value=node_id),
|
||||
),
|
||||
],
|
||||
)
|
||||
self._client.delete(
|
||||
collection_name=self._collection_name,
|
||||
points_selector=FilterSelector(
|
||||
filter=filter
|
||||
),
|
||||
],
|
||||
)
|
||||
self._client.delete(
|
||||
collection_name=self._collection_name,
|
||||
points_selector=FilterSelector(
|
||||
filter=filter
|
||||
),
|
||||
)
|
||||
)
|
||||
except UnexpectedResponse as e:
|
||||
# Collection does not exist, so return
|
||||
if e.status_code == 404:
|
||||
return
|
||||
# Some other error occurred, so re-raise the exception
|
||||
else:
|
||||
raise e
|
||||
|
||||
def text_exists(self, id: str) -> bool:
|
||||
all_collection_name = []
|
||||
|
|
|
@ -121,18 +121,20 @@ class WeaviateVector(BaseVector):
|
|||
return ids
|
||||
|
||||
def delete_by_metadata_field(self, key: str, value: str):
|
||||
# check whether the index already exists
|
||||
schema = self._default_schema(self._collection_name)
|
||||
if self._client.schema.contains(schema):
|
||||
where_filter = {
|
||||
"operator": "Equal",
|
||||
"path": [key],
|
||||
"valueText": value
|
||||
}
|
||||
|
||||
where_filter = {
|
||||
"operator": "Equal",
|
||||
"path": [key],
|
||||
"valueText": value
|
||||
}
|
||||
|
||||
self._client.batch.delete_objects(
|
||||
class_name=self._collection_name,
|
||||
where=where_filter,
|
||||
output='minimal'
|
||||
)
|
||||
self._client.batch.delete_objects(
|
||||
class_name=self._collection_name,
|
||||
where=where_filter,
|
||||
output='minimal'
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
# check whether the index already exists
|
||||
|
@ -163,11 +165,14 @@ class WeaviateVector(BaseVector):
|
|||
return True
|
||||
|
||||
def delete_by_ids(self, ids: list[str]) -> None:
|
||||
for uuid in ids:
|
||||
self._client.data_object.delete(
|
||||
class_name=self._collection_name,
|
||||
uuid=uuid,
|
||||
)
|
||||
# check whether the index already exists
|
||||
schema = self._default_schema(self._collection_name)
|
||||
if self._client.schema.contains(schema):
|
||||
for uuid in ids:
|
||||
self._client.data_object.delete(
|
||||
class_name=self._collection_name,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||
"""Look up similar documents by embedding vector in Weaviate."""
|
||||
|
|
|
@ -62,6 +62,12 @@ model_config_partial_fields = {
|
|||
'pre_prompt': fields.String,
|
||||
}
|
||||
|
||||
tag_fields = {
|
||||
'id': fields.String,
|
||||
'name': fields.String,
|
||||
'type': fields.String
|
||||
}
|
||||
|
||||
app_partial_fields = {
|
||||
'id': fields.String,
|
||||
'name': fields.String,
|
||||
|
@ -70,9 +76,11 @@ app_partial_fields = {
|
|||
'icon': fields.String,
|
||||
'icon_background': fields.String,
|
||||
'model_config': fields.Nested(model_config_partial_fields, attribute='app_model_config', allow_null=True),
|
||||
'created_at': TimestampField
|
||||
'created_at': TimestampField,
|
||||
'tags': fields.List(fields.Nested(tag_fields))
|
||||
}
|
||||
|
||||
|
||||
app_pagination_fields = {
|
||||
'page': fields.Integer,
|
||||
'limit': fields.Integer(attribute='per_page'),
|
||||
|
|
|
@ -27,6 +27,11 @@ dataset_retrieval_model_fields = {
|
|||
'score_threshold': fields.Float
|
||||
}
|
||||
|
||||
tag_fields = {
|
||||
'id': fields.String,
|
||||
'name': fields.String,
|
||||
'type': fields.String
|
||||
}
|
||||
|
||||
dataset_detail_fields = {
|
||||
'id': fields.String,
|
||||
|
@ -46,7 +51,8 @@ dataset_detail_fields = {
|
|||
'embedding_model': fields.String,
|
||||
'embedding_model_provider': fields.String,
|
||||
'embedding_available': fields.Boolean,
|
||||
'retrieval_model_dict': fields.Nested(dataset_retrieval_model_fields)
|
||||
'retrieval_model_dict': fields.Nested(dataset_retrieval_model_fields),
|
||||
'tags': fields.List(fields.Nested(tag_fields))
|
||||
}
|
||||
|
||||
dataset_query_detail_fields = {
|
||||
|
|
8
api/fields/tag_fields.py
Normal file
8
api/fields/tag_fields.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from flask_restful import fields
|
||||
|
||||
tag_fields = {
|
||||
'id': fields.String,
|
||||
'name': fields.String,
|
||||
'type': fields.String,
|
||||
'binding_count': fields.String
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
"""add-tags-and-binding-table
|
||||
|
||||
Revision ID: 3c7cac9521c6
|
||||
Revises: c3311b089690
|
||||
Create Date: 2024-04-11 06:17:34.278594
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3c7cac9521c6'
|
||||
down_revision = 'c3311b089690'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tag_bindings',
|
||||
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('tenant_id', postgresql.UUID(), nullable=True),
|
||||
sa.Column('tag_id', postgresql.UUID(), nullable=True),
|
||||
sa.Column('target_id', postgresql.UUID(), nullable=True),
|
||||
sa.Column('created_by', postgresql.UUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='tag_binding_pkey')
|
||||
)
|
||||
with op.batch_alter_table('tag_bindings', schema=None) as batch_op:
|
||||
batch_op.create_index('tag_bind_tag_id_idx', ['tag_id'], unique=False)
|
||||
batch_op.create_index('tag_bind_target_id_idx', ['target_id'], unique=False)
|
||||
|
||||
op.create_table('tags',
|
||||
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('tenant_id', postgresql.UUID(), nullable=True),
|
||||
sa.Column('type', sa.String(length=16), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('created_by', postgresql.UUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='tag_pkey')
|
||||
)
|
||||
with op.batch_alter_table('tags', schema=None) as batch_op:
|
||||
batch_op.create_index('tag_name_idx', ['name'], unique=False)
|
||||
batch_op.create_index('tag_type_idx', ['type'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tags', schema=None) as batch_op:
|
||||
batch_op.drop_index('tag_type_idx')
|
||||
batch_op.drop_index('tag_name_idx')
|
||||
|
||||
op.drop_table('tags')
|
||||
with op.batch_alter_table('tag_bindings', schema=None) as batch_op:
|
||||
batch_op.drop_index('tag_bind_target_id_idx')
|
||||
batch_op.drop_index('tag_bind_tag_id_idx')
|
||||
|
||||
op.drop_table('tag_bindings')
|
||||
# ### end Alembic commands ###
|
|
@ -9,7 +9,7 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|||
from extensions.ext_database import db
|
||||
from extensions.ext_storage import storage
|
||||
from models.account import Account
|
||||
from models.model import App, UploadFile
|
||||
from models.model import App, Tag, TagBinding, UploadFile
|
||||
|
||||
|
||||
class Dataset(db.Model):
|
||||
|
@ -118,6 +118,20 @@ class Dataset(db.Model):
|
|||
}
|
||||
return self.retrieval_model if self.retrieval_model else default_retrieval_model
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
tags = db.session.query(Tag).join(
|
||||
TagBinding,
|
||||
Tag.id == TagBinding.tag_id
|
||||
).filter(
|
||||
TagBinding.target_id == self.id,
|
||||
TagBinding.tenant_id == self.tenant_id,
|
||||
Tag.tenant_id == self.tenant_id,
|
||||
Tag.type == 'knowledge'
|
||||
).all()
|
||||
|
||||
return tags if tags else []
|
||||
|
||||
@staticmethod
|
||||
def gen_collection_name_by_id(dataset_id: str) -> str:
|
||||
normalized_dataset_id = dataset_id.replace("-", "_")
|
||||
|
|
|
@ -148,7 +148,7 @@ class App(db.Model):
|
|||
return []
|
||||
agent_mode = app_model_config.agent_mode_dict
|
||||
tools = agent_mode.get('tools', [])
|
||||
|
||||
|
||||
provider_ids = []
|
||||
|
||||
for tool in tools:
|
||||
|
@ -185,6 +185,20 @@ class App(db.Model):
|
|||
|
||||
return deleted_tools
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
tags = db.session.query(Tag).join(
|
||||
TagBinding,
|
||||
Tag.id == TagBinding.tag_id
|
||||
).filter(
|
||||
TagBinding.target_id == self.id,
|
||||
TagBinding.tenant_id == self.tenant_id,
|
||||
Tag.tenant_id == self.tenant_id,
|
||||
Tag.type == 'app'
|
||||
).all()
|
||||
|
||||
return tags if tags else []
|
||||
|
||||
|
||||
class AppModelConfig(db.Model):
|
||||
__tablename__ = 'app_model_configs'
|
||||
|
@ -292,7 +306,8 @@ class AppModelConfig(db.Model):
|
|||
|
||||
@property
|
||||
def agent_mode_dict(self) -> dict:
|
||||
return json.loads(self.agent_mode) if self.agent_mode else {"enabled": False, "strategy": None, "tools": [], "prompt": None}
|
||||
return json.loads(self.agent_mode) if self.agent_mode else {"enabled": False, "strategy": None, "tools": [],
|
||||
"prompt": None}
|
||||
|
||||
@property
|
||||
def chat_prompt_config_dict(self) -> dict:
|
||||
|
@ -463,6 +478,7 @@ class InstalledApp(db.Model):
|
|||
return tenant
|
||||
|
||||
|
||||
|
||||
class Conversation(db.Model):
|
||||
__tablename__ = 'conversations'
|
||||
__table_args__ = (
|
||||
|
@ -1175,11 +1191,11 @@ class MessageAgentThought(db.Model):
|
|||
return json.loads(self.message_files)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@property
|
||||
def tools(self) -> list[str]:
|
||||
return self.tool.split(";") if self.tool else []
|
||||
|
||||
|
||||
@property
|
||||
def tool_labels(self) -> dict:
|
||||
try:
|
||||
|
@ -1189,7 +1205,7 @@ class MessageAgentThought(db.Model):
|
|||
return {}
|
||||
except Exception as e:
|
||||
return {}
|
||||
|
||||
|
||||
@property
|
||||
def tool_meta(self) -> dict:
|
||||
try:
|
||||
|
@ -1199,7 +1215,7 @@ class MessageAgentThought(db.Model):
|
|||
return {}
|
||||
except Exception as e:
|
||||
return {}
|
||||
|
||||
|
||||
@property
|
||||
def tool_inputs_dict(self) -> dict:
|
||||
tools = self.tools
|
||||
|
@ -1222,7 +1238,7 @@ class MessageAgentThought(db.Model):
|
|||
}
|
||||
except Exception as e:
|
||||
return {}
|
||||
|
||||
|
||||
@property
|
||||
def tool_outputs_dict(self) -> dict:
|
||||
tools = self.tools
|
||||
|
@ -1249,6 +1265,7 @@ class MessageAgentThought(db.Model):
|
|||
tool: self.observation for tool in tools
|
||||
}
|
||||
|
||||
|
||||
class DatasetRetrieverResource(db.Model):
|
||||
__tablename__ = 'dataset_retriever_resources'
|
||||
__table_args__ = (
|
||||
|
@ -1274,3 +1291,37 @@ class DatasetRetrieverResource(db.Model):
|
|||
retriever_from = db.Column(db.Text, nullable=False)
|
||||
created_by = db.Column(UUID, nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
|
||||
|
||||
|
||||
class Tag(db.Model):
|
||||
__tablename__ = 'tags'
|
||||
__table_args__ = (
|
||||
db.PrimaryKeyConstraint('id', name='tag_pkey'),
|
||||
db.Index('tag_type_idx', 'type'),
|
||||
db.Index('tag_name_idx', 'name'),
|
||||
)
|
||||
|
||||
TAG_TYPE_LIST = ['knowledge', 'app']
|
||||
|
||||
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
|
||||
tenant_id = db.Column(UUID, nullable=True)
|
||||
type = db.Column(db.String(16), nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
created_by = db.Column(UUID, nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||
|
||||
|
||||
class TagBinding(db.Model):
|
||||
__tablename__ = 'tag_bindings'
|
||||
__table_args__ = (
|
||||
db.PrimaryKeyConstraint('id', name='tag_binding_pkey'),
|
||||
db.Index('tag_bind_target_id_idx', 'target_id'),
|
||||
db.Index('tag_bind_tag_id_idx', 'tag_id'),
|
||||
)
|
||||
|
||||
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
|
||||
tenant_id = db.Column(UUID, nullable=True)
|
||||
tag_id = db.Column(UUID, nullable=True)
|
||||
target_id = db.Column(UUID, nullable=True)
|
||||
created_by = db.Column(UUID, nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||
|
|
|
@ -21,11 +21,12 @@ from extensions.ext_database import db
|
|||
from models.account import Account
|
||||
from models.model import App, AppMode, AppModelConfig
|
||||
from models.tools import ApiToolProvider
|
||||
from services.tag_service import TagService
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
|
||||
class AppService:
|
||||
def get_paginate_apps(self, tenant_id: str, args: dict) -> Pagination:
|
||||
def get_paginate_apps(self, tenant_id: str, args: dict) -> Pagination | None:
|
||||
"""
|
||||
Get app list with pagination
|
||||
:param tenant_id: tenant id
|
||||
|
@ -49,6 +50,14 @@ class AppService:
|
|||
if 'name' in args and args['name']:
|
||||
name = args['name'][:30]
|
||||
filters.append(App.name.ilike(f'%{name}%'))
|
||||
if 'tag_ids' in args and args['tag_ids']:
|
||||
target_ids = TagService.get_target_ids_by_tag_ids('app',
|
||||
tenant_id,
|
||||
args['tag_ids'])
|
||||
if target_ids:
|
||||
filters.append(App.id.in_(target_ids))
|
||||
else:
|
||||
return None
|
||||
|
||||
app_models = db.paginate(
|
||||
db.select(App).where(*filters).order_by(App.created_at.desc()),
|
||||
|
|
|
@ -38,28 +38,39 @@ from services.errors.dataset import DatasetNameDuplicateError
|
|||
from services.errors.document import DocumentIndexingError
|
||||
from services.errors.file import FileNotExistsError
|
||||
from services.feature_service import FeatureModel, FeatureService
|
||||
from services.tag_service import TagService
|
||||
from services.vector_service import VectorService
|
||||
from tasks.clean_notion_document_task import clean_notion_document_task
|
||||
from tasks.deal_dataset_vector_index_task import deal_dataset_vector_index_task
|
||||
from tasks.delete_segment_from_index_task import delete_segment_from_index_task
|
||||
from tasks.document_indexing_task import document_indexing_task
|
||||
from tasks.document_indexing_update_task import document_indexing_update_task
|
||||
from tasks.duplicate_document_indexing_task import duplicate_document_indexing_task
|
||||
from tasks.recover_document_indexing_task import recover_document_indexing_task
|
||||
from tasks.retry_document_indexing_task import retry_document_indexing_task
|
||||
|
||||
|
||||
class DatasetService:
|
||||
|
||||
@staticmethod
|
||||
def get_datasets(page, per_page, provider="vendor", tenant_id=None, user=None):
|
||||
def get_datasets(page, per_page, provider="vendor", tenant_id=None, user=None, search=None, tag_ids=None):
|
||||
if user:
|
||||
permission_filter = db.or_(Dataset.created_by == user.id,
|
||||
Dataset.permission == 'all_team_members')
|
||||
else:
|
||||
permission_filter = Dataset.permission == 'all_team_members'
|
||||
datasets = Dataset.query.filter(
|
||||
query = Dataset.query.filter(
|
||||
db.and_(Dataset.provider == provider, Dataset.tenant_id == tenant_id, permission_filter)) \
|
||||
.order_by(Dataset.created_at.desc()) \
|
||||
.paginate(
|
||||
.order_by(Dataset.created_at.desc())
|
||||
if search:
|
||||
query = query.filter(db.and_(Dataset.name.ilike(f'%{search}%')))
|
||||
if tag_ids:
|
||||
target_ids = TagService.get_target_ids_by_tag_ids('knowledge', tenant_id, tag_ids)
|
||||
if target_ids:
|
||||
query = query.filter(db.and_(Dataset.id.in_(target_ids)))
|
||||
else:
|
||||
return [], 0
|
||||
datasets = query.paginate(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
max_per_page=100,
|
||||
|
@ -165,9 +176,36 @@ class DatasetService:
|
|||
# get embedding model setting
|
||||
try:
|
||||
model_manager = ModelManager()
|
||||
embedding_model = model_manager.get_default_model_instance(
|
||||
embedding_model = model_manager.get_model_instance(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
model_type=ModelType.TEXT_EMBEDDING
|
||||
provider=data['embedding_model_provider'],
|
||||
model_type=ModelType.TEXT_EMBEDDING,
|
||||
model=data['embedding_model']
|
||||
)
|
||||
filtered_data['embedding_model'] = embedding_model.model
|
||||
filtered_data['embedding_model_provider'] = embedding_model.provider
|
||||
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding(
|
||||
embedding_model.provider,
|
||||
embedding_model.model
|
||||
)
|
||||
filtered_data['collection_binding_id'] = dataset_collection_binding.id
|
||||
except LLMBadRequestError:
|
||||
raise ValueError(
|
||||
"No Embedding Model available. Please configure a valid provider "
|
||||
"in the Settings -> Model Provider.")
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ValueError(ex.description)
|
||||
else:
|
||||
if data['embedding_model_provider'] != dataset.embedding_model_provider or \
|
||||
data['embedding_model'] != dataset.embedding_model:
|
||||
action = 'update'
|
||||
try:
|
||||
model_manager = ModelManager()
|
||||
embedding_model = model_manager.get_model_instance(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
provider=data['embedding_model_provider'],
|
||||
model_type=ModelType.TEXT_EMBEDDING,
|
||||
model=data['embedding_model']
|
||||
)
|
||||
filtered_data['embedding_model'] = embedding_model.model
|
||||
filtered_data['embedding_model_provider'] = embedding_model.provider
|
||||
|
@ -376,6 +414,15 @@ class DocumentService:
|
|||
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def get_error_documents_by_dataset_id(dataset_id: str) -> list[Document]:
|
||||
documents = db.session.query(Document).filter(
|
||||
Document.dataset_id == dataset_id,
|
||||
Document.indexing_status == 'error' or Document.indexing_status == 'paused'
|
||||
).all()
|
||||
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def get_batch_documents(dataset_id: str, batch: str) -> list[Document]:
|
||||
documents = db.session.query(Document).filter(
|
||||
|
@ -440,6 +487,20 @@ class DocumentService:
|
|||
# trigger async task
|
||||
recover_document_indexing_task.delay(document.dataset_id, document.id)
|
||||
|
||||
@staticmethod
|
||||
def retry_document(dataset_id: str, documents: list[Document]):
|
||||
for document in documents:
|
||||
# retry document indexing
|
||||
document.indexing_status = 'waiting'
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
# add retry flag
|
||||
retry_indexing_cache_key = 'document_{}_is_retried'.format(document.id)
|
||||
redis_client.setex(retry_indexing_cache_key, 600, 1)
|
||||
# trigger async task
|
||||
document_ids = [document.id for document in documents]
|
||||
retry_document_indexing_task.delay(dataset_id, document_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_documents_position(dataset_id):
|
||||
document = Document.query.filter_by(dataset_id=dataset_id).order_by(Document.position.desc()).first()
|
||||
|
@ -537,6 +598,7 @@ class DocumentService:
|
|||
db.session.commit()
|
||||
position = DocumentService.get_documents_position(dataset.id)
|
||||
document_ids = []
|
||||
duplicate_document_ids = []
|
||||
if document_data["data_source"]["type"] == "upload_file":
|
||||
upload_file_list = document_data["data_source"]["info_list"]['file_info_list']['file_ids']
|
||||
for file_id in upload_file_list:
|
||||
|
@ -553,6 +615,28 @@ class DocumentService:
|
|||
data_source_info = {
|
||||
"upload_file_id": file_id,
|
||||
}
|
||||
# check duplicate
|
||||
if document_data.get('duplicate', False):
|
||||
document = Document.query.filter_by(
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
data_source_type='upload_file',
|
||||
enabled=True,
|
||||
name=file_name
|
||||
).first()
|
||||
if document:
|
||||
document.dataset_process_rule_id = dataset_process_rule.id
|
||||
document.updated_at = datetime.datetime.utcnow()
|
||||
document.created_from = created_from
|
||||
document.doc_form = document_data['doc_form']
|
||||
document.doc_language = document_data['doc_language']
|
||||
document.data_source_info = json.dumps(data_source_info)
|
||||
document.batch = batch
|
||||
document.indexing_status = 'waiting'
|
||||
db.session.add(document)
|
||||
documents.append(document)
|
||||
duplicate_document_ids.append(document.id)
|
||||
continue
|
||||
document = DocumentService.build_document(dataset, dataset_process_rule.id,
|
||||
document_data["data_source"]["type"],
|
||||
document_data["doc_form"],
|
||||
|
@ -618,7 +702,10 @@ class DocumentService:
|
|||
db.session.commit()
|
||||
|
||||
# trigger async task
|
||||
document_indexing_task.delay(dataset.id, document_ids)
|
||||
if document_ids:
|
||||
document_indexing_task.delay(dataset.id, document_ids)
|
||||
if duplicate_document_ids:
|
||||
duplicate_document_indexing_task.delay(dataset.id, duplicate_document_ids)
|
||||
|
||||
return documents, batch
|
||||
|
||||
|
@ -626,7 +713,8 @@ class DocumentService:
|
|||
def check_documents_upload_quota(count: int, features: FeatureModel):
|
||||
can_upload_size = features.documents_upload_quota.limit - features.documents_upload_quota.size
|
||||
if count > can_upload_size:
|
||||
raise ValueError(f'You have reached the limit of your subscription. Only {can_upload_size} documents can be uploaded.')
|
||||
raise ValueError(
|
||||
f'You have reached the limit of your subscription. Only {can_upload_size} documents can be uploaded.')
|
||||
|
||||
@staticmethod
|
||||
def build_document(dataset: Dataset, process_rule_id: str, data_source_type: str, document_form: str,
|
||||
|
@ -752,7 +840,6 @@ class DocumentService:
|
|||
db.session.commit()
|
||||
# trigger async task
|
||||
document_indexing_update_task.delay(document.dataset_id, document.id)
|
||||
|
||||
return document
|
||||
|
||||
@staticmethod
|
||||
|
|
161
api/services/tag_service.py
Normal file
161
api/services/tag_service.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
import uuid
|
||||
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import Dataset
|
||||
from models.model import App, Tag, TagBinding
|
||||
|
||||
|
||||
class TagService:
|
||||
@staticmethod
|
||||
def get_tags(tag_type: str, current_tenant_id: str, keyword: str = None) -> list:
|
||||
query = db.session.query(
|
||||
Tag.id, Tag.type, Tag.name, func.count(TagBinding.id).label('binding_count')
|
||||
).outerjoin(
|
||||
TagBinding, Tag.id == TagBinding.tag_id
|
||||
).filter(
|
||||
Tag.type == tag_type,
|
||||
Tag.tenant_id == current_tenant_id
|
||||
)
|
||||
if keyword:
|
||||
query = query.filter(db.and_(Tag.name.ilike(f'%{keyword}%')))
|
||||
query = query.group_by(
|
||||
Tag.id
|
||||
)
|
||||
results = query.order_by(Tag.created_at.desc()).all()
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_target_ids_by_tag_ids(tag_type: str, current_tenant_id: str, tag_ids: list) -> list:
|
||||
tags = db.session.query(Tag).filter(
|
||||
Tag.id.in_(tag_ids),
|
||||
Tag.tenant_id == current_tenant_id,
|
||||
Tag.type == tag_type
|
||||
).all()
|
||||
if not tags:
|
||||
return []
|
||||
tag_ids = [tag.id for tag in tags]
|
||||
tag_bindings = db.session.query(
|
||||
TagBinding.target_id
|
||||
).filter(
|
||||
TagBinding.tag_id.in_(tag_ids),
|
||||
TagBinding.tenant_id == current_tenant_id
|
||||
).all()
|
||||
if not tag_bindings:
|
||||
return []
|
||||
results = [tag_binding.target_id for tag_binding in tag_bindings]
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_tags_by_target_id(tag_type: str, current_tenant_id: str, target_id: str) -> list:
|
||||
tags = db.session.query(Tag).join(
|
||||
TagBinding,
|
||||
Tag.id == TagBinding.tag_id
|
||||
).filter(
|
||||
TagBinding.target_id == target_id,
|
||||
TagBinding.tenant_id == current_tenant_id,
|
||||
Tag.tenant_id == current_tenant_id,
|
||||
Tag.type == tag_type
|
||||
).all()
|
||||
|
||||
return tags if tags else []
|
||||
|
||||
|
||||
@staticmethod
|
||||
def save_tags(args: dict) -> Tag:
|
||||
tag = Tag(
|
||||
id=str(uuid.uuid4()),
|
||||
name=args['name'],
|
||||
type=args['type'],
|
||||
created_by=current_user.id,
|
||||
tenant_id=current_user.current_tenant_id
|
||||
)
|
||||
db.session.add(tag)
|
||||
db.session.commit()
|
||||
return tag
|
||||
|
||||
@staticmethod
|
||||
def update_tags(args: dict, tag_id: str) -> Tag:
|
||||
tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
|
||||
if not tag:
|
||||
raise NotFound("Tag not found")
|
||||
tag.name = args['name']
|
||||
db.session.commit()
|
||||
return tag
|
||||
|
||||
@staticmethod
|
||||
def get_tag_binding_count(tag_id: str) -> int:
|
||||
count = db.session.query(TagBinding).filter(TagBinding.tag_id == tag_id).count()
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def delete_tag(tag_id: str):
|
||||
tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
|
||||
if not tag:
|
||||
raise NotFound("Tag not found")
|
||||
db.session.delete(tag)
|
||||
# delete tag binding
|
||||
tag_bindings = db.session.query(TagBinding).filter(TagBinding.tag_id == tag_id).all()
|
||||
if tag_bindings:
|
||||
for tag_binding in tag_bindings:
|
||||
db.session.delete(tag_binding)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def save_tag_binding(args):
|
||||
# check if target exists
|
||||
TagService.check_target_exists(args['type'], args['target_id'])
|
||||
# save tag binding
|
||||
for tag_id in args['tag_ids']:
|
||||
tag_binding = db.session.query(TagBinding).filter(
|
||||
TagBinding.tag_id == tag_id,
|
||||
TagBinding.target_id == args['target_id']
|
||||
).first()
|
||||
if tag_binding:
|
||||
continue
|
||||
new_tag_binding = TagBinding(
|
||||
tag_id=tag_id,
|
||||
target_id=args['target_id'],
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
created_by=current_user.id
|
||||
)
|
||||
db.session.add(new_tag_binding)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def delete_tag_binding(args):
|
||||
# check if target exists
|
||||
TagService.check_target_exists(args['type'], args['target_id'])
|
||||
# delete tag binding
|
||||
tag_bindings = db.session.query(TagBinding).filter(
|
||||
TagBinding.target_id == args['target_id'],
|
||||
TagBinding.tag_id == (args['tag_id'])
|
||||
).first()
|
||||
if tag_bindings:
|
||||
db.session.delete(tag_bindings)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def check_target_exists(type: str, target_id: str):
|
||||
if type == 'knowledge':
|
||||
dataset = db.session.query(Dataset).filter(
|
||||
Dataset.tenant_id == current_user.current_tenant_id,
|
||||
Dataset.id == target_id
|
||||
).first()
|
||||
if not dataset:
|
||||
raise NotFound("Dataset not found")
|
||||
elif type == 'app':
|
||||
app = db.session.query(App).filter(
|
||||
App.tenant_id == current_user.current_tenant_id,
|
||||
App.id == target_id
|
||||
).first()
|
||||
if not app:
|
||||
raise NotFound("App not found")
|
||||
else:
|
||||
raise NotFound("Invalid binding type")
|
||||
|
|
@ -16,6 +16,7 @@ from models.dataset import (
|
|||
)
|
||||
|
||||
|
||||
# Add import statement for ValueError
|
||||
@shared_task(queue='dataset')
|
||||
def clean_dataset_task(dataset_id: str, tenant_id: str, indexing_technique: str,
|
||||
index_struct: str, collection_binding_id: str, doc_form: str):
|
||||
|
@ -48,6 +49,9 @@ def clean_dataset_task(dataset_id: str, tenant_id: str, indexing_technique: str,
|
|||
logging.info(click.style('No documents found for dataset: {}'.format(dataset_id), fg='green'))
|
||||
else:
|
||||
logging.info(click.style('Cleaning documents for dataset: {}'.format(dataset_id), fg='green'))
|
||||
# Specify the index type before initializing the index processor
|
||||
if doc_form is None:
|
||||
raise ValueError("Index type must be specified.")
|
||||
index_processor = IndexProcessorFactory(doc_form).init_index_processor()
|
||||
index_processor.clean(dataset, None)
|
||||
|
||||
|
|
|
@ -64,6 +64,39 @@ def deal_dataset_vector_index_task(dataset_id: str, action: str):
|
|||
|
||||
# save vector index
|
||||
index_processor.load(dataset, documents, with_keywords=False)
|
||||
elif action == 'update':
|
||||
# clean index
|
||||
index_processor.clean(dataset, None, with_keywords=False)
|
||||
dataset_documents = db.session.query(DatasetDocument).filter(
|
||||
DatasetDocument.dataset_id == dataset_id,
|
||||
DatasetDocument.indexing_status == 'completed',
|
||||
DatasetDocument.enabled == True,
|
||||
DatasetDocument.archived == False,
|
||||
).all()
|
||||
# add new index
|
||||
if dataset_documents:
|
||||
documents = []
|
||||
for dataset_document in dataset_documents:
|
||||
# delete from vector index
|
||||
segments = db.session.query(DocumentSegment).filter(
|
||||
DocumentSegment.document_id == dataset_document.id,
|
||||
DocumentSegment.enabled == True
|
||||
).order_by(DocumentSegment.position.asc()).all()
|
||||
for segment in segments:
|
||||
document = Document(
|
||||
page_content=segment.content,
|
||||
metadata={
|
||||
"doc_id": segment.index_node_id,
|
||||
"doc_hash": segment.index_node_hash,
|
||||
"document_id": segment.document_id,
|
||||
"dataset_id": segment.dataset_id,
|
||||
}
|
||||
)
|
||||
|
||||
documents.append(document)
|
||||
|
||||
# save vector index
|
||||
index_processor.load(dataset, documents, with_keywords=False)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
|
94
api/tasks/duplicate_document_indexing_task.py
Normal file
94
api/tasks/duplicate_document_indexing_task.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
from flask import current_app
|
||||
|
||||
from core.indexing_runner import DocumentIsPausedException, IndexingRunner
|
||||
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import Dataset, Document, DocumentSegment
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@shared_task(queue='dataset')
|
||||
def duplicate_document_indexing_task(dataset_id: str, document_ids: list):
|
||||
"""
|
||||
Async process document
|
||||
:param dataset_id:
|
||||
:param document_ids:
|
||||
|
||||
Usage: duplicate_document_indexing_task.delay(dataset_id, document_id)
|
||||
"""
|
||||
documents = []
|
||||
start_at = time.perf_counter()
|
||||
|
||||
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
|
||||
|
||||
# check document limit
|
||||
features = FeatureService.get_features(dataset.tenant_id)
|
||||
try:
|
||||
if features.billing.enabled:
|
||||
vector_space = features.vector_space
|
||||
count = len(document_ids)
|
||||
batch_upload_limit = int(current_app.config['BATCH_UPLOAD_LIMIT'])
|
||||
if count > batch_upload_limit:
|
||||
raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.")
|
||||
if 0 < vector_space.limit <= vector_space.size:
|
||||
raise ValueError("Your total number of documents plus the number of uploads have over the limit of "
|
||||
"your subscription.")
|
||||
except Exception as e:
|
||||
for document_id in document_ids:
|
||||
document = db.session.query(Document).filter(
|
||||
Document.id == document_id,
|
||||
Document.dataset_id == dataset_id
|
||||
).first()
|
||||
if document:
|
||||
document.indexing_status = 'error'
|
||||
document.error = str(e)
|
||||
document.stopped_at = datetime.datetime.utcnow()
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
for document_id in document_ids:
|
||||
logging.info(click.style('Start process document: {}'.format(document_id), fg='green'))
|
||||
|
||||
document = db.session.query(Document).filter(
|
||||
Document.id == document_id,
|
||||
Document.dataset_id == dataset_id
|
||||
).first()
|
||||
|
||||
if document:
|
||||
# clean old data
|
||||
index_type = document.doc_form
|
||||
index_processor = IndexProcessorFactory(index_type).init_index_processor()
|
||||
|
||||
segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document_id).all()
|
||||
if segments:
|
||||
index_node_ids = [segment.index_node_id for segment in segments]
|
||||
|
||||
# delete from vector index
|
||||
index_processor.clean(dataset, index_node_ids)
|
||||
|
||||
for segment in segments:
|
||||
db.session.delete(segment)
|
||||
db.session.commit()
|
||||
|
||||
document.indexing_status = 'parsing'
|
||||
document.processing_started_at = datetime.datetime.utcnow()
|
||||
documents.append(document)
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
|
||||
try:
|
||||
indexing_runner = IndexingRunner()
|
||||
indexing_runner.run(documents)
|
||||
end_at = time.perf_counter()
|
||||
logging.info(click.style('Processed dataset: {} latency: {}'.format(dataset_id, end_at - start_at), fg='green'))
|
||||
except DocumentIsPausedException as ex:
|
||||
logging.info(click.style(str(ex), fg='yellow'))
|
||||
except Exception:
|
||||
pass
|
91
api/tasks/retry_document_indexing_task.py
Normal file
91
api/tasks/retry_document_indexing_task.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.rag.index_processor.index_processor_factory import IndexProcessorFactory
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import Dataset, Document, DocumentSegment
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@shared_task(queue='dataset')
|
||||
def retry_document_indexing_task(dataset_id: str, document_ids: list[str]):
|
||||
"""
|
||||
Async process document
|
||||
:param dataset_id:
|
||||
:param document_ids:
|
||||
|
||||
Usage: retry_document_indexing_task.delay(dataset_id, document_id)
|
||||
"""
|
||||
documents = []
|
||||
start_at = time.perf_counter()
|
||||
|
||||
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
|
||||
for document_id in document_ids:
|
||||
retry_indexing_cache_key = 'document_{}_is_retried'.format(document_id)
|
||||
# check document limit
|
||||
features = FeatureService.get_features(dataset.tenant_id)
|
||||
try:
|
||||
if features.billing.enabled:
|
||||
vector_space = features.vector_space
|
||||
if 0 < vector_space.limit <= vector_space.size:
|
||||
raise ValueError("Your total number of documents plus the number of uploads have over the limit of "
|
||||
"your subscription.")
|
||||
except Exception as e:
|
||||
document = db.session.query(Document).filter(
|
||||
Document.id == document_id,
|
||||
Document.dataset_id == dataset_id
|
||||
).first()
|
||||
if document:
|
||||
document.indexing_status = 'error'
|
||||
document.error = str(e)
|
||||
document.stopped_at = datetime.datetime.utcnow()
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
redis_client.delete(retry_indexing_cache_key)
|
||||
return
|
||||
|
||||
logging.info(click.style('Start retry document: {}'.format(document_id), fg='green'))
|
||||
document = db.session.query(Document).filter(
|
||||
Document.id == document_id,
|
||||
Document.dataset_id == dataset_id
|
||||
).first()
|
||||
try:
|
||||
if document:
|
||||
# clean old data
|
||||
index_processor = IndexProcessorFactory(document.doc_form).init_index_processor()
|
||||
|
||||
segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document_id).all()
|
||||
if segments:
|
||||
index_node_ids = [segment.index_node_id for segment in segments]
|
||||
# delete from vector index
|
||||
index_processor.clean(dataset, index_node_ids)
|
||||
|
||||
for segment in segments:
|
||||
db.session.delete(segment)
|
||||
db.session.commit()
|
||||
|
||||
document.indexing_status = 'parsing'
|
||||
document.processing_started_at = datetime.datetime.utcnow()
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
|
||||
indexing_runner = IndexingRunner()
|
||||
indexing_runner.run([document])
|
||||
redis_client.delete(retry_indexing_cache_key)
|
||||
except Exception as ex:
|
||||
document.indexing_status = 'error'
|
||||
document.error = str(ex)
|
||||
document.stopped_at = datetime.datetime.utcnow()
|
||||
db.session.add(document)
|
||||
db.session.commit()
|
||||
logging.info(click.style(str(ex), fg='yellow'))
|
||||
redis_client.delete(retry_indexing_cache_key)
|
||||
pass
|
||||
end_at = time.perf_counter()
|
||||
logging.info(click.style('Retry dataset: {} latency: {}'.format(dataset_id, end_at - start_at), fg='green'))
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
|
@ -22,9 +22,12 @@ import { useProviderContext } from '@/context/provider-context'
|
|||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import EditAppModal from '@/app/components/explore/create-app-modal'
|
||||
import SwitchAppModal from '@/app/components/app/switch-app-modal'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
|
@ -142,6 +145,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
}
|
||||
|
||||
const Operations = (props: HtmlContentProps) => {
|
||||
const onMouseLeave = async () => {
|
||||
props.onClose?.()
|
||||
}
|
||||
const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
|
@ -173,7 +179,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
setShowConfirmDelete(true)
|
||||
}
|
||||
return (
|
||||
<div className="relative w-full py-1">
|
||||
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
||||
<button className={s.actionItem} onClick={onClickSettings}>
|
||||
<span className={s.actionName}>{t('app.editApp')}</span>
|
||||
</button>
|
||||
|
@ -208,6 +214,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
)
|
||||
}
|
||||
|
||||
const [tags, setTags] = useState<Tag[]>(app.tags)
|
||||
useEffect(() => {
|
||||
setTags(app.tags)
|
||||
}, [app.tags])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
@ -254,27 +265,71 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{isCurrentWorkspaceManager && <CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
|
||||
'!hidden h-8 w-8 !p-2 rounded-md border-none hover:!bg-gray-100 group-hover:!inline-flex',
|
||||
)
|
||||
}
|
||||
className={'!w-[128px] h-fit !z-20'}
|
||||
popupClassName={
|
||||
(app.mode === 'completion' || app.mode === 'chat')
|
||||
? '!w-[238px] translate-x-[-110px]'
|
||||
: ''
|
||||
}
|
||||
manualClose
|
||||
/>}
|
||||
</div>
|
||||
<div className='mb-1 px-[14px] text-xs leading-normal text-gray-500 line-clamp-4'>{app.description}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500 group-hover:line-clamp-2 group-hover:max-h-[36px]',
|
||||
tags.length ? 'line-clamp-2' : 'line-clamp-4',
|
||||
)}
|
||||
title={app.description}
|
||||
>
|
||||
{app.description}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
|
||||
tags.length ? 'flex' : '!hidden group-hover:!flex',
|
||||
)}>
|
||||
<div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
<div className={cn(
|
||||
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
|
||||
tags.length ? '!block' : '!hidden',
|
||||
)}>
|
||||
<TagSelector
|
||||
position='bl'
|
||||
type='app'
|
||||
targetID={app.id}
|
||||
value={tags.map(tag => tag.id)}
|
||||
selectedTags={tags}
|
||||
onCacheUpdate={setTags}
|
||||
onChange={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<>
|
||||
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
|
||||
<div className='!hidden group-hover:!flex shrink-0'>
|
||||
<CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={
|
||||
<div
|
||||
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
|
||||
>
|
||||
<DotsHorizontal className='w-4 h-4 text-gray-700' />
|
||||
</div>
|
||||
}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
|
||||
)
|
||||
}
|
||||
popupClassName={
|
||||
(app.mode === 'completion' || app.mode === 'chat')
|
||||
? '!w-[238px] translate-x-[-110px]'
|
||||
: ''
|
||||
}
|
||||
className={'!w-[128px] h-fit !z-20'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showEditModal && (
|
||||
<EditAppModal
|
||||
isEditModal
|
||||
|
|
|
@ -13,19 +13,22 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
|||
import { CheckModal } from '@/hooks/use-pay'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import { DotsGrid, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { DotsGrid } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
// AiText,
|
||||
ChatBot,
|
||||
CuteRobot,
|
||||
} from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
import TagManagementModal from '@/app/components/base/tag-management'
|
||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: AppListResponse,
|
||||
activeTab: string,
|
||||
tags: string[],
|
||||
keywords: string,
|
||||
) => {
|
||||
if (!pageIndex || previousPageData.has_more) {
|
||||
|
@ -36,6 +39,9 @@ const getKey = (
|
|||
else
|
||||
delete params.params.mode
|
||||
|
||||
if (tags.length)
|
||||
params.params.tag_ids = tags
|
||||
|
||||
return params
|
||||
}
|
||||
return null
|
||||
|
@ -44,14 +50,17 @@ const getKey = (
|
|||
const Apps = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
const [activeTab, setActiveTab] = useTabSearchParams({
|
||||
defaultTab: 'all',
|
||||
})
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const [tagIDs, setTagIDs] = useState<string[]>([])
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(
|
||||
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, searchKeywords),
|
||||
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords),
|
||||
fetchAppList,
|
||||
{ revalidateFirstPage: true },
|
||||
)
|
||||
|
@ -61,7 +70,6 @@ const Apps = () => {
|
|||
{ value: 'all', text: t('app.types.all'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'chat', text: t('app.types.chatbot'), icon: <ChatBot className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'agent-chat', text: t('app.types.agent'), icon: <CuteRobot className='w-[14px] h-[14px] mr-1'/> },
|
||||
// { value: 'completion', text: t('app.newApp.completeApp'), icon: <AiText className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'workflow', text: t('app.types.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1'/> },
|
||||
]
|
||||
|
||||
|
@ -88,14 +96,17 @@ const Apps = () => {
|
|||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
handleKeywordsChange('')
|
||||
const { run: handleTagsUpdate } = useDebounceFn(() => {
|
||||
setTagIDs(tagFilterValue)
|
||||
}, { wait: 500 })
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
setTagFilterValue(value)
|
||||
handleTagsUpdate()
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -106,31 +117,9 @@ const Apps = () => {
|
|||
onChange={setActiveTab}
|
||||
options={options}
|
||||
/>
|
||||
<div className="flex items-center px-2 w-[200px] h-8 rounded-lg bg-gray-200">
|
||||
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
|
||||
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="query"
|
||||
className="grow block h-[18px] bg-gray-200 rounded-md border-0 text-gray-600 text-[13px] placeholder:text-gray-500 appearance-none outline-none"
|
||||
placeholder={t('common.operation.search')!}
|
||||
value={keywords}
|
||||
onChange={(e) => {
|
||||
handleKeywordsChange(e.target.value)
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{
|
||||
keywords && (
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center w-4 h-4 cursor-pointer'
|
||||
onClick={handleClear}
|
||||
>
|
||||
<XCircle className='w-3.5 h-3.5 text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center gap-2'>
|
||||
<TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
</div>
|
||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
|
||||
|
@ -142,6 +131,9 @@ const Apps = () => {
|
|||
<CheckModal />
|
||||
</nav>
|
||||
<div ref={anchorRef} className='h-0'> </div>
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='app' show={showTagManagementModal} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
'use client'
|
||||
|
||||
// Libraries
|
||||
import { useRef } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import useSWR from 'swr'
|
||||
|
||||
// Components
|
||||
|
@ -11,15 +12,20 @@ import DatasetFooter from './DatasetFooter'
|
|||
import ApiServer from './ApiServer'
|
||||
import Doc from './Doc'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import TagManagementModal from '@/app/components/base/tag-management'
|
||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||
|
||||
// Services
|
||||
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
|
||||
|
||||
// Hooks
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
|
||||
const Container = () => {
|
||||
const { t } = useTranslation()
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
|
||||
const options = [
|
||||
{ value: 'dataset', text: t('dataset.datasets') },
|
||||
|
@ -32,6 +38,25 @@ const Container = () => {
|
|||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { data } = useSWR(activeTab === 'dataset' ? null : '/datasets/api-base-info', fetchDatasetApiBaseUrl)
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const [tagIDs, setTagIDs] = useState<string[]>([])
|
||||
const { run: handleTagsUpdate } = useDebounceFn(() => {
|
||||
setTagIDs(tagFilterValue)
|
||||
}, { wait: 500 })
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
setTagFilterValue(value)
|
||||
handleTagsUpdate()
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
|
||||
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
|
||||
|
@ -40,13 +65,22 @@ const Container = () => {
|
|||
onChange={newActiveTab => setActiveTab(newActiveTab)}
|
||||
options={options}
|
||||
/>
|
||||
{activeTab === 'dataset' && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
|
||||
</div>
|
||||
|
||||
{activeTab === 'dataset' && (
|
||||
<>
|
||||
<Datasets containerRef={containerRef} />
|
||||
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
|
||||
<DatasetFooter />
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='knowledge' show={showTagManagementModal} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
@ -2,41 +2,44 @@
|
|||
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Link from 'next/link'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import style from '../list.module.css'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { deleteDataset } from '@/service/datasets'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import type { HtmlContentProps } from '@/app/components/base/popover'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import RenameDatasetModal from '@/app/components/datasets/rename-modal'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
|
||||
export type DatasetCardProps = {
|
||||
dataset: DataSet
|
||||
onDelete?: () => void
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
const DatasetCard = ({
|
||||
dataset,
|
||||
onDelete,
|
||||
onSuccess,
|
||||
}: DatasetCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [tags, setTags] = useState<Tag[]>(dataset.tags)
|
||||
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const onDeleteClick: MouseEventHandler = useCallback((e) => {
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}, [])
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
notify({ type: 'success', message: t('dataset.datasetDeleted') })
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('dataset.datasetDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
|
@ -44,53 +47,158 @@ const DatasetCard = ({
|
|||
setShowConfirmDelete(false)
|
||||
}, [dataset.id])
|
||||
|
||||
const Operations = (props: HtmlContentProps) => {
|
||||
const onMouseLeave = async () => {
|
||||
props.onClose?.()
|
||||
}
|
||||
const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowRenameModal(true)
|
||||
}
|
||||
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
return (
|
||||
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
||||
<div className='h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer' onClick={onClickRename}>
|
||||
<span className='text-gray-700 text-sm'>{t('common.operation.settings')}</span>
|
||||
</div>
|
||||
<Divider className="!my-1" />
|
||||
<div
|
||||
className='group h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-red-50 rounded-lg cursor-pointer'
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
<span className={cn('text-gray-700 text-sm', 'group-hover:text-red-500')}>
|
||||
{t('common.operation.delete')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTags(dataset.tags)
|
||||
}, [dataset])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link href={`/datasets/${dataset.id}/documents`} className={cn(style.listItem)} data-disable-nprogress={true}>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' className={cn(!dataset.embedding_available && style.unavailable)} />
|
||||
<div className={cn(style.listItemHeading, !dataset.embedding_available && style.unavailable)}>
|
||||
<div className={style.listItemHeadingContent}>
|
||||
{dataset.name}
|
||||
<Link
|
||||
href={`/datasets/${dataset.id}/documents`}
|
||||
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
|
||||
data-disable-nprogress={true}
|
||||
>
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<div className={cn(
|
||||
'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',
|
||||
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
|
||||
)}>
|
||||
<Folder className='w-5 h-5 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='grow w-0 py-[1px]'>
|
||||
<div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
|
||||
<div className={cn('truncate', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} title={dataset.name}>{dataset.name}</div>
|
||||
{!dataset.embedding_available && (
|
||||
<Tooltip
|
||||
selector={`dataset-tag-${dataset.id}`}
|
||||
htmlContent={t('dataset.unavailableTip')}
|
||||
>
|
||||
<span className='shrink-0 inline-flex w-max ml-1 px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center mt-[1px] text-xs leading-[18px] text-gray-500'>
|
||||
<div
|
||||
className={cn('truncate', (!dataset.embedding_available || !dataset.document_count) && 'opacity-50')}
|
||||
title={`${dataset.document_count}${t('dataset.documentCount')} · ${Math.round(dataset.word_count / 1000)}${t('dataset.wordCount')} · ${dataset.app_count}${t('dataset.appCount')}`}
|
||||
>
|
||||
<span>{dataset.document_count}{t('dataset.documentCount')}</span>
|
||||
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
|
||||
<span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
|
||||
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
|
||||
<span>{dataset.app_count}{t('dataset.appCount')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!dataset.embedding_available && (
|
||||
<Tooltip
|
||||
selector={`dataset-tag-${dataset.id}`}
|
||||
htmlContent={t('dataset.unavailableTip')}
|
||||
>
|
||||
<span className='px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500 group-hover:line-clamp-2 group-hover:max-h-[36px]',
|
||||
tags.length ? 'line-clamp-2' : 'line-clamp-4',
|
||||
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
|
||||
)}
|
||||
<span className={style.deleteDatasetIcon} onClick={onDeleteClick} />
|
||||
title={dataset.description}>
|
||||
{dataset.description}
|
||||
</div>
|
||||
<div className={cn(style.listItemDescription, !dataset.embedding_available && style.unavailable)}>{dataset.description}</div>
|
||||
<div className={cn(style.listItemFooter, style.datasetCardFooter, !dataset.embedding_available && style.unavailable)}>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={cn(style.listItemFooterIcon, style.docIcon)} />
|
||||
{dataset.document_count}{t('dataset.documentCount')}
|
||||
</span>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={cn(style.listItemFooterIcon, style.textIcon)} />
|
||||
{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}
|
||||
</span>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={cn(style.listItemFooterIcon, style.applicationIcon)} />
|
||||
{dataset.app_count}{t('dataset.appCount')}
|
||||
</span>
|
||||
<div className={cn(
|
||||
'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
|
||||
tags.length ? 'flex' : '!hidden group-hover:!flex',
|
||||
)}>
|
||||
<div className={cn('grow flex items-center gap-1 w-0', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
<div className={cn(
|
||||
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
|
||||
tags.length ? '!block' : '!hidden',
|
||||
)}>
|
||||
<TagSelector
|
||||
position='bl'
|
||||
type='knowledge'
|
||||
targetID={dataset.id}
|
||||
value={tags.map(tag => tag.id)}
|
||||
selectedTags={tags}
|
||||
onCacheUpdate={setTags}
|
||||
onChange={onSuccess}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
|
||||
<div className='!hidden group-hover:!flex shrink-0'>
|
||||
<CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={
|
||||
<div
|
||||
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
|
||||
>
|
||||
<DotsHorizontal className='w-4 h-4 text-gray-700' />
|
||||
</div>
|
||||
}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
|
||||
)
|
||||
}
|
||||
className={'!w-[128px] h-fit !z-20'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('dataset.deleteDatasetConfirmTitle')}
|
||||
content={t('dataset.deleteDatasetConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onClose={() => setShowConfirmDelete(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
{showRenameModal && (
|
||||
<RenameDatasetModal
|
||||
show={showRenameModal}
|
||||
dataset={dataset}
|
||||
onClose={() => setShowRenameModal(false)}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('dataset.deleteDatasetConfirmTitle')}
|
||||
content={t('dataset.deleteDatasetConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onClose={() => setShowConfirmDelete(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,21 +10,46 @@ import type { DataSetListResponse } from '@/models/datasets'
|
|||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
|
||||
if (!pageIndex || previousPageData.has_more)
|
||||
return { url: 'datasets', params: { page: pageIndex + 1, limit: 30 } }
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: DataSetListResponse,
|
||||
tags: string[],
|
||||
keyword: string,
|
||||
) => {
|
||||
if (!pageIndex || previousPageData.has_more) {
|
||||
const params: any = {
|
||||
url: 'datasets',
|
||||
params: {
|
||||
page: pageIndex + 1,
|
||||
limit: 30,
|
||||
},
|
||||
}
|
||||
if (tags.length)
|
||||
params.params.tag_ids = tags
|
||||
if (keyword)
|
||||
params.params.keyword = keyword
|
||||
return params
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type Props = {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
tags: string[]
|
||||
keywords: string
|
||||
}
|
||||
|
||||
const Datasets = ({
|
||||
containerRef,
|
||||
tags,
|
||||
keywords,
|
||||
}: Props) => {
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false, revalidateAll: true })
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(
|
||||
(pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords),
|
||||
fetchDatasets,
|
||||
{ revalidateFirstPage: false, revalidateAll: true },
|
||||
)
|
||||
const loadingStateRef = useRef(false)
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
|
@ -53,7 +78,7 @@ const Datasets = ({
|
|||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
|
||||
{ isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> }
|
||||
{data?.map(({ data: datasets }) => datasets.map(dataset => (
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />),
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
'use client'
|
||||
|
||||
import { forwardRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import style from '../list.module.css'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const CreateAppCard = forwardRef<HTMLAnchorElement>((_, ref) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Link ref={ref} className={classNames(style.listItem, style.newItemCard)} href='/datasets/create'>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
|
||||
</span>
|
||||
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
|
||||
{t('dataset.createDataset')}
|
||||
<a ref={ref} className='group flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-white hover:shadow-lg' href='/datasets/create'>
|
||||
<div className='shrnik-0 flex items-center p-4 pb-3'>
|
||||
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg'>
|
||||
<Plus className='w-4 h-4 text-gray-500'/>
|
||||
</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('dataset.createDataset')}</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('dataset.createDatasetIntro')}</div>
|
||||
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
|
||||
</Link>
|
||||
<div className='mb-1 px-4 text-xs leading-normal text-gray-500 line-clamp-4'>{t('dataset.createDatasetIntro')}</div>
|
||||
</a>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.2 1.5H2.3C2.01997 1.5 1.87996 1.5 1.773 1.5545C1.67892 1.60243 1.60243 1.67892 1.5545 1.773C1.5 1.87996 1.5 2.01997 1.5 2.3V4.2C1.5 4.48003 1.5 4.62004 1.5545 4.727C1.60243 4.82108 1.67892 4.89757 1.773 4.9455C1.87996 5 2.01997 5 2.3 5H4.2C4.48003 5 4.62004 5 4.727 4.9455C4.82108 4.89757 4.89757 4.82108 4.9455 4.727C5 4.62004 5 4.48003 5 4.2V2.3C5 2.01997 5 1.87996 4.9455 1.773C4.89757 1.67892 4.82108 1.60243 4.727 1.5545C4.62004 1.5 4.48003 1.5 4.2 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.7 1.5H7.8C7.51997 1.5 7.37996 1.5 7.273 1.5545C7.17892 1.60243 7.10243 1.67892 7.0545 1.773C7 1.87996 7 2.01997 7 2.3V4.2C7 4.48003 7 4.62004 7.0545 4.727C7.10243 4.82108 7.17892 4.89757 7.273 4.9455C7.37996 5 7.51997 5 7.8 5H9.7C9.98003 5 10.12 5 10.227 4.9455C10.3211 4.89757 10.3976 4.82108 10.4455 4.727C10.5 4.62004 10.5 4.48003 10.5 4.2V2.3C10.5 2.01997 10.5 1.87996 10.4455 1.773C10.3976 1.67892 10.3211 1.60243 10.227 1.5545C10.12 1.5 9.98003 1.5 9.7 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.7 7H7.8C7.51997 7 7.37996 7 7.273 7.0545C7.17892 7.10243 7.10243 7.17892 7.0545 7.273C7 7.37996 7 7.51997 7 7.8V9.7C7 9.98003 7 10.12 7.0545 10.227C7.10243 10.3211 7.17892 10.3976 7.273 10.4455C7.37996 10.5 7.51997 10.5 7.8 10.5H9.7C9.98003 10.5 10.12 10.5 10.227 10.4455C10.3211 10.3976 10.3976 10.3211 10.4455 10.227C10.5 10.12 10.5 9.98003 10.5 9.7V7.8C10.5 7.51997 10.5 7.37996 10.4455 7.273C10.3976 7.17892 10.3211 7.10243 10.227 7.0545C10.12 7 9.98003 7 9.7 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.2 7H2.3C2.01997 7 1.87996 7 1.773 7.0545C1.67892 7.10243 1.60243 7.17892 1.5545 7.273C1.5 7.37996 1.5 7.51997 1.5 7.8V9.7C1.5 9.98003 1.5 10.12 1.5545 10.227C1.60243 10.3211 1.67892 10.3976 1.773 10.4455C1.87996 10.5 2.01997 10.5 2.3 10.5H4.2C4.48003 10.5 4.62004 10.5 4.727 10.4455C4.82108 10.3976 4.89757 10.3211 4.9455 10.227C5 10.12 5 9.98003 5 9.7V7.8C5 7.51997 5 7.37996 4.9455 7.273C4.89757 7.17892 4.82108 7.10243 4.727 7.0545C4.62004 7 4.48003 7 4.2 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 457 B |
|
@ -1,3 +0,0 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 457 B |
|
@ -159,18 +159,6 @@
|
|||
background-image: url("./apps/assets/completion-solid.svg");
|
||||
}
|
||||
|
||||
.docIcon {
|
||||
background-image: url("./datasets/assets/doc.svg");
|
||||
}
|
||||
|
||||
.textIcon {
|
||||
background-image: url("./datasets/assets/text.svg");
|
||||
}
|
||||
|
||||
.applicationIcon {
|
||||
background-image: url("./datasets/assets/application.svg");
|
||||
}
|
||||
|
||||
.newItemCardHeading {
|
||||
@apply transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon" clip-path="url(#clip0_17795_9693)">
|
||||
<path id="Icon_2" d="M4.66699 4.6665H4.67283M1.16699 3.03317L1.16699 5.6433C1.16699 5.92866 1.16699 6.07134 1.19923 6.20561C1.22781 6.32465 1.27495 6.43845 1.33891 6.54284C1.41106 6.66057 1.51195 6.76146 1.71373 6.96324L6.18709 11.4366C6.88012 12.1296 7.22664 12.4761 7.62621 12.606C7.97769 12.7202 8.35629 12.7202 8.70777 12.606C9.10735 12.4761 9.45386 12.1296 10.1469 11.4366L11.4371 10.1464C12.1301 9.45337 12.4766 9.10686 12.6065 8.70728C12.7207 8.35581 12.7207 7.9772 12.6065 7.62572C12.4766 7.22615 12.1301 6.87963 11.4371 6.1866L6.96372 1.71324C6.76195 1.51146 6.66106 1.41057 6.54332 1.33842C6.43894 1.27446 6.32514 1.22732 6.20609 1.19874C6.07183 1.1665 5.92915 1.1665 5.64379 1.1665L3.03366 1.1665C2.38026 1.1665 2.05357 1.1665 1.804 1.29366C1.58448 1.40552 1.406 1.58399 1.29415 1.80352C1.16699 2.05308 1.16699 2.37978 1.16699 3.03317ZM4.95866 4.6665C4.95866 4.82759 4.82808 4.95817 4.66699 4.95817C4.50591 4.95817 4.37533 4.82759 4.37533 4.6665C4.37533 4.50542 4.50591 4.37484 4.66699 4.37484C4.82808 4.37484 4.95866 4.50542 4.95866 4.6665Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_17795_9693">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="tag-03">
|
||||
<path id="Icon" d="M14 7.3335L8.93726 2.27075C8.59135 1.92485 8.4184 1.7519 8.21657 1.62822C8.03762 1.51856 7.84254 1.43775 7.63846 1.38876C7.40829 1.3335 7.16369 1.3335 6.67452 1.3335L4 1.3335M2 5.80016L2 7.11651C2 7.44263 2 7.60569 2.03684 7.75914C2.0695 7.89519 2.12337 8.02525 2.19648 8.14454C2.27894 8.2791 2.39424 8.3944 2.62484 8.625L7.82484 13.825C8.35286 14.353 8.61687 14.617 8.92131 14.716C9.1891 14.803 9.47757 14.803 9.74536 14.716C10.0498 14.617 10.3138 14.353 10.8418 13.825L12.4915 12.1753C13.0195 11.6473 13.2835 11.3833 13.3825 11.0789C13.4695 10.8111 13.4695 10.5226 13.3825 10.2548C13.2835 9.95037 13.0195 9.68636 12.4915 9.15834L7.62484 4.29167C7.39424 4.06107 7.27894 3.94577 7.14438 3.86331C7.02508 3.7902 6.89502 3.73633 6.75898 3.70367C6.60553 3.66683 6.44247 3.66683 6.11634 3.66683H4.13333C3.3866 3.66683 3.01323 3.66683 2.72801 3.81215C2.47713 3.93999 2.27316 4.14396 2.14532 4.39484C2 4.68006 2 5.05343 2 5.80016Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"viewBox": "0 0 14 14",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"clip-path": "url(#clip0_17795_9693)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon_2",
|
||||
"d": "M4.66699 4.6665H4.67283M1.16699 3.03317L1.16699 5.6433C1.16699 5.92866 1.16699 6.07134 1.19923 6.20561C1.22781 6.32465 1.27495 6.43845 1.33891 6.54284C1.41106 6.66057 1.51195 6.76146 1.71373 6.96324L6.18709 11.4366C6.88012 12.1296 7.22664 12.4761 7.62621 12.606C7.97769 12.7202 8.35629 12.7202 8.70777 12.606C9.10735 12.4761 9.45386 12.1296 10.1469 11.4366L11.4371 10.1464C12.1301 9.45337 12.4766 9.10686 12.6065 8.70728C12.7207 8.35581 12.7207 7.9772 12.6065 7.62572C12.4766 7.22615 12.1301 6.87963 11.4371 6.1866L6.96372 1.71324C6.76195 1.51146 6.66106 1.41057 6.54332 1.33842C6.43894 1.27446 6.32514 1.22732 6.20609 1.19874C6.07183 1.1665 5.92915 1.1665 5.64379 1.1665L3.03366 1.1665C2.38026 1.1665 2.05357 1.1665 1.804 1.29366C1.58448 1.40552 1.406 1.58399 1.29415 1.80352C1.16699 2.05308 1.16699 2.37978 1.16699 3.03317ZM4.95866 4.6665C4.95866 4.82759 4.82808 4.95817 4.66699 4.95817C4.50591 4.95817 4.37533 4.82759 4.37533 4.6665C4.37533 4.50542 4.50591 4.37484 4.66699 4.37484C4.82808 4.37484 4.95866 4.50542 4.95866 4.6665Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_17795_9693"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Tag01"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Tag01.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Tag01'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "tag-03"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"d": "M14 7.3335L8.93726 2.27075C8.59135 1.92485 8.4184 1.7519 8.21657 1.62822C8.03762 1.51856 7.84254 1.43775 7.63846 1.38876C7.40829 1.3335 7.16369 1.3335 6.67452 1.3335L4 1.3335M2 5.80016L2 7.11651C2 7.44263 2 7.60569 2.03684 7.75914C2.0695 7.89519 2.12337 8.02525 2.19648 8.14454C2.27894 8.2791 2.39424 8.3944 2.62484 8.625L7.82484 13.825C8.35286 14.353 8.61687 14.617 8.92131 14.716C9.1891 14.803 9.47757 14.803 9.74536 14.716C10.0498 14.617 10.3138 14.353 10.8418 13.825L12.4915 12.1753C13.0195 11.6473 13.2835 11.3833 13.3825 11.0789C13.4695 10.8111 13.4695 10.5226 13.3825 10.2548C13.2835 9.95037 13.0195 9.68636 12.4915 9.15834L7.62484 4.29167C7.39424 4.06107 7.27894 3.94577 7.14438 3.86331C7.02508 3.7902 6.89502 3.73633 6.75898 3.70367C6.60553 3.66683 6.44247 3.66683 6.11634 3.66683H4.13333C3.3866 3.66683 3.01323 3.66683 2.72801 3.81215C2.47713 3.93999 2.27316 4.14396 2.14532 4.39484C2 4.68006 2 5.05343 2 5.80016Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Tag03"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Tag03.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Tag03'
|
||||
|
||||
export default Icon
|
|
@ -1,3 +1,5 @@
|
|||
export { default as CoinsStacked01 } from './CoinsStacked01'
|
||||
export { default as GoldCoin } from './GoldCoin'
|
||||
export { default as ReceiptList } from './ReceiptList'
|
||||
export { default as Tag01 } from './Tag01'
|
||||
export { default as Tag03 } from './Tag03'
|
||||
|
|
|
@ -13,7 +13,7 @@ type IPopover = {
|
|||
htmlContent: React.ReactElement<HtmlContentProps>
|
||||
popupClassName?: string
|
||||
trigger?: 'click' | 'hover'
|
||||
position?: 'bottom' | 'br'
|
||||
position?: 'bottom' | 'br' | 'bl'
|
||||
btnElement?: string | React.ReactNode
|
||||
btnClassName?: string | ((open: boolean) => string)
|
||||
manualClose?: boolean
|
||||
|
@ -71,7 +71,13 @@ export default function CustomPopover({
|
|||
</Popover.Button>
|
||||
<Transition as={Fragment}>
|
||||
<Popover.Panel
|
||||
className={`${s.popupPanel} ${position === 'br' ? 'right-0' : 'translate-x-1/2 left-1/2'} ${className}`}
|
||||
className={cn(
|
||||
s.popupPanel,
|
||||
position === 'bottom' && '-translate-x-1/2 left-1/2',
|
||||
position === 'bl' && 'left-0',
|
||||
position === 'br' && 'right-0',
|
||||
className,
|
||||
)}
|
||||
{...(trigger !== 'hover'
|
||||
? {}
|
||||
: {
|
||||
|
|
85
web/app/components/base/retry-button/index.tsx
Normal file
85
web/app/components/base/retry-button/index.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useReducer } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
import useSWR from 'swr'
|
||||
import s from './style.module.css'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { getErrorDocs, retryErrorDocs } from '@/service/datasets'
|
||||
import type { IndexingStatusResponse } from '@/models/datasets'
|
||||
|
||||
const WarningIcon = () =>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000 /svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.40616 0.834307C6.14751 0.719294 5.85222 0.719294 5.59356 0.834307C5.3938 0.923133 5.26403 1.07959 5.17373 1.20708C5.08495 1.33242 4.9899 1.49664 4.88536 1.67723L0.751783 8.81705C0.646828 8.9983 0.551451 9.16302 0.486781 9.3028C0.421056 9.44487 0.349754 9.63584 0.372478 9.85381C0.401884 10.1359 0.549654 10.3922 0.779012 10.5589C0.956259 10.6878 1.15726 10.7218 1.31314 10.7361C1.46651 10.7501 1.65684 10.7501 1.86628 10.7501H10.1334C10.3429 10.7501 10.5332 10.7501 10.6866 10.7361C10.8425 10.7218 11.0435 10.6878 11.2207 10.5589C11.4501 10.3922 11.5978 10.1359 11.6272 9.85381C11.65 9.63584 11.5787 9.44487 11.5129 9.3028C11.4483 9.16303 11.3529 8.99833 11.248 8.81709L7.11436 1.67722C7.00983 1.49663 6.91477 1.33242 6.82599 1.20708C6.73569 1.07959 6.60593 0.923133 6.40616 0.834307ZM6.49988 4.50012C6.49988 4.22398 6.27602 4.00012 5.99988 4.00012C5.72374 4.00012 5.49988 4.22398 5.49988 4.50012V6.50012C5.49988 6.77626 5.72374 7.00012 5.99988 7.00012C6.27602 7.00012 6.49988 6.77626 6.49988 6.50012V4.50012ZM5.99988 8.00012C5.72374 8.00012 5.49988 8.22398 5.49988 8.50012C5.49988 8.77626 5.72374 9.00012 5.99988 9.00012H6.00488C6.28102 9.00012 6.50488 8.77626 6.50488 8.50012C6.50488 8.22398 6.28102 8.00012 6.00488 8.00012H5.99988Z" fill="#F79009" />
|
||||
</svg>
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
}
|
||||
type IIndexState = {
|
||||
value: string
|
||||
}
|
||||
type ActionType = 'retry' | 'success' | 'error'
|
||||
|
||||
type IAction = {
|
||||
type: ActionType
|
||||
}
|
||||
const indexStateReducer = (state: IIndexState, action: IAction) => {
|
||||
const actionMap = {
|
||||
retry: 'retry',
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
value: actionMap[action.type] || state.value,
|
||||
}
|
||||
}
|
||||
|
||||
const RetryButton: FC<Props> = ({ datasetId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' })
|
||||
const { data: errorDocs } = useSWR({ datasetId }, getErrorDocs)
|
||||
|
||||
const onRetryErrorDocs = async () => {
|
||||
dispatch({ type: 'retry' })
|
||||
const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || []
|
||||
const res = await retryErrorDocs({ datasetId, document_ids })
|
||||
if (res.result === 'success')
|
||||
dispatch({ type: 'success' })
|
||||
else
|
||||
dispatch({ type: 'error' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (errorDocs?.total === 0)
|
||||
dispatch({ type: 'success' })
|
||||
else
|
||||
dispatch({ type: 'error' })
|
||||
}, [errorDocs?.total])
|
||||
|
||||
if (indexState.value === 'success')
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={classNames('inline-flex justify-center items-center gap-2', s.retryBtn)}>
|
||||
<WarningIcon />
|
||||
<span className='flex shrink-0 text-sm text-gray-500'>
|
||||
{errorDocs?.total} {t('dataset.docsFailedNotice')}
|
||||
</span>
|
||||
<Divider type='vertical' className='!h-4' />
|
||||
<span
|
||||
className={classNames(
|
||||
'text-primary-600 font-semibold text-sm cursor-pointer',
|
||||
indexState.value === 'retry' && '!text-gray-500 !cursor-not-allowed',
|
||||
)}
|
||||
onClick={indexState.value === 'error' ? onRetryErrorDocs : undefined}
|
||||
>
|
||||
{t('dataset.retry')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RetryButton
|
4
web/app/components/base/retry-button/style.module.css
Normal file
4
web/app/components/base/retry-button/style.module.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.retryBtn {
|
||||
@apply inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base;
|
||||
@apply border-solid border border-gray-200 text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300;
|
||||
}
|
66
web/app/components/base/search-input/index.tsx
Normal file
66
web/app/components/base/search-input/index.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
type SearchInputProps = {
|
||||
placeholder?: string
|
||||
className?: string
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
white?: boolean
|
||||
}
|
||||
const SearchInput: FC<SearchInputProps> = ({
|
||||
placeholder,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
white,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [focus, setFocus] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'group flex items-center px-2 h-8 rounded-lg bg-gray-200 hover:bg-gray-300 border border-transparent overflow-hidden',
|
||||
focus && '!bg-white hover:bg-white shawdow-xs !border-gray-300',
|
||||
!focus && value && 'hover:!bg-gray-200 hover:!shawdow-xs hover:!border-black/5',
|
||||
white && '!bg-white hover:!bg-white shawdow-xs !border-gray-300 hover:!border-gray-300',
|
||||
className,
|
||||
)}>
|
||||
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
|
||||
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="query"
|
||||
className={cn(
|
||||
'grow block h-[18px] bg-gray-200 rounded-md border-0 text-gray-700 text-[13px] placeholder:text-gray-500 appearance-none outline-none group-hover:bg-gray-300 caret-blue-600',
|
||||
focus && '!bg-white hover:bg-white group-hover:bg-white placeholder:!text-gray-400',
|
||||
!focus && value && 'hover:!bg-gray-200 group-hover:!bg-gray-200',
|
||||
white && '!bg-white hover:!bg-white group-hover:!bg-white placeholder:!text-gray-400',
|
||||
)}
|
||||
placeholder={placeholder || t('common.operation.search')!}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{value && (
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center w-4 h-4 cursor-pointer group/clear'
|
||||
onClick={() => onChange('')}
|
||||
>
|
||||
<XCircle className='w-3.5 h-3.5 text-gray-400 group-hover/clear:text-gray-600' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
6
web/app/components/base/tag-management/constant.ts
Normal file
6
web/app/components/base/tag-management/constant.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type Tag = {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
binding_count: number
|
||||
}
|
142
web/app/components/base/tag-management/filter.tsx
Normal file
142
web/app/components/base/tag-management/filter.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn, useMount } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useStore as useTagStore } from './store'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
|
||||
import { fetchTagList } from '@/service/tag'
|
||||
|
||||
type TagFilterProps = {
|
||||
type: 'knowledge' | 'app'
|
||||
value: string[]
|
||||
onChange: (v: string[]) => void
|
||||
}
|
||||
const TagFilter: FC<TagFilterProps> = ({
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const tagList = useTagStore(s => s.tagList)
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const filteredTagList = useMemo(() => {
|
||||
return tagList.filter(tag => tag.type === type && tag.name.includes(searchKeywords))
|
||||
}, [type, tagList, searchKeywords])
|
||||
|
||||
const currentTag = useMemo(() => {
|
||||
return tagList.find(tag => tag.id === value[0])
|
||||
}, [value, tagList])
|
||||
|
||||
const selectTag = (tag: Tag) => {
|
||||
if (value.includes(tag.id))
|
||||
onChange(value.filter(v => v !== tag.id))
|
||||
else
|
||||
onChange([...value, tag.id])
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
fetchTagList(type).then((res) => {
|
||||
setTagList(res)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-gray-200 cursor-pointer hover:bg-gray-300',
|
||||
open && !value.length && '!bg-gray-300 hover:bg-gray-300',
|
||||
!open && !!value.length && '!bg-white/80 shadow-xs !border-black/5 hover:!bg-gray-200',
|
||||
open && !!value.length && '!bg-gray-200 !border-black/5 shadow-xs hover:!bg-gray-200',
|
||||
)}>
|
||||
<div className='p-[1px]'>
|
||||
<Tag01 className='h-3.5 w-3.5 text-gray-700' />
|
||||
</div>
|
||||
<div className='text-[13px] leading-[18px] text-gray-700'>
|
||||
{!value.length && t('common.tag.placeholder')}
|
||||
{!!value.length && currentTag?.name}
|
||||
</div>
|
||||
{value.length > 1 && (
|
||||
<div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
|
||||
)}
|
||||
{!value.length && (
|
||||
<div className='p-[1px]'>
|
||||
<ChevronDown className='h-3.5 w-3.5 text-gray-700'/>
|
||||
</div>
|
||||
)}
|
||||
{!!value.length && (
|
||||
<div className='p-[1px] cursor-pointer group/clear' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}>
|
||||
<XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600'/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
<div className='p-2 border-b-[0.5px] border-black/5'>
|
||||
<SearchInput white value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
{filteredTagList.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => selectTag(tag)}
|
||||
>
|
||||
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
|
||||
{value.includes(tag.id) && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
|
||||
</div>
|
||||
))}
|
||||
{!filteredTagList.length && (
|
||||
<div className='p-3 flex flex-col items-center gap-1'>
|
||||
<Tag03 className='h-6 w-6 text-gray-300' />
|
||||
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default TagFilter
|
93
web/app/components/base/tag-management/index.tsx
Normal file
93
web/app/components/base/tag-management/index.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useTagStore } from './store'
|
||||
import TagItemEditor from './tag-item-editor'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
createTag,
|
||||
fetchTagList,
|
||||
} from '@/service/tag'
|
||||
|
||||
type TagManagementModalProps = {
|
||||
type: 'knowledge' | 'app'
|
||||
show: boolean
|
||||
}
|
||||
|
||||
const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const tagList = useTagStore(s => s.tagList)
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
|
||||
|
||||
const getTagList = async (type: 'knowledge' | 'app') => {
|
||||
const res = await fetchTagList(type)
|
||||
setTagList(res)
|
||||
}
|
||||
|
||||
const [pending, setPending] = useState<Boolean>(false)
|
||||
const [name, setName] = useState<string>('')
|
||||
const createNewTag = async () => {
|
||||
if (!name)
|
||||
return
|
||||
if (pending)
|
||||
return
|
||||
try {
|
||||
setPending(true)
|
||||
const newTag = await createTag(name, type)
|
||||
notify({ type: 'success', message: t('common.tag.created') })
|
||||
setTagList([
|
||||
newTag,
|
||||
...tagList,
|
||||
])
|
||||
setName('')
|
||||
setPending(false)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: t('common.tag.failed') })
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTagList(type)
|
||||
}, [type])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapperClassName='!z-[1020]'
|
||||
className='px-8 py-6 !max-w-[600px] !w-[600px] rounded-xl'
|
||||
isShow={show}
|
||||
onClose={() => setShowTagManagementModal(false)}
|
||||
>
|
||||
<div className='relative pb-2 text-xl font-semibold leading-[30px] text-gray-900'>{t('common.tag.manageTags')}</div>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={() => setShowTagManagementModal(false)}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<div className='mt-3 flex flex-wrap gap-2'>
|
||||
<input
|
||||
className='shrink-0 w-[100px] px-2 py-1 rounded-lg border border-dashed border-gray-200 text-sm leading-5 text-gray-700 outline-none appearance-none placeholder:text-gray-300 caret-primary-600 focus:border-solid'
|
||||
placeholder={t('common.tag.addNew') || ''}
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && createNewTag()}
|
||||
onBlur={createNewTag}
|
||||
/>
|
||||
{tagList.map(tag => (
|
||||
<TagItemEditor
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagManagementModal
|
272
web/app/components/base/tag-management/selector.tsx
Normal file
272
web/app/components/base/tag-management/selector.tsx
Normal file
|
@ -0,0 +1,272 @@
|
|||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useStore as useTagStore } from './store'
|
||||
import type { HtmlContentProps } from '@/app/components/base/popover'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { bindTag, createTag, fetchTagList, unBindTag } from '@/service/tag'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
|
||||
type TagSelectorProps = {
|
||||
targetID: string
|
||||
isPopover?: boolean
|
||||
position?: 'bl' | 'br'
|
||||
type: 'knowledge' | 'app'
|
||||
value: string[]
|
||||
selectedTags: Tag[]
|
||||
onCacheUpdate: (tags: Tag[]) => void
|
||||
onChange?: () => void
|
||||
}
|
||||
|
||||
type PanelProps = {
|
||||
onCreate: () => void
|
||||
} & HtmlContentProps & TagSelectorProps
|
||||
|
||||
const Panel = (props: PanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { targetID, type, value, selectedTags, onCacheUpdate, onChange, onCreate } = props
|
||||
const tagList = useTagStore(s => s.tagList)
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
|
||||
const [selectedTagIDs, setSelectedTagIDs] = useState<string[]>(value)
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
}
|
||||
|
||||
const notExisted = useMemo(() => {
|
||||
return tagList.every(tag => tag.type === type && tag.name !== keywords)
|
||||
}, [type, tagList, keywords])
|
||||
const filteredSelectedTagList = useMemo(() => {
|
||||
return selectedTags.filter(tag => tag.name.includes(keywords))
|
||||
}, [keywords, selectedTags])
|
||||
const filteredTagList = useMemo(() => {
|
||||
return tagList.filter(tag => tag.type === type && !value.includes(tag.id) && tag.name.includes(keywords))
|
||||
}, [type, tagList, value, keywords])
|
||||
|
||||
const [creating, setCreating] = useState<Boolean>(false)
|
||||
const createNewTag = async () => {
|
||||
if (!keywords)
|
||||
return
|
||||
if (creating)
|
||||
return
|
||||
try {
|
||||
setCreating(true)
|
||||
const newTag = await createTag(keywords, type)
|
||||
notify({ type: 'success', message: t('common.tag.created') })
|
||||
setTagList([
|
||||
...tagList,
|
||||
newTag,
|
||||
])
|
||||
setCreating(false)
|
||||
onCreate()
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: t('common.tag.failed') })
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
const bind = async (tagIDs: string[]) => {
|
||||
try {
|
||||
await bindTag(tagIDs, targetID, type)
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
}
|
||||
}
|
||||
const unbind = async (tagID: string) => {
|
||||
try {
|
||||
await unBindTag(tagID, targetID, type)
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
}
|
||||
}
|
||||
const selectTag = (tag: Tag) => {
|
||||
if (selectedTagIDs.includes(tag.id))
|
||||
setSelectedTagIDs(selectedTagIDs.filter(v => v !== tag.id))
|
||||
else
|
||||
setSelectedTagIDs([...selectedTagIDs, tag.id])
|
||||
}
|
||||
|
||||
const valueNotChanged = useMemo(() => {
|
||||
return value.length === selectedTagIDs.length && value.every(v => selectedTagIDs.includes(v)) && selectedTagIDs.every(v => value.includes(v))
|
||||
}, [value, selectedTagIDs])
|
||||
const handleValueChange = () => {
|
||||
const addTagIDs = selectedTagIDs.filter(v => !value.includes(v))
|
||||
const removeTagIDs = value.filter(v => !selectedTagIDs.includes(v))
|
||||
const selectedTags = tagList.filter(tag => selectedTagIDs.includes(tag.id))
|
||||
onCacheUpdate(selectedTags)
|
||||
Promise.all([
|
||||
...(addTagIDs.length ? [bind(addTagIDs)] : []),
|
||||
...[removeTagIDs.length ? removeTagIDs.map(tagID => unbind(tagID)) : []],
|
||||
]).finally(() => {
|
||||
if (onChange)
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
useUnmount(() => {
|
||||
if (valueNotChanged)
|
||||
return
|
||||
handleValueChange()
|
||||
})
|
||||
|
||||
const onMouseLeave = async () => {
|
||||
props.onClose?.()
|
||||
}
|
||||
return (
|
||||
<div className='relative w-full bg-white rounded-lg border-[0.5px] border-gray-200' onMouseLeave={onMouseLeave}>
|
||||
<div className='p-2 border-b-[0.5px] border-black/5'>
|
||||
<SearchInput placeholder={t('common.tag.selectorPlaceholder') || ''} white value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
{keywords && notExisted && (
|
||||
<div className='p-1'>
|
||||
<div className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100' onClick={createNewTag}>
|
||||
<Plus className='h-4 w-4 text-gray-500' />
|
||||
<div className='grow text-sm text-gray-700 leading-5 truncate'>
|
||||
{`${t('common.tag.create')} `}
|
||||
<span className='font-medium'>{`"${keywords}"`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{keywords && notExisted && filteredTagList.length > 0 && (
|
||||
<Divider className='!h-[1px] !my-0' />
|
||||
)}
|
||||
{(filteredTagList.length > 0 || filteredSelectedTagList.length > 0) && (
|
||||
<div className='p-1 max-h-[172px] overflow-y-auto'>
|
||||
{filteredSelectedTagList.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => selectTag(tag)}
|
||||
>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={selectedTagIDs.includes(tag.id)}
|
||||
onCheck={() => {}}
|
||||
/>
|
||||
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredTagList.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => selectTag(tag)}
|
||||
>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={selectedTagIDs.includes(tag.id)}
|
||||
onCheck={() => {}}
|
||||
/>
|
||||
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!keywords && !filteredTagList.length && !filteredSelectedTagList.length && (
|
||||
<div className='p-1'>
|
||||
<div className='p-3 flex flex-col items-center gap-1'>
|
||||
<Tag03 className='h-6 w-6 text-gray-300' />
|
||||
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Divider className='!h-[1px] !my-0' />
|
||||
<div className='p-1'>
|
||||
<div className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100' onClick={() => setShowTagManagementModal(true)}>
|
||||
<Tag03 className='h-4 w-4 text-gray-500' />
|
||||
<div className='grow text-sm text-gray-700 leading-5 truncate'>
|
||||
{t('common.tag.manageTags')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TagSelector: FC<TagSelectorProps> = ({
|
||||
targetID,
|
||||
isPopover = true,
|
||||
position,
|
||||
type,
|
||||
value,
|
||||
selectedTags,
|
||||
onCacheUpdate,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
|
||||
const getTagList = async () => {
|
||||
const res = await fetchTagList(type)
|
||||
setTagList(res)
|
||||
}
|
||||
|
||||
const triggerContent = useMemo(() => {
|
||||
if (selectedTags?.length)
|
||||
return selectedTags.map(tag => tag.name).join(', ')
|
||||
return ''
|
||||
}, [selectedTags])
|
||||
|
||||
const Trigger = () => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'group/tip relative w-full flex items-center gap-1 px-2 py-[7px] rounded-md cursor-pointer hover:bg-gray-100',
|
||||
)}>
|
||||
<Tag01 className='shrink-0 w-3 h-3' />
|
||||
<div className='grow text-xs text-start leading-[18px] font-normal truncate'>
|
||||
{!triggerContent ? t('common.tag.addTag') : triggerContent}
|
||||
</div>
|
||||
<span className='hidden absolute top-[-21px] left-[50%] translate-x-[-50%] px-2 py-[3px] border-[0.5px] border-black/5 rounded-md bg-gray-25 text-gray-700 text-xs font-medium leading-[18px] group-hover/tip:block'>{t('common.tag.editTag')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isPopover && (
|
||||
<CustomPopover
|
||||
htmlContent={
|
||||
<Panel
|
||||
type={type}
|
||||
targetID={targetID}
|
||||
value={value}
|
||||
selectedTags={selectedTags}
|
||||
onCacheUpdate={onCacheUpdate}
|
||||
onChange={onChange}
|
||||
onCreate={getTagList}
|
||||
/>
|
||||
}
|
||||
position={position}
|
||||
trigger="click"
|
||||
btnElement={<Trigger />}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-gray-100 !text-gray-700' : '!bg-transparent',
|
||||
'!w-full !p-0 !border-0 !text-gray-500 hover:!bg-gray-100 hover:!text-gray-700',
|
||||
)
|
||||
}
|
||||
popupClassName='!w-full !ring-0'
|
||||
className={'!w-full h-fit !z-20'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default TagSelector
|
19
web/app/components/base/tag-management/store.ts
Normal file
19
web/app/components/base/tag-management/store.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { create } from 'zustand'
|
||||
import type { Tag } from './constant'
|
||||
|
||||
type State = {
|
||||
tagList: Tag[]
|
||||
showTagManagementModal: boolean
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setTagList: (tagList?: Tag[]) => void
|
||||
setShowTagManagementModal: (showTagManagementModal: boolean) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
tagList: [],
|
||||
setTagList: tagList => set(() => ({ tagList })),
|
||||
showTagManagementModal: false,
|
||||
setShowTagManagementModal: showTagManagementModal => set(() => ({ showTagManagementModal })),
|
||||
}))
|
3
web/app/components/base/tag-management/style.module.css
Normal file
3
web/app/components/base/tag-management/style.module.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.bg {
|
||||
background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
|
||||
}
|
147
web/app/components/base/tag-management/tag-item-editor.tsx
Normal file
147
web/app/components/base/tag-management/tag-item-editor.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useTagStore } from './store'
|
||||
import TagRemoveModal from './tag-remove-modal'
|
||||
import { Edit03, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
deleteTag,
|
||||
updateTag,
|
||||
} from '@/service/tag'
|
||||
|
||||
type TagItemEditorProps = {
|
||||
tag: Tag
|
||||
}
|
||||
const TagItemEditor: FC<TagItemEditorProps> = ({
|
||||
tag,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const tagList = useTagStore(s => s.tagList)
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [name, setName] = useState(tag.name)
|
||||
const editTag = async (tagID: string, name: string) => {
|
||||
if (name === tag.name) {
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
if (!name) {
|
||||
notify({ type: 'error', message: 'tag name is empty' })
|
||||
setName(tag.name)
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const newList = tagList.map((tag) => {
|
||||
if (tag.id === tagID) {
|
||||
return {
|
||||
...tag,
|
||||
name,
|
||||
}
|
||||
}
|
||||
return tag
|
||||
})
|
||||
setTagList([
|
||||
...newList,
|
||||
])
|
||||
setIsEditing(false)
|
||||
await updateTag(tagID, name)
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
setName(name)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
setName(tag.name)
|
||||
const recoverList = tagList.map((tag) => {
|
||||
if (tag.id === tagID) {
|
||||
return {
|
||||
...tag,
|
||||
name: tag.name,
|
||||
}
|
||||
}
|
||||
return tag
|
||||
})
|
||||
setTagList([
|
||||
...recoverList,
|
||||
])
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false)
|
||||
const [pending, setPending] = useState<Boolean>(false)
|
||||
const removeTag = async (tagID: string) => {
|
||||
if (pending)
|
||||
return
|
||||
try {
|
||||
setPending(true)
|
||||
await deleteTag(tagID)
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
const newList = tagList.filter(tag => tag.id !== tagID)
|
||||
setTagList([
|
||||
...newList,
|
||||
])
|
||||
setPending(false)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
const { run: handleRemove } = useDebounceFn(() => {
|
||||
removeTag(tag.id)
|
||||
}, { wait: 200 })
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('shrink-0 flex items-center gap-0.5 pr-1 pl-2 py-1 rounded-lg border border-gray-200 text-sm leading-5 text-gray-700')}>
|
||||
{!isEditing && (
|
||||
<>
|
||||
<div className='text-sm leading-5 text-gray-700'>
|
||||
{tag.name}
|
||||
</div>
|
||||
<div className='shrink-0 px-1 text-sm leading-4.5 text-gray-500 font-medium'>{tag.binding_count}</div>
|
||||
<div className='group/edit shrink-0 p-1 rounded-md cursor-pointer hover:bg-black/5' onClick={() => setIsEditing(true)}>
|
||||
<Edit03 className='w-3 h-3 text-gray-500 group-hover/edit:text-gray-800' />
|
||||
</div>
|
||||
<div className='group/remove shrink-0 p-1 rounded-md cursor-pointer hover:bg-black/5' onClick={() => {
|
||||
if (tag.binding_count)
|
||||
setShowRemoveModal(true)
|
||||
else
|
||||
handleRemove()
|
||||
}}>
|
||||
<Trash03 className='w-3 h-3 text-gray-500 group-hover/remove:text-gray-800' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isEditing && (
|
||||
<input
|
||||
className='shrink-0 outline-none appearance-none placeholder:text-gray-300 caret-primary-600'
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)}
|
||||
onBlur={() => editTag(tag.id, name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<TagRemoveModal
|
||||
tag={tag}
|
||||
show={showRemoveModal}
|
||||
onConfirm={() => {
|
||||
handleRemove()
|
||||
setShowRemoveModal(false)
|
||||
}}
|
||||
onClose={() => setShowRemoveModal(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagItemEditor
|
50
web/app/components/base/tag-management/tag-remove-modal.tsx
Normal file
50
web/app/components/base/tag-management/tag-remove-modal.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
'use client'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
|
||||
type TagRemoveModalProps = {
|
||||
show: boolean
|
||||
tag: Tag
|
||||
onConfirm: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const TagRemoveModal = ({ show, tag, onConfirm, onClose }: TagRemoveModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapperClassName='!z-[1020]'
|
||||
className={cn('p-8 max-w-[480px] w-[480px]', s.bg)}
|
||||
isShow={show}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
|
||||
<AlertTriangle className='w-6 h-6 text-[rgb(247,144,9)]' />
|
||||
</div>
|
||||
<div className='mt-3 text-xl font-semibold leading-[30px] text-gray-900'>
|
||||
{`${t('common.tag.delete')} `}
|
||||
<span>{`"${tag.name}"`}</span>
|
||||
</div>
|
||||
<div className='my-1 text-gray-500 text-sm leading-5'>
|
||||
{t('common.tag.deleteTip')}
|
||||
</div>
|
||||
<div className='pt-6 flex items-center justify-end'>
|
||||
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='text-sm font-medium border-red-700 border-[0.5px]' type="warning" onClick={onConfirm}>{t('common.operation.delete')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagRemoveModal
|
|
@ -5,7 +5,6 @@ import useSWR from 'swr'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { debounce, groupBy, omit } from 'lodash-es'
|
||||
// import Link from 'next/link'
|
||||
import { PlusIcon } from '@heroicons/react/24/solid'
|
||||
import List from './list'
|
||||
import s from './style.module.css'
|
||||
|
@ -20,7 +19,7 @@ import { NotionPageSelectorModal } from '@/app/components/base/notion-page-selec
|
|||
import type { NotionPage } from '@/models/common'
|
||||
import type { CreateDocumentReq } from '@/models/datasets'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
|
||||
import RetryButton from '@/app/components/base/retry-button'
|
||||
// Custom page count is not currently supported.
|
||||
const limit = 15
|
||||
|
||||
|
@ -198,7 +197,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
|||
<p className={s.desc}>{t('datasetDocuments.list.desc')}</p>
|
||||
</div>
|
||||
<div className='flex flex-col px-6 py-4 flex-1'>
|
||||
<div className='flex items-center justify-between flex-wrap gap-y-2 '>
|
||||
<div className='flex items-center justify-between flex-wrap'>
|
||||
<Input
|
||||
showPrefix
|
||||
wrapperClassName='!w-[200px]'
|
||||
|
@ -206,13 +205,16 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
|||
onChange={debounce(setSearchValue, 500)}
|
||||
value={searchValue}
|
||||
/>
|
||||
{embeddingAvailable && (
|
||||
<Button type='primary' onClick={routeToDocCreate} className='!h-8 !text-[13px] !shrink-0'>
|
||||
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
|
||||
{isDataSourceNotion && t('datasetDocuments.list.addPages')}
|
||||
{!isDataSourceNotion && t('datasetDocuments.list.addFile')}
|
||||
</Button>
|
||||
)}
|
||||
<div className='flex gap-2 justify-center items-center !h-8'>
|
||||
<RetryButton datasetId={datasetId} />
|
||||
{embeddingAvailable && (
|
||||
<Button type='primary' onClick={routeToDocCreate} className='!h-8 !text-[13px] !shrink-0'>
|
||||
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
|
||||
{isDataSourceNotion && t('datasetDocuments.list.addPages')}
|
||||
{!isDataSourceNotion && t('datasetDocuments.list.addFile')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading
|
||||
? <Loading type='app' />
|
||||
|
|
|
@ -332,7 +332,7 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
|
|||
<td className='w-12'>#</td>
|
||||
<td>{t('datasetDocuments.list.table.header.fileName')}</td>
|
||||
<td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
|
||||
<td className='w-24'>{t('datasetDocuments.list.table.header.hitCount')}</td>
|
||||
<td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
|
||||
<td className='w-44'>
|
||||
<div className='flex justify-between items-center'>
|
||||
{t('datasetDocuments.list.table.header.uploadTime')}
|
||||
|
|
106
web/app/components/datasets/rename-modal/index.tsx
Normal file
106
web/app/components/datasets/rename-modal/index.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
'use client'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useState } from 'react'
|
||||
import { BookOpenIcon } from '@heroicons/react/24/outline'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
|
||||
type RenameDatasetModalProps = {
|
||||
show: boolean
|
||||
dataset: DataSet
|
||||
onSuccess?: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDatasetModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [name, setName] = useState<string>(dataset.name)
|
||||
const [description, setDescription] = useState<string>(dataset.description)
|
||||
|
||||
const onConfirm: MouseEventHandler = async () => {
|
||||
if (!name.trim()) {
|
||||
notify({ type: 'error', message: t('datasetSettings.form.nameError') })
|
||||
return
|
||||
}
|
||||
try {
|
||||
setLoading(true)
|
||||
await updateDatasetSetting({
|
||||
datasetId: dataset.id,
|
||||
body: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
})
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
onClose()
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapperClassName='z-20'
|
||||
className='px-8 py-6 max-w-[520px] w-[520px] rounded-xl'
|
||||
isShow={show}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div className='relative pb-2 text-xl font-medium leading-[30px] text-gray-900'>{t('datasetSettings.title')}</div>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<div>
|
||||
<div className={cn('flex justify-between py-4 flex-wrap items-center')}>
|
||||
<div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-gray-900'>
|
||||
{t('datasetSettings.form.name')}
|
||||
</div>
|
||||
<input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
|
||||
placeholder={t('datasetSettings.form.namePlaceholder') || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('flex justify-between py-4 flex-wrap items-center')}>
|
||||
<div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-gray-900'>
|
||||
{t('datasetSettings.form.desc')}
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
|
||||
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
|
||||
/>
|
||||
<a className='mt-2 flex items-center h-[18px] px-3 text-xs text-gray-500 hover:text-primary-600' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>
|
||||
<BookOpenIcon className='w-3 h-[18px] mr-1' />
|
||||
{t('datasetSettings.form.descWrite')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pt-6 flex justify-end'>
|
||||
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='text-sm font-medium' disabled={loading} type="primary" onClick={onConfirm}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenameDatasetModal
|
|
@ -59,7 +59,17 @@ const Form = () => {
|
|||
const [permission, setPermission] = useState(currentDataset?.permission)
|
||||
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
|
||||
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
|
||||
|
||||
const [embeddingModel, setEmbeddingModel] = useState<DefaultModel>(
|
||||
currentDataset?.embedding_model
|
||||
? {
|
||||
provider: currentDataset.embedding_model_provider,
|
||||
model: currentDataset.embedding_model,
|
||||
}
|
||||
: {
|
||||
provider: '',
|
||||
model: '',
|
||||
},
|
||||
)
|
||||
const {
|
||||
modelList: rerankModelList,
|
||||
defaultModel: rerankDefaultModel,
|
||||
|
@ -101,6 +111,8 @@ const Form = () => {
|
|||
permission,
|
||||
indexing_technique: indexMethod,
|
||||
retrieval_model: postRetrievalConfig,
|
||||
embedding_model: embeddingModel.model,
|
||||
embedding_model_provider: embeddingModel.provider,
|
||||
},
|
||||
})
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
|
@ -189,18 +201,13 @@ const Form = () => {
|
|||
</div>
|
||||
<div className='w-[480px]'>
|
||||
<ModelSelector
|
||||
readonly
|
||||
triggerClassName='!h-9 !cursor-not-allowed opacity-60'
|
||||
defaultModel={{
|
||||
provider: currentDataset.embedding_model_provider,
|
||||
model: currentDataset.embedding_model,
|
||||
}}
|
||||
triggerClassName=''
|
||||
defaultModel={embeddingModel}
|
||||
modelList={embeddingModelList}
|
||||
onSelect={(model: DefaultModel) => {
|
||||
setEmbeddingModel(model)
|
||||
}}
|
||||
/>
|
||||
<div className='mt-2 w-full text-xs leading-6 text-gray-500'>
|
||||
{t('datasetSettings.form.embeddingModelTip')}
|
||||
<span className='text-[#155eef] cursor-pointer' onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'Protokoll',
|
||||
learnMore: 'Mehr erfahren',
|
||||
params: 'Parameter',
|
||||
duplicate: 'Duplikat',
|
||||
rename: 'Umbenennen',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Bitte eingeben',
|
||||
|
@ -500,6 +502,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: 'Ungültiger Bildlink',
|
||||
imageUpload: 'Bild-Upload',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'Alle Tags',
|
||||
addNew: 'Neues Tag hinzufügen',
|
||||
noTag: 'Keine Tags',
|
||||
noTagYet: 'Noch keine Tags',
|
||||
addTag: 'Tags hinzufügen',
|
||||
editTag: 'Tags bearbeiten',
|
||||
manageTags: 'Tags verwalten',
|
||||
selectorPlaceholder: 'Typ zum Suchen oder Erstellen',
|
||||
create: 'Erstellen',
|
||||
delete: 'Tag löschen',
|
||||
deleteTip: 'Das Tag wird verwendet, löschen?',
|
||||
created: 'Tag erfolgreich erstellt',
|
||||
failed: 'Tag-Erstellung fehlgeschlagen',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: 'Wissen',
|
||||
documentCount: ' Dokumente',
|
||||
wordCount: 'k Wörter',
|
||||
wordCount: ' k Wörter',
|
||||
appCount: ' verknüpfte Apps',
|
||||
createDataset: 'Wissen erstellen',
|
||||
createDatasetIntro: 'Importiere deine eigenen Textdaten oder schreibe Daten in Echtzeit über Webhook für die LLM-Kontextverbesserung.',
|
||||
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: 'Ändern',
|
||||
changeRetrievalMethod: 'Abfragemethode ändern',
|
||||
},
|
||||
docsFailedNotice: 'Dokumente konnten nicht indiziert werden',
|
||||
retry: 'Wiederholen',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'Log',
|
||||
learnMore: 'Learn More',
|
||||
params: 'Params',
|
||||
duplicate: 'Duplicate',
|
||||
rename: 'Rename',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Please enter',
|
||||
|
@ -505,6 +507,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: 'Invalid image link',
|
||||
imageUpload: 'Image Upload',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'All Tags',
|
||||
addNew: 'Add new tag',
|
||||
noTag: 'No tags',
|
||||
noTagYet: 'No tags yet',
|
||||
addTag: 'Add tags',
|
||||
editTag: 'Edit tags',
|
||||
manageTags: 'Manage Tags',
|
||||
selectorPlaceholder: 'Type to search or create',
|
||||
create: 'Create',
|
||||
delete: 'Delete tag',
|
||||
deleteTip: 'The tag is being used, delete it?',
|
||||
created: 'Tag created successfully',
|
||||
failed: 'Tag creation failed',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: 'Knowledge',
|
||||
documentCount: ' docs',
|
||||
wordCount: 'k words',
|
||||
wordCount: ' k words',
|
||||
appCount: ' linked apps',
|
||||
createDataset: 'Create Knowledge',
|
||||
createDatasetIntro: 'Import your own text data or write data in real-time via Webhook for LLM context enhancement.',
|
||||
|
@ -20,7 +20,7 @@ const translation = {
|
|||
unavailable: 'Unavailable',
|
||||
unavailableTip: 'Embedding model is not available, the default embedding model needs to be configured',
|
||||
datasets: 'KNOWLEDGE',
|
||||
datasetsApi: 'API',
|
||||
datasetsApi: 'API ACCESS',
|
||||
retrieval: {
|
||||
semantic_search: {
|
||||
title: 'Vector Search',
|
||||
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: 'Change',
|
||||
changeRetrievalMethod: 'Change retrieval method',
|
||||
},
|
||||
docsFailedNotice: 'documents failed to be indexed',
|
||||
retry: 'Retry',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'Journal',
|
||||
learnMore: 'En savoir plus',
|
||||
params: 'Paramètres',
|
||||
duplicate: 'Dupliquer',
|
||||
rename: 'Renommer',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Veuillez entrer',
|
||||
|
@ -505,6 +507,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: 'Lien d\'image invalide',
|
||||
imageUpload: 'Téléchargement d\'image',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'Toutes les balises',
|
||||
addNew: 'Ajouter une nouvelle balise',
|
||||
noTag: 'Aucune balise',
|
||||
noTagYet: 'Aucune balise pour l\'instant',
|
||||
addTag: 'ajouter une balise',
|
||||
editTag: 'Modifier les balises',
|
||||
manageTags: 'Gérer les balises',
|
||||
selectorPlaceholder: 'Type de recherche ou de création',
|
||||
create: 'Créer',
|
||||
delete: 'Supprimer la balise',
|
||||
deleteTip: 'Le tag est utilisé, le supprimer ?',
|
||||
created: 'Tag créé avec succès',
|
||||
failed: 'La création de la balise a échoué',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: 'Connaissance',
|
||||
documentCount: ' documents',
|
||||
wordCount: 'k mots',
|
||||
wordCount: ' k mots',
|
||||
appCount: ' applications liées',
|
||||
createDataset: 'Créer des Connaissances',
|
||||
createDatasetIntro: 'Importez vos propres données textuelles ou écrivez des données en temps réel via Webhook pour l\'amélioration du contexte LLM.',
|
||||
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: 'Changer',
|
||||
changeRetrievalMethod: 'Changer la méthode de récupération',
|
||||
},
|
||||
docsFailedNotice: 'Les documents n\'ont pas pu être indexés',
|
||||
retry: 'Réessayer',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'ログ',
|
||||
learnMore: '詳細を見る',
|
||||
params: 'パラメータ',
|
||||
duplicate: '重複',
|
||||
rename: '名前の変更',
|
||||
},
|
||||
placeholder: {
|
||||
input: '入力してください',
|
||||
|
@ -456,6 +458,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: '無効な画像リンク',
|
||||
imageUpload: '画像アップロード',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'すべてのタグ',
|
||||
addNew: '新しいタグを追加',
|
||||
noTag: 'タグなし',
|
||||
noTagYet: 'まだタグがありません',
|
||||
addTag: 'タグを追加',
|
||||
editTag: 'タグを編集',
|
||||
manageTags: 'タグの管理',
|
||||
selectorPlaceholder: '検索または作成する文字を入力',
|
||||
create: '作成',
|
||||
delete: 'タグを削除',
|
||||
deleteTip: 'タグは使用されています、削除しますか',
|
||||
created: 'タグは正常に作成されました',
|
||||
failed: 'タグの作成に失敗しました',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: '知識',
|
||||
documentCount: ' ドキュメント',
|
||||
wordCount: 'k 単語',
|
||||
wordCount: ' k 単語',
|
||||
appCount: ' リンクされたアプリ',
|
||||
createDataset: '知識を作成',
|
||||
createDatasetIntro: '独自のテキストデータをインポートするか、LLMコンテキストの強化のためにWebhookを介してリアルタイムでデータを書き込むことができます。',
|
||||
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: '変更',
|
||||
changeRetrievalMethod: '検索方法の変更',
|
||||
},
|
||||
docsFailedNotice: 'ドキュメントのインデックスに失敗しました',
|
||||
retry: '再試行',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'Log',
|
||||
learnMore: 'Saiba Mais',
|
||||
params: 'Parâmetros',
|
||||
duplicate: 'Duplicada',
|
||||
rename: 'Renomear',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Por favor, insira',
|
||||
|
@ -505,6 +507,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: 'Link da imagem inválido',
|
||||
imageUpload: 'Enviar Imagem',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'Todas as tags',
|
||||
addNew: 'Adicionar nova tag',
|
||||
noTag: 'Sem tags',
|
||||
noTagYet: 'Nenhuma tag ainda',
|
||||
addTag: 'adicionar etiqueta',
|
||||
editTag: 'Editar tags',
|
||||
manageTags: 'Gerenciar tags',
|
||||
selectorPlaceholder: 'Digite para pesquisar ou criar',
|
||||
create: 'Criar',
|
||||
delete: 'Excluir etiqueta',
|
||||
deleteTip: 'A tag está sendo usada, excluí-la?',
|
||||
created: 'Tag criada com sucesso',
|
||||
failed: 'Falha na criação da tag',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: 'Wiedza',
|
||||
documentCount: ' documentos',
|
||||
wordCount: 'k palavras',
|
||||
wordCount: ' k palavras',
|
||||
appCount: ' aplicativos vinculados',
|
||||
createDataset: 'Criar Conhecimento',
|
||||
createDatasetIntro: 'Importe seus próprios dados de texto ou escreva dados em tempo real via Webhook para aprimoramento de contexto LLM.',
|
||||
|
@ -41,6 +41,8 @@ const translation = {
|
|||
change: 'Alterar',
|
||||
changeRetrievalMethod: 'Alterar método de recuperação',
|
||||
},
|
||||
docsFailedNotice: 'documentos falharam ao serem indexados',
|
||||
retry: 'Tentar novamente',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'Журнал',
|
||||
learnMore: 'Дізнатися більше',
|
||||
params: 'Параметри',
|
||||
duplicate: 'дублікат',
|
||||
rename: 'Перейменувати',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Будь ласка, введіть текст',
|
||||
|
@ -505,6 +507,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: 'Недійсне посилання на зображення',
|
||||
imageUpload: 'Завантаження зображення',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'Усі теги',
|
||||
addNew: 'Додати новий тег',
|
||||
noTag: 'Без тегів',
|
||||
noTagYet: 'Ще немає тегів',
|
||||
addTag: 'додати тег',
|
||||
editTag: 'Редагувати теги',
|
||||
manageTags: 'Керувати тегами',
|
||||
selectorPlaceholder: 'Введіть для пошуку або створення',
|
||||
create: 'Створити',
|
||||
delete: 'Видалити тег',
|
||||
deleteTip: 'Тег використовується, видалити його?',
|
||||
created: 'Тег створено успішно',
|
||||
failed: 'Не вдалося створити тег',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: 'Змінити',
|
||||
changeRetrievalMethod: 'Змінити метод пошуку',
|
||||
},
|
||||
docsFailedNotice: 'документи не вдалося проіндексувати',
|
||||
retry: 'Повторити спробу',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: 'Nhật ký',
|
||||
learnMore: 'Tìm hiểu thêm',
|
||||
params: 'Tham số',
|
||||
duplicate: 'Nhân bản',
|
||||
rename: 'Đổi tên',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Vui lòng nhập',
|
||||
|
@ -505,6 +507,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: 'Liên kết ảnh không hợp lệ',
|
||||
imageUpload: 'Tải ảnh lên',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'Tất cả các thẻ',
|
||||
addNew: 'Thêm thẻ mới',
|
||||
noTag: 'Không có thẻ',
|
||||
noTagYet: 'Chưa có thẻ',
|
||||
addTag: 'thêm thẻ',
|
||||
editTag: 'Chỉnh sửa thẻ',
|
||||
manageTags: 'Quản lý thẻ',
|
||||
selectorPlaceholder: 'Nhập để tìm kiếm hoặc tạo',
|
||||
create: 'Tạo',
|
||||
delete: 'Xóa thẻ',
|
||||
deleteTip: 'Thẻ đang được sử dụng, xóa nó đi?',
|
||||
created: 'Thẻ được tạo thành công',
|
||||
failed: 'Tạo thẻ không thành công',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: 'Kiến thức',
|
||||
documentCount: ' tài liệu',
|
||||
wordCount: 'k từ',
|
||||
wordCount: ' k từ',
|
||||
appCount: ' ứng dụng liên kết',
|
||||
createDataset: 'Tạo Kiến thức',
|
||||
createDatasetIntro: 'Nhập dữ liệu văn bản của bạn hoặc viết dữ liệu theo thời gian thực qua Webhook để tăng cường ngữ cảnh LLM.',
|
||||
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: 'Thay đổi',
|
||||
changeRetrievalMethod: 'Thay đổi phương pháp truy xuất',
|
||||
},
|
||||
docsFailedNotice: 'tài liệu không được lập chỉ mục',
|
||||
retry: 'Thử lại',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -34,6 +34,8 @@ const translation = {
|
|||
log: '日志',
|
||||
learnMore: '了解更多',
|
||||
params: '参数设置',
|
||||
duplicate: '复制',
|
||||
rename: '重命名',
|
||||
},
|
||||
placeholder: {
|
||||
input: '请输入',
|
||||
|
@ -505,6 +507,21 @@ const translation = {
|
|||
pasteImageLinkInvalid: '图片链接无效',
|
||||
imageUpload: '图片上传',
|
||||
},
|
||||
tag: {
|
||||
placeholder: '全部标签',
|
||||
addNew: '创建新标签',
|
||||
noTag: '没有标签',
|
||||
noTagYet: '还没有标签',
|
||||
addTag: '添加标签',
|
||||
editTag: '修改标签',
|
||||
manageTags: '管理标签',
|
||||
selectorPlaceholder: '搜索或者创建',
|
||||
create: '创建',
|
||||
delete: '删除标签',
|
||||
deleteTip: '标签正在使用中,是否删除?',
|
||||
created: '标签创建成功',
|
||||
failed: '标签创建失败',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const translation = {
|
||||
knowledge: '知识库',
|
||||
documentCount: ' 文档',
|
||||
wordCount: '千字符',
|
||||
wordCount: ' 千字符',
|
||||
appCount: ' 关联应用',
|
||||
createDataset: '创建知识库',
|
||||
createDatasetIntro: '导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。',
|
||||
|
@ -42,6 +42,8 @@ const translation = {
|
|||
change: '更改',
|
||||
changeRetrievalMethod: '更改检索方法',
|
||||
},
|
||||
docsFailedNotice: '文档无法被索引',
|
||||
retry: '重试',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { DataSourceNotionPage } from './common'
|
||||
import type { AppMode, RetrievalConfig } from '@/types/app'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
|
||||
export enum DataSourceType {
|
||||
FILE = 'upload_file',
|
||||
|
@ -27,6 +28,7 @@ export type DataSet = {
|
|||
embedding_available: boolean
|
||||
retrieval_model_dict: RetrievalConfig
|
||||
retrieval_model: RetrievalConfig
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
export type CustomFile = File & {
|
||||
|
@ -410,3 +412,8 @@ export enum DocForm {
|
|||
TEXT = 'text_model',
|
||||
QA = 'qa_model',
|
||||
}
|
||||
|
||||
export type ErrorDocsResponse = {
|
||||
data: IndexingStatusResponse[]
|
||||
total: number
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import type {
|
|||
DataSetListResponse,
|
||||
DocumentDetailResponse,
|
||||
DocumentListResponse,
|
||||
ErrorDocsResponse,
|
||||
FileIndexingEstimateResponse,
|
||||
HitTestingRecordsResponse,
|
||||
HitTestingResponse,
|
||||
|
@ -49,7 +50,12 @@ export const fetchDatasetDetail: Fetcher<DataSet, string> = (datasetId: string)
|
|||
return get<DataSet>(`/datasets/${datasetId}`)
|
||||
}
|
||||
|
||||
export const updateDatasetSetting: Fetcher<DataSet, { datasetId: string; body: Partial<Pick<DataSet, 'name' | 'description' | 'permission' | 'indexing_technique' | 'retrieval_model'>> }> = ({ datasetId, body }) => {
|
||||
export const updateDatasetSetting: Fetcher<DataSet, {
|
||||
datasetId: string
|
||||
body: Partial<Pick<DataSet,
|
||||
'name' | 'description' | 'permission' | 'indexing_technique' | 'retrieval_model' | 'embedding_model' | 'embedding_model_provider'
|
||||
>>
|
||||
}> = ({ datasetId, body }) => {
|
||||
return patch<DataSet>(`/datasets/${datasetId}`, { body })
|
||||
}
|
||||
|
||||
|
@ -222,3 +228,11 @@ type FileTypesRes = {
|
|||
export const fetchSupportFileTypes: Fetcher<FileTypesRes, { url: string }> = ({ url }) => {
|
||||
return get<FileTypesRes>(url)
|
||||
}
|
||||
|
||||
export const getErrorDocs: Fetcher<ErrorDocsResponse, { datasetId: string }> = ({ datasetId }) => {
|
||||
return get<ErrorDocsResponse>(`/datasets/${datasetId}/error-docs`)
|
||||
}
|
||||
|
||||
export const retryErrorDocs: Fetcher<CommonResponse, { datasetId: string; document_ids: string[] }> = ({ datasetId, document_ids }) => {
|
||||
return post<CommonResponse>(`/datasets/${datasetId}/retry`, { body: { document_ids } })
|
||||
}
|
||||
|
|
47
web/service/tag.ts
Normal file
47
web/service/tag.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { del, get, patch, post } from './base'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
|
||||
export const fetchTagList = (type: string) => {
|
||||
return get<Tag[]>('/tags', { params: { type } })
|
||||
}
|
||||
|
||||
export const createTag = (name: string, type: string) => {
|
||||
return post<Tag>('/tags', {
|
||||
body: {
|
||||
name,
|
||||
type,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTag = (tagID: string, name: string) => {
|
||||
return patch(`/tags/${tagID}`, {
|
||||
body: {
|
||||
name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteTag = (tagID: string) => {
|
||||
return del(`/tags/${tagID}`)
|
||||
}
|
||||
|
||||
export const bindTag = (tagIDList: string[], targetID: string, type: string) => {
|
||||
return post('/tag-bindings/create', {
|
||||
body: {
|
||||
tag_ids: tagIDList,
|
||||
target_id: targetID,
|
||||
type,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const unBindTag = (tagID: string, targetID: string, type: string) => {
|
||||
return post('/tag-bindings/remove', {
|
||||
body: {
|
||||
tag_id: tagID,
|
||||
target_id: targetID,
|
||||
type,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import type { AnnotationReplyConfig, ChatPromptConfig, CompletionPromptConfig, DatasetConfigs, PromptMode } from '@/models/debug'
|
||||
import type { CollectionType } from '@/app/components/tools/types'
|
||||
import type { LanguagesSupported } from '@/i18n/language'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
|
||||
export enum ProviderType {
|
||||
openai = 'openai',
|
||||
anthropic = 'anthropic',
|
||||
|
@ -309,6 +311,7 @@ export type App = {
|
|||
site: SiteConfig
|
||||
/** api site url */
|
||||
api_base_url: string
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user