Knowledge optimization (#3755)
Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: JzoNg <jzongcode@gmail.com>pull/3763/head
parent
3cd8e6f5c6
commit
f257f2c396
@ -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')
|
||||||
@ -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 ###
|
||||||
@ -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")
|
||||||
|
|
||||||
@ -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
|
||||||
@ -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'))
|
||||||
@ -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 |
@ -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 CoinsStacked01 } from './CoinsStacked01'
|
||||||
export { default as GoldCoin } from './GoldCoin'
|
export { default as GoldCoin } from './GoldCoin'
|
||||||
export { default as ReceiptList } from './ReceiptList'
|
export { default as ReceiptList } from './ReceiptList'
|
||||||
|
export { default as Tag01 } from './Tag01'
|
||||||
|
export { default as Tag03 } from './Tag03'
|
||||||
|
|||||||
@ -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
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
export type Tag = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
binding_count: number
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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 })),
|
||||||
|
}))
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue