Feat/dataset notion import (#392)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: JzoNg <jzongcode@gmail.com>pull/394/head
@ -0,0 +1,95 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import flask_login
|
||||
import requests
|
||||
from flask import request, redirect, current_app, session
|
||||
from flask_login import current_user, login_required
|
||||
from flask_restful import Resource
|
||||
from werkzeug.exceptions import Forbidden
|
||||
from libs.oauth_data_source import NotionOAuth
|
||||
from controllers.console import api
|
||||
from ..setup import setup_required
|
||||
from ..wraps import account_initialization_required
|
||||
|
||||
|
||||
def get_oauth_providers():
|
||||
with current_app.app_context():
|
||||
notion_oauth = NotionOAuth(client_id=current_app.config.get('NOTION_CLIENT_ID'),
|
||||
client_secret=current_app.config.get(
|
||||
'NOTION_CLIENT_SECRET'),
|
||||
redirect_uri=current_app.config.get(
|
||||
'CONSOLE_URL') + '/console/api/oauth/data-source/callback/notion')
|
||||
|
||||
OAUTH_PROVIDERS = {
|
||||
'notion': notion_oauth
|
||||
}
|
||||
return OAUTH_PROVIDERS
|
||||
|
||||
|
||||
class OAuthDataSource(Resource):
|
||||
def get(self, provider: str):
|
||||
# The role of the current user in the table must be admin or owner
|
||||
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||
raise Forbidden()
|
||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||
with current_app.app_context():
|
||||
oauth_provider = OAUTH_DATASOURCE_PROVIDERS.get(provider)
|
||||
print(vars(oauth_provider))
|
||||
if not oauth_provider:
|
||||
return {'error': 'Invalid provider'}, 400
|
||||
|
||||
auth_url = oauth_provider.get_authorization_url()
|
||||
return redirect(auth_url)
|
||||
|
||||
|
||||
class OAuthDataSourceCallback(Resource):
|
||||
def get(self, provider: str):
|
||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||
with current_app.app_context():
|
||||
oauth_provider = OAUTH_DATASOURCE_PROVIDERS.get(provider)
|
||||
if not oauth_provider:
|
||||
return {'error': 'Invalid provider'}, 400
|
||||
if 'code' in request.args:
|
||||
code = request.args.get('code')
|
||||
try:
|
||||
oauth_provider.get_access_token(code)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logging.exception(
|
||||
f"An error occurred during the OAuthCallback process with {provider}: {e.response.text}")
|
||||
return {'error': 'OAuth data source process failed'}, 400
|
||||
|
||||
return redirect(f'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=success')
|
||||
elif 'error' in request.args:
|
||||
error = request.args.get('error')
|
||||
return redirect(f'{current_app.config.get("CONSOLE_URL")}?oauth_data_source={error}')
|
||||
else:
|
||||
return redirect(f'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=access_denied')
|
||||
|
||||
|
||||
class OAuthDataSourceSync(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, provider, binding_id):
|
||||
provider = str(provider)
|
||||
binding_id = str(binding_id)
|
||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||
with current_app.app_context():
|
||||
oauth_provider = OAUTH_DATASOURCE_PROVIDERS.get(provider)
|
||||
if not oauth_provider:
|
||||
return {'error': 'Invalid provider'}, 400
|
||||
try:
|
||||
oauth_provider.sync_data_source(binding_id)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logging.exception(
|
||||
f"An error occurred during the OAuthCallback process with {provider}: {e.response.text}")
|
||||
return {'error': 'OAuth data source process failed'}, 400
|
||||
|
||||
return {'result': 'success'}, 200
|
||||
|
||||
|
||||
api.add_resource(OAuthDataSource, '/oauth/data-source/<string:provider>')
|
||||
api.add_resource(OAuthDataSourceCallback, '/oauth/data-source/callback/<string:provider>')
|
||||
api.add_resource(OAuthDataSourceSync, '/oauth/data-source/<string:provider>/<uuid:binding_id>/sync')
|
||||
@ -0,0 +1,303 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from cachetools import TTLCache
|
||||
from flask import request, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from flask_restful import Resource, marshal_with, fields, reqparse, marshal
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from core.data_source.notion import NotionPageReader
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import TimestampField
|
||||
from libs.oauth_data_source import NotionOAuth
|
||||
from models.dataset import Document
|
||||
from models.source import DataSourceBinding
|
||||
from services.dataset_service import DatasetService, DocumentService
|
||||
from tasks.document_indexing_sync_task import document_indexing_sync_task
|
||||
|
||||
cache = TTLCache(maxsize=None, ttl=30)
|
||||
|
||||
FILE_SIZE_LIMIT = 15 * 1024 * 1024 # 15MB
|
||||
ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm']
|
||||
PREVIEW_WORDS_LIMIT = 3000
|
||||
|
||||
|
||||
class DataSourceApi(Resource):
|
||||
integrate_icon_fields = {
|
||||
'type': fields.String,
|
||||
'url': fields.String,
|
||||
'emoji': fields.String
|
||||
}
|
||||
integrate_page_fields = {
|
||||
'page_name': fields.String,
|
||||
'page_id': fields.String,
|
||||
'page_icon': fields.Nested(integrate_icon_fields, allow_null=True),
|
||||
'parent_id': fields.String,
|
||||
'type': fields.String
|
||||
}
|
||||
integrate_workspace_fields = {
|
||||
'workspace_name': fields.String,
|
||||
'workspace_id': fields.String,
|
||||
'workspace_icon': fields.String,
|
||||
'pages': fields.List(fields.Nested(integrate_page_fields)),
|
||||
'total': fields.Integer
|
||||
}
|
||||
integrate_fields = {
|
||||
'id': fields.String,
|
||||
'provider': fields.String,
|
||||
'created_at': TimestampField,
|
||||
'is_bound': fields.Boolean,
|
||||
'disabled': fields.Boolean,
|
||||
'link': fields.String,
|
||||
'source_info': fields.Nested(integrate_workspace_fields)
|
||||
}
|
||||
integrate_list_fields = {
|
||||
'data': fields.List(fields.Nested(integrate_fields)),
|
||||
}
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(integrate_list_fields)
|
||||
def get(self):
|
||||
# get workspace data source integrates
|
||||
data_source_integrates = db.session.query(DataSourceBinding).filter(
|
||||
DataSourceBinding.tenant_id == current_user.current_tenant_id,
|
||||
DataSourceBinding.disabled == False
|
||||
).all()
|
||||
|
||||
base_url = request.url_root.rstrip('/')
|
||||
data_source_oauth_base_path = "/console/api/oauth/data-source"
|
||||
providers = ["notion"]
|
||||
|
||||
integrate_data = []
|
||||
for provider in providers:
|
||||
# existing_integrate = next((ai for ai in data_source_integrates if ai.provider == provider), None)
|
||||
existing_integrates = filter(lambda item: item.provider == provider, data_source_integrates)
|
||||
if existing_integrates:
|
||||
for existing_integrate in list(existing_integrates):
|
||||
integrate_data.append({
|
||||
'id': existing_integrate.id,
|
||||
'provider': provider,
|
||||
'created_at': existing_integrate.created_at,
|
||||
'is_bound': True,
|
||||
'disabled': existing_integrate.disabled,
|
||||
'source_info': existing_integrate.source_info,
|
||||
'link': f'{base_url}{data_source_oauth_base_path}/{provider}'
|
||||
})
|
||||
else:
|
||||
integrate_data.append({
|
||||
'id': None,
|
||||
'provider': provider,
|
||||
'created_at': None,
|
||||
'source_info': None,
|
||||
'is_bound': False,
|
||||
'disabled': None,
|
||||
'link': f'{base_url}{data_source_oauth_base_path}/{provider}'
|
||||
})
|
||||
return {'data': integrate_data}, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def patch(self, binding_id, action):
|
||||
binding_id = str(binding_id)
|
||||
action = str(action)
|
||||
data_source_binding = DataSourceBinding.query.filter_by(
|
||||
id=binding_id
|
||||
).first()
|
||||
if data_source_binding is None:
|
||||
raise NotFound('Data source binding not found.')
|
||||
# enable binding
|
||||
if action == 'enable':
|
||||
if data_source_binding.disabled:
|
||||
data_source_binding.disabled = False
|
||||
data_source_binding.updated_at = datetime.datetime.utcnow()
|
||||
db.session.add(data_source_binding)
|
||||
db.session.commit()
|
||||
else:
|
||||
raise ValueError('Data source is not disabled.')
|
||||
# disable binding
|
||||
if action == 'disable':
|
||||
if not data_source_binding.disabled:
|
||||
data_source_binding.disabled = True
|
||||
data_source_binding.updated_at = datetime.datetime.utcnow()
|
||||
db.session.add(data_source_binding)
|
||||
db.session.commit()
|
||||
else:
|
||||
raise ValueError('Data source is disabled.')
|
||||
return {'result': 'success'}, 200
|
||||
|
||||
|
||||
class DataSourceNotionListApi(Resource):
|
||||
integrate_icon_fields = {
|
||||
'type': fields.String,
|
||||
'url': fields.String,
|
||||
'emoji': fields.String
|
||||
}
|
||||
integrate_page_fields = {
|
||||
'page_name': fields.String,
|
||||
'page_id': fields.String,
|
||||
'page_icon': fields.Nested(integrate_icon_fields, allow_null=True),
|
||||
'is_bound': fields.Boolean,
|
||||
'parent_id': fields.String,
|
||||
'type': fields.String
|
||||
}
|
||||
integrate_workspace_fields = {
|
||||
'workspace_name': fields.String,
|
||||
'workspace_id': fields.String,
|
||||
'workspace_icon': fields.String,
|
||||
'pages': fields.List(fields.Nested(integrate_page_fields))
|
||||
}
|
||||
integrate_notion_info_list_fields = {
|
||||
'notion_info': fields.List(fields.Nested(integrate_workspace_fields)),
|
||||
}
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(integrate_notion_info_list_fields)
|
||||
def get(self):
|
||||
dataset_id = request.args.get('dataset_id', default=None, type=str)
|
||||
exist_page_ids = []
|
||||
# import notion in the exist dataset
|
||||
if dataset_id:
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
if not dataset:
|
||||
raise NotFound('Dataset not found.')
|
||||
if dataset.data_source_type != 'notion_import':
|
||||
raise ValueError('Dataset is not notion type.')
|
||||
documents = Document.query.filter_by(
|
||||
dataset_id=dataset_id,
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
data_source_type='notion_import',
|
||||
enabled=True
|
||||
).all()
|
||||
if documents:
|
||||
for document in documents:
|
||||
data_source_info = json.loads(document.data_source_info)
|
||||
exist_page_ids.append(data_source_info['notion_page_id'])
|
||||
# get all authorized pages
|
||||
data_source_bindings = DataSourceBinding.query.filter_by(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
provider='notion',
|
||||
disabled=False
|
||||
).all()
|
||||
if not data_source_bindings:
|
||||
return {
|
||||
'notion_info': []
|
||||
}, 200
|
||||
pre_import_info_list = []
|
||||
for data_source_binding in data_source_bindings:
|
||||
source_info = data_source_binding.source_info
|
||||
pages = source_info['pages']
|
||||
# Filter out already bound pages
|
||||
for page in pages:
|
||||
if page['page_id'] in exist_page_ids:
|
||||
page['is_bound'] = True
|
||||
else:
|
||||
page['is_bound'] = False
|
||||
pre_import_info = {
|
||||
'workspace_name': source_info['workspace_name'],
|
||||
'workspace_icon': source_info['workspace_icon'],
|
||||
'workspace_id': source_info['workspace_id'],
|
||||
'pages': pages,
|
||||
}
|
||||
pre_import_info_list.append(pre_import_info)
|
||||
return {
|
||||
'notion_info': pre_import_info_list
|
||||
}, 200
|
||||
|
||||
|
||||
class DataSourceNotionApi(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, workspace_id, page_id, page_type):
|
||||
workspace_id = str(workspace_id)
|
||||
page_id = str(page_id)
|
||||
data_source_binding = DataSourceBinding.query.filter(
|
||||
db.and_(
|
||||
DataSourceBinding.tenant_id == current_user.current_tenant_id,
|
||||
DataSourceBinding.provider == 'notion',
|
||||
DataSourceBinding.disabled == False,
|
||||
DataSourceBinding.source_info['workspace_id'] == f'"{workspace_id}"'
|
||||
)
|
||||
).first()
|
||||
if not data_source_binding:
|
||||
raise NotFound('Data source binding not found.')
|
||||
reader = NotionPageReader(integration_token=data_source_binding.access_token)
|
||||
if page_type == 'page':
|
||||
page_content = reader.read_page(page_id)
|
||||
elif page_type == 'database':
|
||||
page_content = reader.query_database_data(page_id)
|
||||
else:
|
||||
page_content = ""
|
||||
return {
|
||||
'content': page_content
|
||||
}, 200
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('notion_info_list', type=list, required=True, nullable=True, location='json')
|
||||
parser.add_argument('process_rule', type=dict, required=True, nullable=True, location='json')
|
||||
args = parser.parse_args()
|
||||
# validate args
|
||||
DocumentService.estimate_args_validate(args)
|
||||
indexing_runner = IndexingRunner()
|
||||
response = indexing_runner.notion_indexing_estimate(args['notion_info_list'], args['process_rule'])
|
||||
return response, 200
|
||||
|
||||
|
||||
class DataSourceNotionDatasetSyncApi(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.")
|
||||
|
||||
documents = DocumentService.get_document_by_dataset_id(dataset_id_str)
|
||||
for document in documents:
|
||||
document_indexing_sync_task.delay(dataset_id_str, document.id)
|
||||
return 200
|
||||
|
||||
|
||||
class DataSourceNotionDocumentSyncApi(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, dataset_id, document_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
|
||||
document = DocumentService.get_document(dataset_id_str, document_id_str)
|
||||
if document is None:
|
||||
raise NotFound("Document not found.")
|
||||
document_indexing_sync_task.delay(dataset_id_str, document_id_str)
|
||||
return 200
|
||||
|
||||
|
||||
api.add_resource(DataSourceApi, '/data-source/integrates', '/data-source/integrates/<uuid:binding_id>/<string:action>')
|
||||
api.add_resource(DataSourceNotionListApi, '/notion/pre-import/pages')
|
||||
api.add_resource(DataSourceNotionApi,
|
||||
'/notion/workspaces/<uuid:workspace_id>/pages/<uuid:page_id>/<string:page_type>/preview',
|
||||
'/datasets/notion-indexing-estimate')
|
||||
api.add_resource(DataSourceNotionDatasetSyncApi, '/datasets/<uuid:dataset_id>/notion/sync')
|
||||
api.add_resource(DataSourceNotionDocumentSyncApi, '/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/notion/sync')
|
||||
@ -0,0 +1,367 @@
|
||||
"""Notion reader."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests # type: ignore
|
||||
|
||||
from llama_index.readers.base import BaseReader
|
||||
from llama_index.readers.schema.base import Document
|
||||
|
||||
INTEGRATION_TOKEN_NAME = "NOTION_INTEGRATION_TOKEN"
|
||||
BLOCK_CHILD_URL_TMPL = "https://api.notion.com/v1/blocks/{block_id}/children"
|
||||
DATABASE_URL_TMPL = "https://api.notion.com/v1/databases/{database_id}/query"
|
||||
SEARCH_URL = "https://api.notion.com/v1/search"
|
||||
RETRIEVE_PAGE_URL_TMPL = "https://api.notion.com/v1/pages/{page_id}"
|
||||
RETRIEVE_DATABASE_URL_TMPL = "https://api.notion.com/v1/databases/{database_id}"
|
||||
HEADING_TYPE = ['heading_1', 'heading_2', 'heading_3']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: Notion DB reader coming soon!
|
||||
class NotionPageReader(BaseReader):
|
||||
"""Notion Page reader.
|
||||
|
||||
Reads a set of Notion pages.
|
||||
|
||||
Args:
|
||||
integration_token (str): Notion integration token.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, integration_token: Optional[str] = None) -> None:
|
||||
"""Initialize with parameters."""
|
||||
if integration_token is None:
|
||||
integration_token = os.getenv(INTEGRATION_TOKEN_NAME)
|
||||
if integration_token is None:
|
||||
raise ValueError(
|
||||
"Must specify `integration_token` or set environment "
|
||||
"variable `NOTION_INTEGRATION_TOKEN`."
|
||||
)
|
||||
self.token = integration_token
|
||||
self.headers = {
|
||||
"Authorization": "Bearer " + self.token,
|
||||
"Content-Type": "application/json",
|
||||
"Notion-Version": "2022-06-28",
|
||||
}
|
||||
|
||||
def _read_block(self, block_id: str, num_tabs: int = 0) -> str:
|
||||
"""Read a block."""
|
||||
done = False
|
||||
result_lines_arr = []
|
||||
cur_block_id = block_id
|
||||
while not done:
|
||||
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id)
|
||||
query_dict: Dict[str, Any] = {}
|
||||
|
||||
res = requests.request(
|
||||
"GET", block_url, headers=self.headers, json=query_dict
|
||||
)
|
||||
data = res.json()
|
||||
if 'results' not in data or data["results"] is None:
|
||||
done = True
|
||||
break
|
||||
heading = ''
|
||||
for result in data["results"]:
|
||||
result_type = result["type"]
|
||||
result_obj = result[result_type]
|
||||
cur_result_text_arr = []
|
||||
if result_type == 'table':
|
||||
result_block_id = result["id"]
|
||||
text = self._read_table_rows(result_block_id)
|
||||
result_lines_arr.append(text)
|
||||
else:
|
||||
if "rich_text" in result_obj:
|
||||
for rich_text in result_obj["rich_text"]:
|
||||
# skip if doesn't have text object
|
||||
if "text" in rich_text:
|
||||
text = rich_text["text"]["content"]
|
||||
prefix = "\t" * num_tabs
|
||||
cur_result_text_arr.append(prefix + text)
|
||||
if result_type in HEADING_TYPE:
|
||||
heading = text
|
||||
result_block_id = result["id"]
|
||||
has_children = result["has_children"]
|
||||
if has_children:
|
||||
children_text = self._read_block(
|
||||
result_block_id, num_tabs=num_tabs + 1
|
||||
)
|
||||
cur_result_text_arr.append(children_text)
|
||||
|
||||
cur_result_text = "\n".join(cur_result_text_arr)
|
||||
if result_type in HEADING_TYPE:
|
||||
result_lines_arr.append(cur_result_text)
|
||||
else:
|
||||
result_lines_arr.append(f'{heading}\n{cur_result_text}')
|
||||
|
||||
if data["next_cursor"] is None:
|
||||
done = True
|
||||
break
|
||||
else:
|
||||
cur_block_id = data["next_cursor"]
|
||||
|
||||
result_lines = "\n".join(result_lines_arr)
|
||||
return result_lines
|
||||
|
||||
def _read_table_rows(self, block_id: str) -> str:
|
||||
"""Read table rows."""
|
||||
done = False
|
||||
result_lines_arr = []
|
||||
cur_block_id = block_id
|
||||
while not done:
|
||||
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id)
|
||||
query_dict: Dict[str, Any] = {}
|
||||
|
||||
res = requests.request(
|
||||
"GET", block_url, headers=self.headers, json=query_dict
|
||||
)
|
||||
data = res.json()
|
||||
# get table headers text
|
||||
table_header_cell_texts = []
|
||||
tabel_header_cells = data["results"][0]['table_row']['cells']
|
||||
for tabel_header_cell in tabel_header_cells:
|
||||
if tabel_header_cell:
|
||||
for table_header_cell_text in tabel_header_cell:
|
||||
text = table_header_cell_text["text"]["content"]
|
||||
table_header_cell_texts.append(text)
|
||||
# get table columns text and format
|
||||
results = data["results"]
|
||||
for i in range(len(results)-1):
|
||||
column_texts = []
|
||||
tabel_column_cells = data["results"][i+1]['table_row']['cells']
|
||||
for j in range(len(tabel_column_cells)):
|
||||
if tabel_column_cells[j]:
|
||||
for table_column_cell_text in tabel_column_cells[j]:
|
||||
column_text = table_column_cell_text["text"]["content"]
|
||||
column_texts.append(f'{table_header_cell_texts[j]}:{column_text}')
|
||||
|
||||
cur_result_text = "\n".join(column_texts)
|
||||
result_lines_arr.append(cur_result_text)
|
||||
|
||||
if data["next_cursor"] is None:
|
||||
done = True
|
||||
break
|
||||
else:
|
||||
cur_block_id = data["next_cursor"]
|
||||
|
||||
result_lines = "\n".join(result_lines_arr)
|
||||
return result_lines
|
||||
def _read_parent_blocks(self, block_id: str, num_tabs: int = 0) -> List[str]:
|
||||
"""Read a block."""
|
||||
done = False
|
||||
result_lines_arr = []
|
||||
cur_block_id = block_id
|
||||
while not done:
|
||||
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id)
|
||||
query_dict: Dict[str, Any] = {}
|
||||
|
||||
res = requests.request(
|
||||
"GET", block_url, headers=self.headers, json=query_dict
|
||||
)
|
||||
data = res.json()
|
||||
# current block's heading
|
||||
heading = ''
|
||||
for result in data["results"]:
|
||||
result_type = result["type"]
|
||||
result_obj = result[result_type]
|
||||
cur_result_text_arr = []
|
||||
if result_type == 'table':
|
||||
result_block_id = result["id"]
|
||||
text = self._read_table_rows(result_block_id)
|
||||
text += "\n\n"
|
||||
result_lines_arr.append(text)
|
||||
else:
|
||||
if "rich_text" in result_obj:
|
||||
for rich_text in result_obj["rich_text"]:
|
||||
# skip if doesn't have text object
|
||||
if "text" in rich_text:
|
||||
text = rich_text["text"]["content"]
|
||||
cur_result_text_arr.append(text)
|
||||
if result_type in HEADING_TYPE:
|
||||
heading = text
|
||||
|
||||
result_block_id = result["id"]
|
||||
has_children = result["has_children"]
|
||||
if has_children:
|
||||
children_text = self._read_block(
|
||||
result_block_id, num_tabs=num_tabs + 1
|
||||
)
|
||||
cur_result_text_arr.append(children_text)
|
||||
|
||||
cur_result_text = "\n".join(cur_result_text_arr)
|
||||
cur_result_text += "\n\n"
|
||||
if result_type in HEADING_TYPE:
|
||||
result_lines_arr.append(cur_result_text)
|
||||
else:
|
||||
result_lines_arr.append(f'{heading}\n{cur_result_text}')
|
||||
|
||||
if data["next_cursor"] is None:
|
||||
done = True
|
||||
break
|
||||
else:
|
||||
cur_block_id = data["next_cursor"]
|
||||
return result_lines_arr
|
||||
|
||||
def read_page(self, page_id: str) -> str:
|
||||
"""Read a page."""
|
||||
return self._read_block(page_id)
|
||||
|
||||
def read_page_as_documents(self, page_id: str) -> List[str]:
|
||||
"""Read a page as documents."""
|
||||
return self._read_parent_blocks(page_id)
|
||||
|
||||
def query_database_data(
|
||||
self, database_id: str, query_dict: Dict[str, Any] = {}
|
||||
) -> str:
|
||||
"""Get all the pages from a Notion database."""
|
||||
res = requests.post\
|
||||
(
|
||||
DATABASE_URL_TMPL.format(database_id=database_id),
|
||||
headers=self.headers,
|
||||
json=query_dict,
|
||||
)
|
||||
data = res.json()
|
||||
database_content_list = []
|
||||
if 'results' not in data or data["results"] is None:
|
||||
return ""
|
||||
for result in data["results"]:
|
||||
properties = result['properties']
|
||||
data = {}
|
||||
for property_name, property_value in properties.items():
|
||||
type = property_value['type']
|
||||
if type == 'multi_select':
|
||||
value = []
|
||||
multi_select_list = property_value[type]
|
||||
for multi_select in multi_select_list:
|
||||
value.append(multi_select['name'])
|
||||
elif type == 'rich_text' or type == 'title':
|
||||
if len(property_value[type]) > 0:
|
||||
value = property_value[type][0]['plain_text']
|
||||
else:
|
||||
value = ''
|
||||
elif type == 'select' or type == 'status':
|
||||
if property_value[type]:
|
||||
value = property_value[type]['name']
|
||||
else:
|
||||
value = ''
|
||||
else:
|
||||
value = property_value[type]
|
||||
data[property_name] = value
|
||||
database_content_list.append(json.dumps(data))
|
||||
|
||||
return "\n\n".join(database_content_list)
|
||||
|
||||
def query_database(
|
||||
self, database_id: str, query_dict: Dict[str, Any] = {}
|
||||
) -> List[str]:
|
||||
"""Get all the pages from a Notion database."""
|
||||
res = requests.post\
|
||||
(
|
||||
DATABASE_URL_TMPL.format(database_id=database_id),
|
||||
headers=self.headers,
|
||||
json=query_dict,
|
||||
)
|
||||
data = res.json()
|
||||
page_ids = []
|
||||
for result in data["results"]:
|
||||
page_id = result["id"]
|
||||
page_ids.append(page_id)
|
||||
|
||||
return page_ids
|
||||
|
||||
def search(self, query: str) -> List[str]:
|
||||
"""Search Notion page given a text query."""
|
||||
done = False
|
||||
next_cursor: Optional[str] = None
|
||||
page_ids = []
|
||||
while not done:
|
||||
query_dict = {
|
||||
"query": query,
|
||||
}
|
||||
if next_cursor is not None:
|
||||
query_dict["start_cursor"] = next_cursor
|
||||
res = requests.post(SEARCH_URL, headers=self.headers, json=query_dict)
|
||||
data = res.json()
|
||||
for result in data["results"]:
|
||||
page_id = result["id"]
|
||||
page_ids.append(page_id)
|
||||
|
||||
if data["next_cursor"] is None:
|
||||
done = True
|
||||
break
|
||||
else:
|
||||
next_cursor = data["next_cursor"]
|
||||
return page_ids
|
||||
|
||||
def load_data(
|
||||
self, page_ids: List[str] = [], database_id: Optional[str] = None
|
||||
) -> List[Document]:
|
||||
"""Load data from the input directory.
|
||||
|
||||
Args:
|
||||
page_ids (List[str]): List of page ids to load.
|
||||
|
||||
Returns:
|
||||
List[Document]: List of documents.
|
||||
|
||||
"""
|
||||
if not page_ids and not database_id:
|
||||
raise ValueError("Must specify either `page_ids` or `database_id`.")
|
||||
docs = []
|
||||
if database_id is not None:
|
||||
# get all the pages in the database
|
||||
page_ids = self.query_database(database_id)
|
||||
for page_id in page_ids:
|
||||
page_text = self.read_page(page_id)
|
||||
docs.append(Document(page_text))
|
||||
else:
|
||||
for page_id in page_ids:
|
||||
page_text = self.read_page(page_id)
|
||||
docs.append(Document(page_text))
|
||||
|
||||
return docs
|
||||
|
||||
def load_data_as_documents(
|
||||
self, page_ids: List[str] = [], database_id: Optional[str] = None
|
||||
) -> List[Document]:
|
||||
if not page_ids and not database_id:
|
||||
raise ValueError("Must specify either `page_ids` or `database_id`.")
|
||||
docs = []
|
||||
if database_id is not None:
|
||||
# get all the pages in the database
|
||||
page_text = self.query_database_data(database_id)
|
||||
docs.append(Document(page_text))
|
||||
else:
|
||||
for page_id in page_ids:
|
||||
page_text_list = self.read_page_as_documents(page_id)
|
||||
for page_text in page_text_list:
|
||||
docs.append(Document(page_text))
|
||||
|
||||
return docs
|
||||
|
||||
def get_page_last_edited_time(self, page_id: str) -> str:
|
||||
retrieve_page_url = RETRIEVE_PAGE_URL_TMPL.format(page_id=page_id)
|
||||
query_dict: Dict[str, Any] = {}
|
||||
|
||||
res = requests.request(
|
||||
"GET", retrieve_page_url, headers=self.headers, json=query_dict
|
||||
)
|
||||
data = res.json()
|
||||
return data["last_edited_time"]
|
||||
|
||||
def get_database_last_edited_time(self, database_id: str) -> str:
|
||||
retrieve_page_url = RETRIEVE_DATABASE_URL_TMPL.format(database_id=database_id)
|
||||
query_dict: Dict[str, Any] = {}
|
||||
|
||||
res = requests.request(
|
||||
"GET", retrieve_page_url, headers=self.headers, json=query_dict
|
||||
)
|
||||
data = res.json()
|
||||
return data["last_edited_time"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
reader = NotionPageReader()
|
||||
logger.info(reader.search("What I"))
|
||||
@ -0,0 +1,256 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
from flask_login import current_user
|
||||
|
||||
from extensions.ext_database import db
|
||||
from models.source import DataSourceBinding
|
||||
|
||||
|
||||
class OAuthDataSource:
|
||||
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.redirect_uri = redirect_uri
|
||||
|
||||
def get_authorization_url(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_access_token(self, code: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class NotionOAuth(OAuthDataSource):
|
||||
_AUTH_URL = 'https://api.notion.com/v1/oauth/authorize'
|
||||
_TOKEN_URL = 'https://api.notion.com/v1/oauth/token'
|
||||
_NOTION_PAGE_SEARCH = "https://api.notion.com/v1/search"
|
||||
_NOTION_BLOCK_SEARCH = "https://api.notion.com/v1/blocks"
|
||||
|
||||
def get_authorization_url(self):
|
||||
params = {
|
||||
'client_id': self.client_id,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': self.redirect_uri,
|
||||
'owner': 'user'
|
||||
}
|
||||
return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}"
|
||||
|
||||
def get_access_token(self, code: str):
|
||||
data = {
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': self.redirect_uri
|
||||
}
|
||||
headers = {'Accept': 'application/json'}
|
||||
auth = (self.client_id, self.client_secret)
|
||||
response = requests.post(self._TOKEN_URL, data=data, auth=auth, headers=headers)
|
||||
|
||||
response_json = response.json()
|
||||
access_token = response_json.get('access_token')
|
||||
if not access_token:
|
||||
raise ValueError(f"Error in Notion OAuth: {response_json}")
|
||||
workspace_name = response_json.get('workspace_name')
|
||||
workspace_icon = response_json.get('workspace_icon')
|
||||
workspace_id = response_json.get('workspace_id')
|
||||
# get all authorized pages
|
||||
pages = self.get_authorized_pages(access_token)
|
||||
source_info = {
|
||||
'workspace_name': workspace_name,
|
||||
'workspace_icon': workspace_icon,
|
||||
'workspace_id': workspace_id,
|
||||
'pages': pages,
|
||||
'total': len(pages)
|
||||
}
|
||||
# save data source binding
|
||||
data_source_binding = DataSourceBinding.query.filter(
|
||||
db.and_(
|
||||
DataSourceBinding.tenant_id == current_user.current_tenant_id,
|
||||
DataSourceBinding.provider == 'notion',
|
||||
DataSourceBinding.access_token == access_token
|
||||
)
|
||||
).first()
|
||||
if data_source_binding:
|
||||
data_source_binding.source_info = source_info
|
||||
data_source_binding.disabled = False
|
||||
db.session.commit()
|
||||
else:
|
||||
new_data_source_binding = DataSourceBinding(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
access_token=access_token,
|
||||
source_info=source_info,
|
||||
provider='notion'
|
||||
)
|
||||
db.session.add(new_data_source_binding)
|
||||
db.session.commit()
|
||||
|
||||
def sync_data_source(self, binding_id: str):
|
||||
# save data source binding
|
||||
data_source_binding = DataSourceBinding.query.filter(
|
||||
db.and_(
|
||||
DataSourceBinding.tenant_id == current_user.current_tenant_id,
|
||||
DataSourceBinding.provider == 'notion',
|
||||
DataSourceBinding.id == binding_id,
|
||||
DataSourceBinding.disabled == False
|
||||
)
|
||||
).first()
|
||||
if data_source_binding:
|
||||
# get all authorized pages
|
||||
pages = self.get_authorized_pages(data_source_binding.access_token)
|
||||
source_info = data_source_binding.source_info
|
||||
new_source_info = {
|
||||
'workspace_name': source_info['workspace_name'],
|
||||
'workspace_icon': source_info['workspace_icon'],
|
||||
'workspace_id': source_info['workspace_id'],
|
||||
'pages': pages,
|
||||
'total': len(pages)
|
||||
}
|
||||
data_source_binding.source_info = new_source_info
|
||||
data_source_binding.disabled = False
|
||||
db.session.commit()
|
||||
else:
|
||||
raise ValueError('Data source binding not found')
|
||||
|
||||
def get_authorized_pages(self, access_token: str):
|
||||
pages = []
|
||||
page_results = self.notion_page_search(access_token)
|
||||
database_results = self.notion_database_search(access_token)
|
||||
# get page detail
|
||||
for page_result in page_results:
|
||||
page_id = page_result['id']
|
||||
if 'Name' in page_result['properties']:
|
||||
if len(page_result['properties']['Name']['title']) > 0:
|
||||
page_name = page_result['properties']['Name']['title'][0]['plain_text']
|
||||
else:
|
||||
page_name = 'Untitled'
|
||||
elif 'title' in page_result['properties']:
|
||||
if len(page_result['properties']['title']['title']) > 0:
|
||||
page_name = page_result['properties']['title']['title'][0]['plain_text']
|
||||
else:
|
||||
page_name = 'Untitled'
|
||||
elif 'Title' in page_result['properties']:
|
||||
if len(page_result['properties']['Title']['title']) > 0:
|
||||
page_name = page_result['properties']['Title']['title'][0]['plain_text']
|
||||
else:
|
||||
page_name = 'Untitled'
|
||||
else:
|
||||
page_name = 'Untitled'
|
||||
page_icon = page_result['icon']
|
||||
if page_icon:
|
||||
icon_type = page_icon['type']
|
||||
if icon_type == 'external' or icon_type == 'file':
|
||||
url = page_icon[icon_type]['url']
|
||||
icon = {
|
||||
'type': 'url',
|
||||
'url': url if url.startswith('http') else f'https://www.notion.so{url}'
|
||||
}
|
||||
else:
|
||||
icon = {
|
||||
'type': 'emoji',
|
||||
'emoji': page_icon[icon_type]
|
||||
}
|
||||
else:
|
||||
icon = None
|
||||
parent = page_result['parent']
|
||||
parent_type = parent['type']
|
||||
if parent_type == 'block_id':
|
||||
parent_id = self.notion_block_parent_page_id(access_token, parent[parent_type])
|
||||
elif parent_type == 'workspace':
|
||||
parent_id = 'root'
|
||||
else:
|
||||
parent_id = parent[parent_type]
|
||||
page = {
|
||||
'page_id': page_id,
|
||||
'page_name': page_name,
|
||||
'page_icon': icon,
|
||||
'parent_id': parent_id,
|
||||
'type': 'page'
|
||||
}
|
||||
pages.append(page)
|
||||
# get database detail
|
||||
for database_result in database_results:
|
||||
page_id = database_result['id']
|
||||
if len(database_result['title']) > 0:
|
||||
page_name = database_result['title'][0]['plain_text']
|
||||
else:
|
||||
page_name = 'Untitled'
|
||||
page_icon = database_result['icon']
|
||||
if page_icon:
|
||||
icon_type = page_icon['type']
|
||||
if icon_type == 'external' or icon_type == 'file':
|
||||
url = page_icon[icon_type]['url']
|
||||
icon = {
|
||||
'type': 'url',
|
||||
'url': url if url.startswith('http') else f'https://www.notion.so{url}'
|
||||
}
|
||||
else:
|
||||
icon = {
|
||||
'type': icon_type,
|
||||
icon_type: page_icon[icon_type]
|
||||
}
|
||||
else:
|
||||
icon = None
|
||||
parent = database_result['parent']
|
||||
parent_type = parent['type']
|
||||
if parent_type == 'block_id':
|
||||
parent_id = self.notion_block_parent_page_id(access_token, parent[parent_type])
|
||||
elif parent_type == 'workspace':
|
||||
parent_id = 'root'
|
||||
else:
|
||||
parent_id = parent[parent_type]
|
||||
page = {
|
||||
'page_id': page_id,
|
||||
'page_name': page_name,
|
||||
'page_icon': icon,
|
||||
'parent_id': parent_id,
|
||||
'type': 'database'
|
||||
}
|
||||
pages.append(page)
|
||||
return pages
|
||||
|
||||
def notion_page_search(self, access_token: str):
|
||||
data = {
|
||||
'filter': {
|
||||
"value": "page",
|
||||
"property": "object"
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f"Bearer {access_token}",
|
||||
'Notion-Version': '2022-06-28',
|
||||
}
|
||||
response = requests.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers)
|
||||
response_json = response.json()
|
||||
results = response_json['results']
|
||||
return results
|
||||
|
||||
def notion_block_parent_page_id(self, access_token: str, block_id: str):
|
||||
headers = {
|
||||
'Authorization': f"Bearer {access_token}",
|
||||
'Notion-Version': '2022-06-28',
|
||||
}
|
||||
response = requests.get(url=f'{self._NOTION_BLOCK_SEARCH}/{block_id}', headers=headers)
|
||||
response_json = response.json()
|
||||
parent = response_json['parent']
|
||||
parent_type = parent['type']
|
||||
if parent_type == 'block_id':
|
||||
return self.notion_block_parent_page_id(access_token, parent[parent_type])
|
||||
return parent[parent_type]
|
||||
|
||||
def notion_database_search(self, access_token: str):
|
||||
data = {
|
||||
'filter': {
|
||||
"value": "database",
|
||||
"property": "object"
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f"Bearer {access_token}",
|
||||
'Notion-Version': '2022-06-28',
|
||||
}
|
||||
response = requests.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers)
|
||||
response_json = response.json()
|
||||
results = response_json['results']
|
||||
return results
|
||||
@ -0,0 +1,46 @@
|
||||
"""e08af0a69ccefbb59fa80c778efee300bb780980
|
||||
|
||||
Revision ID: e32f6ccb87c6
|
||||
Revises: a45f4dfde53b
|
||||
Create Date: 2023-06-06 19:58:33.103819
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e32f6ccb87c6'
|
||||
down_revision = '614f77cecc48'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('data_source_bindings',
|
||||
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('tenant_id', postgresql.UUID(), nullable=False),
|
||||
sa.Column('access_token', sa.String(length=255), nullable=False),
|
||||
sa.Column('provider', sa.String(length=255), nullable=False),
|
||||
sa.Column('source_info', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.Column('disabled', sa.Boolean(), server_default=sa.text('false'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name='source_binding_pkey')
|
||||
)
|
||||
with op.batch_alter_table('data_source_bindings', schema=None) as batch_op:
|
||||
batch_op.create_index('source_binding_tenant_id_idx', ['tenant_id'], unique=False)
|
||||
batch_op.create_index('source_info_idx', ['source_info'], unique=False, postgresql_using='gin')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('data_source_bindings', schema=None) as batch_op:
|
||||
batch_op.drop_index('source_info_idx', postgresql_using='gin')
|
||||
batch_op.drop_index('source_binding_tenant_id_idx')
|
||||
|
||||
op.drop_table('data_source_bindings')
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,21 @@
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from extensions.ext_database import db
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
class DataSourceBinding(db.Model):
|
||||
__tablename__ = 'data_source_bindings'
|
||||
__table_args__ = (
|
||||
db.PrimaryKeyConstraint('id', name='source_binding_pkey'),
|
||||
db.Index('source_binding_tenant_id_idx', 'tenant_id'),
|
||||
db.Index('source_info_idx', "source_info", postgresql_using='gin')
|
||||
)
|
||||
|
||||
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
|
||||
tenant_id = db.Column(UUID, nullable=False)
|
||||
access_token = db.Column(db.String(255), nullable=False)
|
||||
provider = db.Column(db.String(255), nullable=False)
|
||||
source_info = db.Column(JSONB, nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||
disabled = db.Column(db.Boolean, nullable=True, server_default=db.text('false'))
|
||||
@ -0,0 +1,58 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
|
||||
from core.index.keyword_table_index import KeywordTableIndex
|
||||
from core.index.vector_index import VectorIndex
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import DocumentSegment, Dataset, Document
|
||||
|
||||
|
||||
@shared_task
|
||||
def clean_notion_document_task(document_ids: List[str], dataset_id: str):
|
||||
"""
|
||||
Clean document when document deleted.
|
||||
:param document_ids: document ids
|
||||
:param dataset_id: dataset id
|
||||
|
||||
Usage: clean_notion_document_task.delay(document_ids, dataset_id)
|
||||
"""
|
||||
logging.info(click.style('Start clean document when import form notion document deleted: {}'.format(dataset_id), fg='green'))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
try:
|
||||
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
|
||||
|
||||
if not dataset:
|
||||
raise Exception('Document has no dataset')
|
||||
|
||||
vector_index = VectorIndex(dataset=dataset)
|
||||
keyword_table_index = KeywordTableIndex(dataset=dataset)
|
||||
for document_id in document_ids:
|
||||
document = db.session.query(Document).filter(
|
||||
Document.id == document_id
|
||||
).first()
|
||||
db.session.delete(document)
|
||||
segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document_id).all()
|
||||
index_node_ids = [segment.index_node_id for segment in segments]
|
||||
|
||||
# delete from vector index
|
||||
vector_index.del_nodes(index_node_ids)
|
||||
|
||||
# delete from keyword index
|
||||
if index_node_ids:
|
||||
keyword_table_index.del_nodes(index_node_ids)
|
||||
|
||||
for segment in segments:
|
||||
db.session.delete(segment)
|
||||
db.session.commit()
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
click.style('Clean document when import form notion document deleted end :: {} latency: {}'.format(
|
||||
dataset_id, end_at - start_at),
|
||||
fg='green'))
|
||||
except Exception:
|
||||
logging.exception("Cleaned document when import form notion document deleted failed")
|
||||
@ -0,0 +1,109 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from core.data_source.notion import NotionPageReader
|
||||
from core.index.keyword_table_index import KeywordTableIndex
|
||||
from core.index.vector_index import VectorIndex
|
||||
from core.indexing_runner import IndexingRunner, DocumentIsPausedException
|
||||
from core.llm.error import ProviderTokenNotInitError
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import Document, Dataset, DocumentSegment
|
||||
from models.source import DataSourceBinding
|
||||
|
||||
|
||||
@shared_task
|
||||
def document_indexing_sync_task(dataset_id: str, document_id: str):
|
||||
"""
|
||||
Async update document
|
||||
:param dataset_id:
|
||||
:param document_id:
|
||||
|
||||
Usage: document_indexing_sync_task.delay(dataset_id, document_id)
|
||||
"""
|
||||
logging.info(click.style('Start sync document: {}'.format(document_id), fg='green'))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
document = db.session.query(Document).filter(
|
||||
Document.id == document_id,
|
||||
Document.dataset_id == dataset_id
|
||||
).first()
|
||||
|
||||
if not document:
|
||||
raise NotFound('Document not found')
|
||||
|
||||
data_source_info = document.data_source_info_dict
|
||||
if document.data_source_type == 'notion_import':
|
||||
if not data_source_info or 'notion_page_id' not in data_source_info \
|
||||
or 'notion_workspace_id' not in data_source_info:
|
||||
raise ValueError("no notion page found")
|
||||
workspace_id = data_source_info['notion_workspace_id']
|
||||
page_id = data_source_info['notion_page_id']
|
||||
page_edited_time = data_source_info['last_edited_time']
|
||||
data_source_binding = DataSourceBinding.query.filter(
|
||||
db.and_(
|
||||
DataSourceBinding.tenant_id == document.tenant_id,
|
||||
DataSourceBinding.provider == 'notion',
|
||||
DataSourceBinding.disabled == False,
|
||||
DataSourceBinding.source_info['workspace_id'] == f'"{workspace_id}"'
|
||||
)
|
||||
).first()
|
||||
if not data_source_binding:
|
||||
raise ValueError('Data source binding not found.')
|
||||
reader = NotionPageReader(integration_token=data_source_binding.access_token)
|
||||
last_edited_time = reader.get_page_last_edited_time(page_id)
|
||||
# check the page is updated
|
||||
if last_edited_time != page_edited_time:
|
||||
document.indexing_status = 'parsing'
|
||||
document.processing_started_at = datetime.datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
# delete all document segment and index
|
||||
try:
|
||||
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
|
||||
if not dataset:
|
||||
raise Exception('Dataset not found')
|
||||
|
||||
vector_index = VectorIndex(dataset=dataset)
|
||||
keyword_table_index = KeywordTableIndex(dataset=dataset)
|
||||
|
||||
segments = db.session.query(DocumentSegment).filter(DocumentSegment.document_id == document_id).all()
|
||||
index_node_ids = [segment.index_node_id for segment in segments]
|
||||
|
||||
# delete from vector index
|
||||
vector_index.del_nodes(index_node_ids)
|
||||
|
||||
# delete from keyword index
|
||||
if index_node_ids:
|
||||
keyword_table_index.del_nodes(index_node_ids)
|
||||
|
||||
for segment in segments:
|
||||
db.session.delete(segment)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
click.style('Cleaned document when document update data source or process rule: {} latency: {}'.format(document_id, end_at - start_at), fg='green'))
|
||||
except Exception:
|
||||
logging.exception("Cleaned document when document update data source or process rule failed")
|
||||
try:
|
||||
indexing_runner = IndexingRunner()
|
||||
indexing_runner.run([document])
|
||||
end_at = time.perf_counter()
|
||||
logging.info(click.style('update document: {} latency: {}'.format(document.id, end_at - start_at), fg='green'))
|
||||
except DocumentIsPausedException:
|
||||
logging.info(click.style('Document update paused, document id: {}'.format(document.id), fg='yellow'))
|
||||
except ProviderTokenNotInitError as e:
|
||||
document.indexing_status = 'error'
|
||||
document.error = str(e.description)
|
||||
document.stopped_at = datetime.datetime.utcnow()
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logging.exception("consume update document failed")
|
||||
document.indexing_status = 'error'
|
||||
document.error = str(e)
|
||||
document.stopped_at = datetime.datetime.utcnow()
|
||||
db.session.commit()
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 3L4.5 8.5L2 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@ -0,0 +1,9 @@
|
||||
.wrapper {
|
||||
border-color: #d0d5dd;
|
||||
}
|
||||
|
||||
.checked {
|
||||
background: #155eef url(./assets/check.svg) center center no-repeat;
|
||||
background-size: 12px 12px;
|
||||
border-color: #155eef;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import cn from 'classnames'
|
||||
import s from './index.module.css'
|
||||
|
||||
type CheckboxProps = {
|
||||
checked?: boolean
|
||||
onCheck?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Checkbox = ({ checked, onCheck, className }: CheckboxProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(s.wrapper, checked && s.checked, 'w-4 h-4 border rounded border-gray-300', className)}
|
||||
onClick={onCheck}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Checkbox
|
||||
@ -0,0 +1,6 @@
|
||||
.default-page-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url(../notion-page-selector/assets/notion-page.svg) center center no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import cn from 'classnames'
|
||||
import s from './index.module.css'
|
||||
import type { DataSourceNotionPage } from '@/models/common'
|
||||
|
||||
type IconTypes = 'workspace' | 'page'
|
||||
type NotionIconProps = {
|
||||
type?: IconTypes
|
||||
name?: string | null
|
||||
className?: string
|
||||
src?: string | null | Pick<DataSourceNotionPage, 'page_icon'>['page_icon']
|
||||
}
|
||||
const NotionIcon = ({
|
||||
type = 'workspace',
|
||||
src,
|
||||
name,
|
||||
className,
|
||||
}: NotionIconProps) => {
|
||||
if (type === 'workspace') {
|
||||
if (typeof src === 'string') {
|
||||
if (src.startsWith('https://') || src.startsWith('http://')) {
|
||||
return (
|
||||
<img
|
||||
alt='workspace icon'
|
||||
src={src}
|
||||
className={cn('block object-cover w-5 h-5', className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center w-5 h-5', className)}>{src}</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center w-5 h-5 bg-gray-200 text-xs font-medium text-gray-500 rounded', className)}>{name?.[0].toLocaleUpperCase()}</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof src === 'object' && src !== null) {
|
||||
if (src?.type === 'url') {
|
||||
return (
|
||||
<img
|
||||
alt='page icon'
|
||||
src={src.url || ''}
|
||||
className={cn('block object-cover w-5 h-5', className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center w-5 h-5', className)}>{src?.emoji}</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(s['default-page-icon'], className)} />
|
||||
)
|
||||
}
|
||||
|
||||
export default NotionIcon
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8C13.5 4.96243 11.0376 2.5 8 2.5ZM9.85355 6.14645C10.0488 6.34171 10.0488 6.65829 9.85355 6.85355L8.70711 8L9.85355 9.14645C10.0488 9.34171 10.0488 9.65829 9.85355 9.85355C9.65829 10.0488 9.34171 10.0488 9.14645 9.85355L8 8.70711L6.85355 9.85355C6.65829 10.0488 6.34171 10.0488 6.14645 9.85355C5.95118 9.65829 5.95118 9.34171 6.14645 9.14645L7.29289 8L6.14645 6.85355C5.95118 6.65829 5.95118 6.34171 6.14645 6.14645C6.34171 5.95118 6.65829 5.95118 6.85355 6.14645L8 7.29289L9.14645 6.14645C9.34171 5.95118 9.65829 5.95118 9.85355 6.14645Z" fill="#98A2B3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 809 B |
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.49939 19.1498H13.6897C15.3354 19.1498 16.1891 18.2807 16.1891 16.6273V9.6521C16.1891 8.58313 16.0507 8.09095 15.3816 7.41418L11.3441 3.30749C10.6981 2.65381 10.1675 2.5 9.20618 2.5H5.49939C3.85363 2.5 3 3.36902 3 5.02246V16.6273C3 18.2884 3.85363 19.1498 5.49939 19.1498ZM5.62243 17.6424C4.87646 17.6424 4.50732 17.2502 4.50732 16.5351V5.11475C4.50732 4.40722 4.87646 4.00732 5.62243 4.00732H8.89856V8.22168C8.89856 9.32142 9.44457 9.85205 10.5366 9.85205H14.6818V16.5351C14.6818 17.2502 14.3049 17.6424 13.5589 17.6424H5.62243ZM10.675 8.52929C10.3597 8.52929 10.229 8.39087 10.229 8.07556V4.21496L14.4741 8.52929H10.675Z" fill="#37352F" fill-opacity="0.45"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.49939 19.1498H13.6897C15.3354 19.1498 16.1891 18.2807 16.1891 16.6273V9.6521C16.1891 8.58313 16.0507 8.09095 15.3816 7.41418L11.3441 3.30749C10.6981 2.65381 10.1675 2.5 9.20618 2.5H5.49939C3.85363 2.5 3 3.36902 3 5.02246V16.6273C3 18.2884 3.85363 19.1498 5.49939 19.1498ZM5.62243 17.6424C4.87645 17.6424 4.50732 17.2502 4.50732 16.5351V5.11475C4.50732 4.40722 4.87645 4.00732 5.62243 4.00732H8.89856V8.22168C8.89856 9.32142 9.44457 9.85205 10.5366 9.85205H14.6818V16.5351C14.6818 17.2502 14.3049 17.6424 13.5589 17.6424H5.62243ZM10.675 8.52929C10.3597 8.52929 10.229 8.39087 10.229 8.07556V4.21496L14.4741 8.52929H10.675ZM12.3362 11.8746H6.70678C6.41454 11.8746 6.2069 12.09 6.2069 12.3591C6.2069 12.636 6.41454 12.8513 6.70678 12.8513H12.3362C12.613 12.8513 12.8207 12.636 12.8207 12.3591C12.8207 12.09 12.613 11.8746 12.3362 11.8746ZM12.3362 14.4587H6.70678C6.41454 14.4587 6.2069 14.674 6.2069 14.9509C6.2069 15.22 6.41454 15.4276 6.70678 15.4276H12.3362C12.613 15.4276 12.8207 15.22 12.8207 14.9509C12.8207 14.674 12.613 14.4587 12.3362 14.4587Z" fill="#37352F" fill-opacity="0.45"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Icon_2" d="M12.25 12.25L10.2084 10.2083M11.6667 6.70833C11.6667 9.44675 9.44675 11.6667 6.70833 11.6667C3.96992 11.6667 1.75 9.44675 1.75 6.70833C1.75 3.96992 3.96992 1.75 6.70833 1.75C9.44675 1.75 11.6667 3.96992 11.6667 6.70833Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
@ -0,0 +1,11 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5943_4745)">
|
||||
<path d="M6.99984 8.74984C7.96634 8.74984 8.74984 7.96634 8.74984 6.99984C8.74984 6.03334 7.96634 5.24984 6.99984 5.24984C6.03334 5.24984 5.24984 6.03334 5.24984 6.99984C5.24984 7.96634 6.03334 8.74984 6.99984 8.74984Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.9241 8.59075C10.8535 8.75069 10.8324 8.92812 10.8636 9.10015C10.8948 9.27218 10.9768 9.43092 11.0991 9.5559L11.1309 9.58772C11.2295 9.68622 11.3077 9.80319 11.3611 9.93195C11.4145 10.0607 11.442 10.1987 11.442 10.3381C11.442 10.4775 11.4145 10.6155 11.3611 10.7442C11.3077 10.873 11.2295 10.99 11.1309 11.0885C11.0324 11.1871 10.9154 11.2653 10.7867 11.3187C10.6579 11.3721 10.5199 11.3995 10.3805 11.3995C10.2411 11.3995 10.1031 11.3721 9.97437 11.3187C9.84561 11.2653 9.72864 11.1871 9.63014 11.0885L9.59832 11.0567C9.47334 10.9344 9.3146 10.8524 9.14257 10.8212C8.97055 10.79 8.79312 10.8111 8.63317 10.8817C8.47632 10.9489 8.34256 11.0605 8.24833 11.2028C8.15411 11.345 8.10355 11.5118 8.10287 11.6824V11.7726C8.10287 12.0539 7.99112 12.3236 7.79222 12.5225C7.59332 12.7214 7.32355 12.8332 7.04226 12.8332C6.76097 12.8332 6.4912 12.7214 6.2923 12.5225C6.0934 12.3236 5.98166 12.0539 5.98166 11.7726V11.7248C5.97755 11.5493 5.92073 11.3791 5.81859 11.2363C5.71645 11.0935 5.57371 10.9847 5.40893 10.9241C5.24898 10.8535 5.07155 10.8324 4.89953 10.8636C4.7275 10.8948 4.56876 10.9768 4.44378 11.0991L4.41196 11.1309C4.31346 11.2295 4.19648 11.3077 4.06773 11.3611C3.93897 11.4145 3.80096 11.442 3.66158 11.442C3.5222 11.442 3.38419 11.4145 3.25543 11.3611C3.12668 11.3077 3.0097 11.2295 2.9112 11.1309C2.81259 11.0324 2.73436 10.9154 2.68099 10.7867C2.62761 10.6579 2.60014 10.5199 2.60014 10.3805C2.60014 10.2411 2.62761 10.1031 2.68099 9.97437C2.73436 9.84561 2.81259 9.72864 2.9112 9.63014L2.94302 9.59832C3.06527 9.47334 3.14728 9.3146 3.17848 9.14257C3.20967 8.97055 3.18861 8.79312 3.11802 8.63317C3.0508 8.47632 2.93918 8.34256 2.7969 8.24833C2.65463 8.15411 2.48791 8.10355 2.31726 8.10287H2.22711C1.94582 8.10287 1.67605 7.99112 1.47715 7.79222C1.27825 7.59332 1.1665 7.32355 1.1665 7.04226C1.1665 6.76097 1.27825 6.4912 1.47715 6.2923C1.67605 6.0934 1.94582 5.98166 2.22711 5.98166H2.27484C2.45036 5.97755 2.6206 5.92073 2.7634 5.81859C2.90621 5.71645 3.01499 5.57371 3.07559 5.40893C3.14619 5.24898 3.16724 5.07155 3.13605 4.89953C3.10486 4.7275 3.02285 4.56876 2.90059 4.44378L2.86878 4.41196C2.77017 4.31346 2.69194 4.19648 2.63856 4.06773C2.58519 3.93897 2.55772 3.80096 2.55772 3.66158C2.55772 3.5222 2.58519 3.38419 2.63856 3.25543C2.69194 3.12668 2.77017 3.0097 2.86878 2.9112C2.96728 2.81259 3.08425 2.73436 3.21301 2.68099C3.34176 2.62761 3.47978 2.60014 3.61916 2.60014C3.75854 2.60014 3.89655 2.62761 4.0253 2.68099C4.15406 2.73436 4.27103 2.81259 4.36953 2.9112L4.40135 2.94302C4.52633 3.06527 4.68507 3.14728 4.8571 3.17848C5.02913 3.20967 5.20656 3.18861 5.3665 3.11802H5.40893C5.56578 3.0508 5.69954 2.93918 5.79377 2.7969C5.88799 2.65463 5.93855 2.48791 5.93923 2.31726V2.22711C5.93923 1.94582 6.05097 1.67605 6.24988 1.47715C6.44878 1.27825 6.71855 1.1665 6.99984 1.1665C7.28113 1.1665 7.5509 1.27825 7.7498 1.47715C7.9487 1.67605 8.06044 1.94582 8.06044 2.22711V2.27484C8.06112 2.44548 8.11169 2.6122 8.20591 2.75448C8.30013 2.89675 8.4339 3.00837 8.59075 3.07559C8.75069 3.14619 8.92812 3.16724 9.10015 3.13605C9.27218 3.10486 9.43092 3.02285 9.5559 2.90059L9.58772 2.86878C9.68622 2.77017 9.80319 2.69194 9.93195 2.63856C10.0607 2.58519 10.1987 2.55772 10.3381 2.55772C10.4775 2.55772 10.6155 2.58519 10.7442 2.63856C10.873 2.69194 10.99 2.77017 11.0885 2.86878C11.1871 2.96728 11.2653 3.08425 11.3187 3.21301C11.3721 3.34176 11.3995 3.47978 11.3995 3.61916C11.3995 3.75854 11.3721 3.89655 11.3187 4.0253C11.2653 4.15406 11.1871 4.27103 11.0885 4.36953L11.0567 4.40135C10.9344 4.52633 10.8524 4.68507 10.8212 4.8571C10.79 5.02913 10.8111 5.20656 10.8817 5.3665V5.40893C10.9489 5.56578 11.0605 5.69954 11.2028 5.79377C11.345 5.88799 11.5118 5.93855 11.6824 5.93923H11.7726C12.0539 5.93923 12.3236 6.05097 12.5225 6.24988C12.7214 6.44878 12.8332 6.71855 12.8332 6.99984C12.8332 7.28113 12.7214 7.5509 12.5225 7.7498C12.3236 7.9487 12.0539 8.06044 11.7726 8.06044H11.7248C11.5542 8.06112 11.3875 8.11169 11.2452 8.20591C11.1029 8.30013 10.9913 8.4339 10.9241 8.59075Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5943_4745">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,4 @@
|
||||
.setting-icon {
|
||||
background: url(./assets/setting.svg) center center no-repeat;
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import cn from 'classnames'
|
||||
import s from './base.module.css'
|
||||
import WorkspaceSelector from './workspace-selector'
|
||||
import SearchInput from './search-input'
|
||||
import PageSelector from './page-selector'
|
||||
import { preImportNotionPages } from '@/service/datasets'
|
||||
import AccountSetting from '@/app/components/header/account-setting'
|
||||
import { NotionConnector } from '@/app/components/datasets/create/step-one'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common'
|
||||
|
||||
export type NotionPageSelectorValue = DataSourceNotionPage & { workspace_id: string }
|
||||
|
||||
type NotionPageSelectorProps = {
|
||||
value?: string[]
|
||||
onSelect: (selectedPages: NotionPageSelectorValue[]) => void
|
||||
canPreview?: boolean
|
||||
previewPageId?: string
|
||||
onPreview?: (selectedPage: NotionPageSelectorValue) => void
|
||||
datasetId?: string
|
||||
}
|
||||
|
||||
const NotionPageSelector = ({
|
||||
value,
|
||||
onSelect,
|
||||
canPreview,
|
||||
previewPageId,
|
||||
onPreview,
|
||||
datasetId = '',
|
||||
}: NotionPageSelectorProps) => {
|
||||
const { data, mutate } = useSWR({ url: '/notion/pre-import/pages', datasetId }, preImportNotionPages)
|
||||
const [prevData, setPrevData] = useState(data)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [showDataSourceSetting, setShowDataSourceSetting] = useState(false)
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useState('')
|
||||
|
||||
const notionWorkspaces = useMemo(() => {
|
||||
return data?.notion_info || []
|
||||
}, [data?.notion_info])
|
||||
const firstWorkspaceId = notionWorkspaces[0]?.workspace_id
|
||||
const currentWorkspace = notionWorkspaces.find(workspace => workspace.workspace_id === currentWorkspaceId)
|
||||
|
||||
const getPagesMapAndSelectedPagesId: [DataSourceNotionPageMap, Set<string>] = useMemo(() => {
|
||||
const selectedPagesId = new Set<string>()
|
||||
const pagesMap = notionWorkspaces.reduce((prev: DataSourceNotionPageMap, next: DataSourceNotionWorkspace) => {
|
||||
next.pages.forEach((page) => {
|
||||
if (page.is_bound)
|
||||
selectedPagesId.add(page.page_id)
|
||||
prev[page.page_id] = {
|
||||
...page,
|
||||
workspace_id: next.workspace_id,
|
||||
}
|
||||
})
|
||||
|
||||
return prev
|
||||
}, {})
|
||||
return [pagesMap, selectedPagesId]
|
||||
}, [notionWorkspaces])
|
||||
const defaultSelectedPagesId = [...Array.from(getPagesMapAndSelectedPagesId[1]), ...(value || [])]
|
||||
const [selectedPagesId, setSelectedPagesId] = useState<Set<string>>(new Set(defaultSelectedPagesId))
|
||||
|
||||
if (prevData !== data) {
|
||||
setPrevData(data)
|
||||
setSelectedPagesId(new Set(defaultSelectedPagesId))
|
||||
}
|
||||
|
||||
const handleSearchValueChange = useCallback((value: string) => {
|
||||
setSearchValue(value)
|
||||
}, [])
|
||||
const handleSelectWorkspace = useCallback((workspaceId: string) => {
|
||||
setCurrentWorkspaceId(workspaceId)
|
||||
}, [])
|
||||
const handleSelecPages = (selectedPagesId: Set<string>) => {
|
||||
setSelectedPagesId(new Set(Array.from(selectedPagesId)))
|
||||
const selectedPages = Array.from(selectedPagesId).map(pageId => getPagesMapAndSelectedPagesId[0][pageId])
|
||||
onSelect(selectedPages)
|
||||
}
|
||||
const handlePreviewPage = (previewPageId: string) => {
|
||||
if (onPreview)
|
||||
onPreview(getPagesMapAndSelectedPagesId[0][previewPageId])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentWorkspaceId(firstWorkspaceId)
|
||||
}, [firstWorkspaceId])
|
||||
|
||||
return (
|
||||
<div className='bg-gray-25 border border-gray-200 rounded-xl'>
|
||||
{
|
||||
data?.notion_info?.length
|
||||
? (
|
||||
<>
|
||||
<div className='flex items-center pl-[10px] pr-2 h-11 bg-white border-b border-b-gray-200 rounded-t-xl'>
|
||||
<WorkspaceSelector
|
||||
value={currentWorkspaceId || firstWorkspaceId}
|
||||
items={notionWorkspaces}
|
||||
onSelect={handleSelectWorkspace}
|
||||
/>
|
||||
<div className='mx-1 w-[1px] h-3 bg-gray-200' />
|
||||
<div
|
||||
className={cn(s['setting-icon'], 'w-6 h-6 cursor-pointer')}
|
||||
onClick={() => setShowDataSourceSetting(true)}
|
||||
/>
|
||||
<div className='grow' />
|
||||
<SearchInput
|
||||
value={searchValue}
|
||||
onChange={handleSearchValueChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='rounded-b-xl overflow-hidden'>
|
||||
<PageSelector
|
||||
value={selectedPagesId}
|
||||
searchValue={searchValue}
|
||||
list={currentWorkspace?.pages || []}
|
||||
pagesMap={getPagesMapAndSelectedPagesId[0]}
|
||||
onSelect={handleSelecPages}
|
||||
canPreview={canPreview}
|
||||
previewPageId={previewPageId}
|
||||
onPreview={handlePreviewPage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<NotionConnector onSetting={() => setShowDataSourceSetting(true)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
showDataSourceSetting && (
|
||||
<AccountSetting activeTab='data-source' onCancel={() => {
|
||||
setShowDataSourceSetting(false)
|
||||
mutate()
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotionPageSelector
|
||||
@ -0,0 +1,2 @@
|
||||
export { default as NotionPageSelectorModal } from './notion-page-selector-modal'
|
||||
export { default as NotionPageSelector } from './base'
|
||||
@ -0,0 +1,28 @@
|
||||
.modal {
|
||||
width: 600px !important;
|
||||
max-width: 600px !important;
|
||||
padding: 24px 32px !important;
|
||||
}
|
||||
|
||||
.operate {
|
||||
padding: 0 8px;
|
||||
min-width: 96px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid #eaecf0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #667085;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.operate-save {
|
||||
margin-left: 8px;
|
||||
border-color: #155eef;
|
||||
background-color: #155eef;
|
||||
color: #ffffff;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import NotionPageSelector from '../base'
|
||||
import type { NotionPageSelectorValue } from '../base'
|
||||
import s from './index.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
|
||||
type NotionPageSelectorModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onSave: (selectedPages: NotionPageSelectorValue[]) => void
|
||||
datasetId: string
|
||||
}
|
||||
const NotionPageSelectorModal = ({
|
||||
isShow,
|
||||
onClose,
|
||||
onSave,
|
||||
datasetId,
|
||||
}: NotionPageSelectorModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedPages, setSelectedPages] = useState<NotionPageSelectorValue[]>([])
|
||||
|
||||
const handleClose = () => {
|
||||
onClose()
|
||||
}
|
||||
const handleSelectPage = (newSelectedPages: NotionPageSelectorValue[]) => {
|
||||
setSelectedPages(newSelectedPages)
|
||||
}
|
||||
const handleSave = () => {
|
||||
onSave(selectedPages)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={s.modal}
|
||||
isShow={isShow}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div className='flex items-center justify-between mb-6 h-8'>
|
||||
<div className='text-xl font-semibold text-gray-900'>{t('common.dataSource.notion.selector.addPages')}</div>
|
||||
<div
|
||||
className='flex items-center justify-center -mr-2 w-8 h-8 cursor-pointer'
|
||||
onClick={handleClose}>
|
||||
<XMarkIcon className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
<NotionPageSelector
|
||||
onSelect={handleSelectPage}
|
||||
canPreview={false}
|
||||
datasetId={datasetId}
|
||||
/>
|
||||
<div className='mt-8 flex justify-end'>
|
||||
<div className={s.operate} onClick={handleClose}>{t('common.operation.cancel')}</div>
|
||||
<div className={cn(s.operate, s['operate-save'])} onClick={handleSave}>{t('common.operation.save')}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotionPageSelectorModal
|
||||
@ -0,0 +1,17 @@
|
||||
.arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url(../assets/down-arrow.svg) center center no-repeat;
|
||||
background-size: 16px 16px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.arrow-expand {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
background-color: #eff4ff;
|
||||
border: 1px solid #D1E0FF;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
@ -0,0 +1,299 @@
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList as List, areEqual } from 'react-window'
|
||||
import type { ListChildComponentProps } from 'react-window'
|
||||
import cn from 'classnames'
|
||||
import Checkbox from '../../checkbox'
|
||||
import NotionIcon from '../../notion-icon'
|
||||
import s from './index.module.css'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
|
||||
type PageSelectorProps = {
|
||||
value: Set<string>
|
||||
searchValue: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
list: DataSourceNotionPage[]
|
||||
onSelect: (selectedPagesId: Set<string>) => void
|
||||
canPreview?: boolean
|
||||
previewPageId?: string
|
||||
onPreview?: (selectedPageId: string) => void
|
||||
}
|
||||
type NotionPageTreeItem = {
|
||||
children: Set<string>
|
||||
descendants: Set<string>
|
||||
deepth: number
|
||||
ancestors: string[]
|
||||
} & DataSourceNotionPage
|
||||
type NotionPageTreeMap = Record<string, NotionPageTreeItem>
|
||||
type NotionPageItem = {
|
||||
expand: boolean
|
||||
deepth: number
|
||||
} & DataSourceNotionPage
|
||||
|
||||
const recursivePushInParentDescendants = (
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
listTreeMap: NotionPageTreeMap,
|
||||
current: NotionPageTreeItem,
|
||||
leafItem: NotionPageTreeItem,
|
||||
) => {
|
||||
const parentId = current.parent_id
|
||||
const pageId = current.page_id
|
||||
|
||||
if (!parentId || !pageId)
|
||||
return
|
||||
|
||||
if (parentId !== 'root' && pagesMap[parentId]) {
|
||||
if (!listTreeMap[parentId]) {
|
||||
const children = new Set([pageId])
|
||||
const descendants = new Set([pageId, leafItem.page_id])
|
||||
listTreeMap[parentId] = {
|
||||
...pagesMap[parentId],
|
||||
children,
|
||||
descendants,
|
||||
deepth: 0,
|
||||
ancestors: [],
|
||||
}
|
||||
}
|
||||
else {
|
||||
listTreeMap[parentId].children.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(pageId)
|
||||
listTreeMap[parentId].descendants.add(leafItem.page_id)
|
||||
}
|
||||
leafItem.deepth++
|
||||
leafItem.ancestors.unshift(listTreeMap[parentId].page_name)
|
||||
|
||||
if (listTreeMap[parentId].parent_id !== 'root')
|
||||
recursivePushInParentDescendants(pagesMap, listTreeMap, listTreeMap[parentId], leafItem)
|
||||
}
|
||||
}
|
||||
|
||||
const Item = memo(({ index, style, data }: ListChildComponentProps<{
|
||||
dataList: NotionPageItem[]
|
||||
handleToggle: (index: number) => void
|
||||
checkedIds: Set<string>
|
||||
handleCheck: (index: number) => void
|
||||
canPreview?: boolean
|
||||
handlePreview: (index: number) => void
|
||||
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||
searchValue: string
|
||||
previewPageId: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
}>) => {
|
||||
const { t } = useTranslation()
|
||||
const { dataList, handleToggle, checkedIds, handleCheck, canPreview, handlePreview, listMapWithChildrenAndDescendants, searchValue, previewPageId, pagesMap } = data
|
||||
const current = dataList[index]
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||
const breadCrumbs = ancestors.length ? [...ancestors, current.page_name] : [current.page_name]
|
||||
|
||||
const renderArrow = () => {
|
||||
if (hasChild) {
|
||||
return (
|
||||
<div
|
||||
className={cn(s.arrow, current.expand && s['arrow-expand'], 'shrink-0 mr-1 w-5 h-5 hover:bg-gray-200 rounded-md')}
|
||||
style={{ marginLeft: current.deepth * 8 }}
|
||||
onClick={() => handleToggle(index)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (current.parent_id === 'root' || !pagesMap[current.parent_id]) {
|
||||
return (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='shrink-0 mr-1 w-5 h-5' style={{ marginLeft: current.deepth * 8 }} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex items-center pl-2 pr-[2px] rounded-md border border-transparent hover:bg-gray-100 cursor-pointer', previewPageId === current.page_id && s['preview-item'])}
|
||||
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
|
||||
>
|
||||
<Checkbox
|
||||
className='shrink-0 mr-2 group-hover:border-primary-600 group-hover:border-[2px]'
|
||||
checked={checkedIds.has(current.page_id)}
|
||||
onCheck={() => handleCheck(index)}
|
||||
/>
|
||||
{!searchValue && renderArrow()}
|
||||
<NotionIcon
|
||||
className='shrink-0 mr-1'
|
||||
type='page'
|
||||
src={current.page_icon}
|
||||
/>
|
||||
<div
|
||||
className='grow text-sm font-medium text-gray-700 truncate'
|
||||
title={current.page_name}
|
||||
>
|
||||
{current.page_name}
|
||||
</div>
|
||||
{
|
||||
canPreview && (
|
||||
<div
|
||||
className='shrink-0 hidden group-hover:flex items-center ml-1 px-2 h-6 rounded-md text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-50 hover:text-gray-700'
|
||||
onClick={() => handlePreview(index)}>
|
||||
{t('common.dataSource.notion.selector.preview')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
searchValue && (
|
||||
<div
|
||||
className='shrink-0 ml-1 max-w-[120px] text-xs text-gray-400 truncate'
|
||||
title={breadCrumbs.join(' / ')}
|
||||
>
|
||||
{breadCrumbs.join(' / ')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}, areEqual)
|
||||
|
||||
const PageSelector = ({
|
||||
value,
|
||||
searchValue,
|
||||
pagesMap,
|
||||
list,
|
||||
onSelect,
|
||||
canPreview = true,
|
||||
previewPageId,
|
||||
onPreview,
|
||||
}: PageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [prevDataList, setPrevDataList] = useState(list)
|
||||
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
||||
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
|
||||
if (prevDataList !== list) {
|
||||
setPrevDataList(list)
|
||||
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
deepth: 0,
|
||||
}
|
||||
}))
|
||||
}
|
||||
const searchDataList = list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
deepth: 0,
|
||||
}
|
||||
})
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
|
||||
|
||||
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||
const pageId = next.page_id
|
||||
if (!prev[pageId])
|
||||
prev[pageId] = { ...next, children: new Set(), descendants: new Set(), deepth: 0, ancestors: [] }
|
||||
|
||||
recursivePushInParentDescendants(pagesMap, prev, prev[pageId], prev[pageId])
|
||||
return prev
|
||||
}, {})
|
||||
}, [list, pagesMap])
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
const current = dataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
||||
let newDataList = []
|
||||
|
||||
if (current.expand) {
|
||||
current.expand = false
|
||||
|
||||
newDataList = [...dataList.filter(item => !descendantsIds.includes(item.page_id))]
|
||||
}
|
||||
else {
|
||||
current.expand = true
|
||||
|
||||
newDataList = [
|
||||
...dataList.slice(0, index + 1),
|
||||
...childrenIds.map(item => ({
|
||||
...pagesMap[item],
|
||||
expand: false,
|
||||
deepth: listMapWithChildrenAndDescendants[item].deepth,
|
||||
})),
|
||||
...dataList.slice(index + 1)]
|
||||
}
|
||||
setDataList(newDataList)
|
||||
}
|
||||
|
||||
const handleCheck = (index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
|
||||
if (value.has(pageId)) {
|
||||
if (!searchValue) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
value.delete(item)
|
||||
}
|
||||
|
||||
value.delete(pageId)
|
||||
}
|
||||
else {
|
||||
if (!searchValue) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
value.add(item)
|
||||
}
|
||||
|
||||
value.add(pageId)
|
||||
}
|
||||
|
||||
onSelect(new Set([...value]))
|
||||
}
|
||||
|
||||
const handlePreview = (index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
|
||||
setLocalPreviewPageId(pageId)
|
||||
|
||||
if (onPreview)
|
||||
onPreview(pageId)
|
||||
}
|
||||
|
||||
if (!currentDataList.length) {
|
||||
return (
|
||||
<div className='flex items-center justify-center h-[296px] text-[13px] text-gray-500'>
|
||||
{t('common.dataSource.notion.selector.noSearchResult')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
className='py-2'
|
||||
height={296}
|
||||
itemCount={currentDataList.length}
|
||||
itemSize={28}
|
||||
width='100%'
|
||||
itemKey={(index, data) => data.dataList[index].page_id}
|
||||
itemData={{
|
||||
dataList: currentDataList,
|
||||
handleToggle,
|
||||
checkedIds: value,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId: currentPreviewPageId,
|
||||
pagesMap,
|
||||
}}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageSelector
|
||||
@ -0,0 +1,15 @@
|
||||
.search-icon {
|
||||
background: url(../assets/search.svg) center center;
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
background: url(../assets/clear.svg) center center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex-basis: 200px;
|
||||
width: 0;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import s from './index.module.css'
|
||||
|
||||
type SearchInputProps = {
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
}
|
||||
const SearchInput = ({
|
||||
value,
|
||||
onChange,
|
||||
}: SearchInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
onChange('')
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(s['input-wrapper'], 'flex items-center px-2 h-7 rounded-md', `${value ? 'bg-white' : 'bg-gray-100'}`)}>
|
||||
<div className={cn(s['search-icon'], 'mr-[6px] w-4 h-4')} />
|
||||
<input
|
||||
className='grow text-[13px] bg-inherit border-0 outline-0 appearance-none'
|
||||
value={value}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
||||
placeholder={t('common.dataSource.notion.selector.searchPages') || ''}
|
||||
/>
|
||||
{
|
||||
value && (
|
||||
<div
|
||||
className={cn(s['clear-icon'], 'ml-1 w-4 h-4 cursor-pointer')}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
@ -0,0 +1,9 @@
|
||||
.down-arrow {
|
||||
background: url(../assets/down-arrow.svg) center center no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.popup {
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
z-index: 10;
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import cn from 'classnames'
|
||||
import NotionIcon from '../../notion-icon'
|
||||
import s from './index.module.css'
|
||||
import type { DataSourceNotionWorkspace } from '@/models/common'
|
||||
|
||||
type WorkspaceSelectorProps = {
|
||||
value: string
|
||||
items: Omit<DataSourceNotionWorkspace, 'total'>[]
|
||||
onSelect: (v: string) => void
|
||||
}
|
||||
export default function WorkspaceSelector({
|
||||
value,
|
||||
items,
|
||||
onSelect,
|
||||
}: WorkspaceSelectorProps) {
|
||||
const { t } = useTranslation()
|
||||
const currentWorkspace = items.find(item => item.workspace_id === value)
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<Menu.Button className={`flex items-center justify-center h-7 rounded-md hover:bg-gray-50 ${open && 'bg-gray-50'} cursor-pointer`}>
|
||||
<NotionIcon
|
||||
className='ml-1 mr-2'
|
||||
src={currentWorkspace?.workspace_icon}
|
||||
name={currentWorkspace?.workspace_name}
|
||||
/>
|
||||
<div className='mr-1 w-[90px] text-left text-sm font-medium text-gray-700 truncate' title={currentWorkspace?.workspace_name}>{currentWorkspace?.workspace_name}</div>
|
||||
<div className='mr-1 px-1 h-[18px] bg-primary-50 rounded-lg text-xs font-medium text-primary-600'>{currentWorkspace?.pages.length}</div>
|
||||
<div className={cn(s['down-arrow'], 'mr-2 w-3 h-3')} />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
s.popup,
|
||||
`absolute left-0 top-8 w-80
|
||||
origin-top-right rounded-lg bg-white
|
||||
border-[0.5px] border-gray-200`,
|
||||
)}
|
||||
>
|
||||
<div className="p-1 max-h-50 overflow-auto">
|
||||
{
|
||||
items.map(item => (
|
||||
<Menu.Item key={item.workspace_id}>
|
||||
<div
|
||||
className='flex items-center px-3 h-9 hover:bg-gray-50 cursor-pointer'
|
||||
onClick={() => onSelect(item.workspace_id)}
|
||||
>
|
||||
<NotionIcon
|
||||
className='shrink-0 mr-2'
|
||||
src={item.workspace_icon}
|
||||
name={item.workspace_name}
|
||||
/>
|
||||
<div className='grow mr-2 text-sm text-gray-700 truncate' title={item.workspace_name}>{item.workspace_name}</div>
|
||||
<div className='shrink-0 text-xs font-medium text-primary-600'>
|
||||
{item.pages.length} {t('common.dataSource.notion.selector.pageSelected')}
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
type ProgressBarProps = {
|
||||
percent: number
|
||||
}
|
||||
const ProgressBar = ({
|
||||
percent = 0,
|
||||
}: ProgressBarProps) => {
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-2 w-[100px] bg-gray-100 rounded-lg'>
|
||||
<div
|
||||
className='h-1 bg-[#2970FF] rounded-lg'
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs font-medium text-gray-500'>{percent}%</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgressBar
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 249 B |
@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.75 4.5C6.7165 4.5 7.5 3.7165 7.5 2.75C7.5 1.7835 6.7165 1 5.75 1C4.7835 1 4 1.7835 4 2.75C4 3.7165 4.7835 4.5 5.75 4.5Z" fill="#444CE7"/>
|
||||
<path d="M3.48775 4.314C3.36842 4.14172 3.30875 4.05558 3.24448 4.02712C3.18679 4.00157 3.12605 3.99844 3.06603 4.01794C2.99918 4.03965 2.94661 4.10099 2.84146 4.22367C2.41951 4.71598 2.13172 5.32705 2.03543 6.00009H2C1.72386 6.00009 1.5 5.77623 1.5 5.50009C1.5 5.31565 1.59961 5.15388 1.75036 5.06668C1.98939 4.9284 2.07107 4.62254 1.9328 4.38351C1.79453 4.14448 1.48867 4.0628 1.24964 4.20107C0.802591 4.45967 0.5 4.94425 0.5 5.50009C0.5 6.32852 1.17157 7.00009 2 7.00009H2.03545C2.14342 7.75422 2.49192 8.43113 2.99997 8.94961L2.99997 10.1117C2.99994 10.1712 2.99992 10.2424 3.00504 10.305C3.01097 10.3776 3.02619 10.4816 3.08171 10.5906C3.15362 10.7317 3.26835 10.8465 3.40948 10.9184C3.51845 10.9739 3.62245 10.9891 3.69505 10.9951C3.7577 11.0002 3.82881 11.0001 3.88836 11.0001H4.86154C4.92109 11.0001 4.99224 11.0002 5.05488 10.9951C5.12749 10.9891 5.23149 10.9739 5.34046 10.9184C5.48158 10.8465 5.59632 10.7317 5.66822 10.5906C5.72375 10.4816 5.73897 10.3776 5.7449 10.305C5.75002 10.2424 5.75 10.1712 5.74997 10.1117L5.74997 10.0001H6.24998L6.24997 10.1115C6.24995 10.1711 6.24992 10.2422 6.25504 10.3048C6.26097 10.3775 6.2762 10.4815 6.33172 10.5904C6.40363 10.7315 6.51836 10.8463 6.65948 10.9182C6.76846 10.9737 6.87245 10.9889 6.94506 10.9949C7.0077 11 7.0788 11 7.13835 10.9999H8.11159C8.17113 11 8.24229 11 8.30493 10.9949C8.37753 10.9889 8.48153 10.9737 8.5905 10.9182C8.73162 10.8463 8.84636 10.7315 8.91827 10.5904C8.97379 10.4815 8.98901 10.3775 8.99494 10.3048C9.00006 10.2422 9.00004 10.1711 9.00001 10.1115L9.00001 9.66299C9.55312 9.40029 10.0258 8.99721 10.3726 8.49993L10.6116 8.49994C10.6711 8.49996 10.7423 8.49999 10.8049 8.49487C10.8775 8.48893 10.9815 8.47371 11.0905 8.41819C11.2316 8.34628 11.3464 8.23155 11.4183 8.09043C11.4738 7.98145 11.489 7.87746 11.4949 7.80485C11.5001 7.74221 11.5 7.67109 11.5 7.61154V5.88181C11.5 5.82509 11.5001 5.75735 11.4954 5.69761C11.49 5.62851 11.4763 5.5294 11.4257 5.42448C11.352 5.27143 11.2285 5.14794 11.0755 5.07422C10.9705 5.02369 10.8714 5.00992 10.8023 5.00454C10.7577 5.00106 10.7087 5.0002 10.6631 4.99999C10.4953 4.64662 10.2702 4.32616 10 4.05044L10 3.51615C10 3.43874 10.0001 3.35111 9.99335 3.27574C9.98593 3.19252 9.96656 3.06385 9.88754 2.93633C9.78902 2.77733 9.63465 2.66089 9.4547 2.60984C9.31038 2.56889 9.18134 2.58561 9.09929 2.60134C9.02497 2.61559 8.94073 2.63969 8.8663 2.66098L8.78839 2.68324C8.6859 2.71252 8.63465 2.72716 8.59861 2.75356C8.5638 2.77904 8.54252 2.80415 8.52309 2.84266C8.50297 2.88255 8.49603 2.94339 8.48215 3.06506C8.32585 4.43546 7.16224 5.5 5.75 5.5C4.81225 5.5 3.98413 5.03063 3.48775 4.314Z" fill="#444CE7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,11 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 0.5C6.27614 0.5 6.5 0.723858 6.5 1V2C6.5 2.27614 6.27614 2.5 6 2.5C5.72386 2.5 5.5 2.27614 5.5 2V1C5.5 0.723858 5.72386 0.5 6 0.5Z" fill="#FB6514"/>
|
||||
<path d="M2.81791 2.11092C2.62265 1.91566 2.30606 1.91566 2.1108 2.11092C1.91554 2.30619 1.91554 2.62277 2.1108 2.81803L2.81791 3.52514C3.01317 3.7204 3.32975 3.7204 3.52502 3.52514C3.72028 3.32988 3.72028 3.01329 3.52502 2.81803L2.81791 2.11092Z" fill="#FB6514"/>
|
||||
<path d="M0.5 6C0.5 5.72386 0.723858 5.5 1 5.5H2C2.27614 5.5 2.5 5.72386 2.5 6C2.5 6.27614 2.27614 6.5 2 6.5H1C0.723858 6.5 0.5 6.27614 0.5 6Z" fill="#FB6514"/>
|
||||
<path d="M10 5.5C9.72386 5.5 9.5 5.72386 9.5 6C9.5 6.27614 9.72386 6.5 10 6.5H11C11.2761 6.5 11.5 6.27614 11.5 6C11.5 5.72386 11.2761 5.5 11 5.5H10Z" fill="#FB6514"/>
|
||||
<path d="M9.18192 8.47482C8.98666 8.27955 8.67008 8.27955 8.47482 8.47482C8.27955 8.67008 8.27955 8.98666 8.47482 9.18192L9.18192 9.88903C9.37718 10.0843 9.69377 10.0843 9.88903 9.88903C10.0843 9.69377 10.0843 9.37718 9.88903 9.18192L9.18192 8.47482Z" fill="#FB6514"/>
|
||||
<path d="M9.88903 2.81803C10.0843 2.62277 10.0843 2.30619 9.88903 2.11092C9.69377 1.91566 9.37718 1.91566 9.18192 2.11092L8.47482 2.81803C8.27955 3.01329 8.27955 3.32988 8.47482 3.52514C8.67008 3.7204 8.98666 3.7204 9.18192 3.52514L9.88903 2.81803Z" fill="#FB6514"/>
|
||||
<path d="M6 9.5C6.27614 9.5 6.5 9.72386 6.5 10V11C6.5 11.2761 6.27614 11.5 6 11.5C5.72386 11.5 5.5 11.2761 5.5 11V10C5.5 9.72386 5.72386 9.5 6 9.5Z" fill="#FB6514"/>
|
||||
<path d="M3.52502 9.18192C3.72028 8.98666 3.72028 8.67008 3.52502 8.47482C3.32975 8.27955 3.01317 8.27955 2.81791 8.47482L2.1108 9.18192C1.91554 9.37718 1.91554 9.69377 2.1108 9.88903C2.30606 10.0843 2.62265 10.0843 2.81791 9.88903L3.52502 9.18192Z" fill="#FB6514"/>
|
||||
<path d="M6.44837 3.27869C6.36413 3.10804 6.19032 3 6.00001 3C5.8097 3 5.6359 3.10804 5.55166 3.27869L4.89538 4.60823L3.4277 4.82276C3.23942 4.85028 3.08308 4.98228 3.02439 5.16328C2.9657 5.34429 3.01484 5.54291 3.15115 5.67568L4.21275 6.70968L3.96221 8.17048C3.93004 8.35807 4.00716 8.54766 4.16115 8.65953C4.31514 8.77139 4.51928 8.78613 4.68774 8.69754L6.00001 8.00742L7.31229 8.69754C7.48075 8.78613 7.68489 8.77139 7.83888 8.65953C7.99287 8.54766 8.06999 8.35807 8.03782 8.17048L7.78728 6.70968L8.84888 5.67568C8.98519 5.54291 9.03433 5.34429 8.97564 5.16328C8.91695 4.98228 8.76061 4.85028 8.57233 4.82276L7.10465 4.60823L6.44837 3.27869Z" fill="#FB6514"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,111 @@
|
||||
.progressContainer {
|
||||
@apply relative pb-4 w-full;
|
||||
border-bottom: 0.5px solid #EAECF0;
|
||||
}
|
||||
.sourceItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 4px;
|
||||
height: 24px;
|
||||
background: #EFF4FF;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sourceItem.error {
|
||||
background: #FEE4E2;
|
||||
}
|
||||
.sourceItem.success {
|
||||
background: #D1FADF;
|
||||
}
|
||||
.progressbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background-color: #B2CCFF;
|
||||
}
|
||||
.sourceItem .info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.sourceItem .info .name {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #101828;
|
||||
}
|
||||
.sourceItem.success .info .name {
|
||||
color: #05603A;
|
||||
}
|
||||
.sourceItem .percent {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #344054;
|
||||
}
|
||||
.sourceItem .error {
|
||||
color: #D92D20;
|
||||
}
|
||||
.sourceItem .success {
|
||||
color: #05603A;
|
||||
}
|
||||
|
||||
|
||||
.cost {
|
||||
@apply flex justify-between items-center text-xs text-gray-700;
|
||||
}
|
||||
.embeddingStatus {
|
||||
@apply flex items-center justify-between text-gray-900 font-medium text-sm mr-2;
|
||||
}
|
||||
.commonIcon {
|
||||
@apply w-3 h-3 mr-1 inline-block align-middle;
|
||||
}
|
||||
.highIcon {
|
||||
mask-image: url(../assets/star.svg);
|
||||
@apply bg-orange-500;
|
||||
}
|
||||
.economyIcon {
|
||||
background-color: #444ce7;
|
||||
mask-image: url(../assets/normal.svg);
|
||||
}
|
||||
.tokens {
|
||||
@apply text-xs font-medium px-1;
|
||||
}
|
||||
.price {
|
||||
color: #f79009;
|
||||
@apply text-xs font-medium;
|
||||
}
|
||||
|
||||
.fileIcon {
|
||||
@apply w-4 h-4 mr-1 bg-center bg-no-repeat;
|
||||
background-image: url(../assets/unknow.svg);
|
||||
background-size: 16px;
|
||||
}
|
||||
.fileIcon.csv {
|
||||
background-image: url(../assets/csv.svg);
|
||||
}
|
||||
|
||||
.fileIcon.xlsx,
|
||||
.fileIcon.xls {
|
||||
background-image: url(../assets/xlsx.svg);
|
||||
}
|
||||
.fileIcon.pdf {
|
||||
background-image: url(../assets/pdf.svg);
|
||||
}
|
||||
.fileIcon.html,
|
||||
.fileIcon.htm {
|
||||
background-image: url(../assets/html.svg);
|
||||
}
|
||||
.fileIcon.md,
|
||||
.fileIcon.markdown {
|
||||
background-image: url(../assets/md.svg);
|
||||
}
|
||||
.fileIcon.txt {
|
||||
background-image: url(../assets/txt.svg);
|
||||
}
|
||||
.fileIcon.json {
|
||||
background-image: url(../assets/json.svg);
|
||||
}
|
||||
@ -0,0 +1,242 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { omit } from 'lodash-es'
|
||||
import { ArrowRightIcon } from '@heroicons/react/24/solid'
|
||||
import { useGetState } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import s from './index.module.css'
|
||||
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { FullDocumentDetail, IndexingStatusResponse, ProcessRuleResponse } from '@/models/datasets'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchIndexingEstimateBatch, fetchProcessRule } from '@/service/datasets'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
batchId: string
|
||||
documents?: FullDocumentDetail[]
|
||||
indexingType?: string
|
||||
}
|
||||
|
||||
const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const segmentationRuleMap = {
|
||||
mode: t('datasetDocuments.embedding.mode'),
|
||||
segmentLength: t('datasetDocuments.embedding.segmentLength'),
|
||||
textCleaning: t('datasetDocuments.embedding.textCleaning'),
|
||||
}
|
||||
|
||||
const getRuleName = (key: string) => {
|
||||
if (key === 'remove_extra_spaces')
|
||||
return t('datasetCreation.stepTwo.removeExtraSpaces')
|
||||
|
||||
if (key === 'remove_urls_emails')
|
||||
return t('datasetCreation.stepTwo.removeUrlEmails')
|
||||
|
||||
if (key === 'remove_stopwords')
|
||||
return t('datasetCreation.stepTwo.removeStopwords')
|
||||
}
|
||||
|
||||
const getValue = useCallback((field: string) => {
|
||||
let value: string | number | undefined = '-'
|
||||
switch (field) {
|
||||
case 'mode':
|
||||
value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)
|
||||
break
|
||||
case 'segmentLength':
|
||||
value = sourceData?.rules?.segmentation?.max_tokens
|
||||
break
|
||||
default:
|
||||
value = sourceData?.mode === 'automatic'
|
||||
? (t('datasetDocuments.embedding.automatic') as string)
|
||||
// eslint-disable-next-line array-callback-return
|
||||
: sourceData?.rules?.pre_processing_rules?.map((rule) => {
|
||||
if (rule.enabled)
|
||||
return getRuleName(rule.id)
|
||||
}).filter(Boolean).join(';')
|
||||
break
|
||||
}
|
||||
return value
|
||||
}, [sourceData])
|
||||
|
||||
return <div className='flex flex-col pt-8 pb-10 first:mt-0'>
|
||||
{Object.keys(segmentationRuleMap).map((field) => {
|
||||
return <FieldInfo
|
||||
key={field}
|
||||
label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}
|
||||
displayedValue={String(getValue(field))}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getFirstDocument = documents[0]
|
||||
|
||||
const [indexingStatusBatchDetail, setIndexingStatusDetail, getIndexingStatusDetail] = useGetState<IndexingStatusResponse[]>([])
|
||||
const fetchIndexingStatus = async () => {
|
||||
const status = await doFetchIndexingStatus({ datasetId, batchId })
|
||||
setIndexingStatusDetail(status.data)
|
||||
}
|
||||
|
||||
const [runId, setRunId, getRunId] = useGetState<any>(null)
|
||||
|
||||
const stopQueryStatus = () => {
|
||||
clearInterval(getRunId())
|
||||
}
|
||||
|
||||
const startQueryStatus = () => {
|
||||
const runId = setInterval(() => {
|
||||
const indexingStatusBatchDetail = getIndexingStatusDetail()
|
||||
const isCompleted = indexingStatusBatchDetail.every(indexingStatusDetail => ['completed', 'error'].includes(indexingStatusDetail.indexing_status))
|
||||
if (isCompleted) {
|
||||
stopQueryStatus()
|
||||
return
|
||||
}
|
||||
fetchIndexingStatus()
|
||||
}, 2500)
|
||||
setRunId(runId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchIndexingStatus()
|
||||
startQueryStatus()
|
||||
return () => {
|
||||
stopQueryStatus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// get rule
|
||||
const { data: ruleDetail, error: ruleError } = useSWR({
|
||||
action: 'fetchProcessRule',
|
||||
params: { documentId: getFirstDocument.id },
|
||||
}, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
// get cost
|
||||
const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({
|
||||
action: 'fetchIndexingEstimateBatch',
|
||||
datasetId,
|
||||
batchId,
|
||||
}, apiParams => fetchIndexingEstimateBatch(omit(apiParams, 'action')), {
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const navToDocumentList = () => {
|
||||
router.push(`/datasets/${datasetId}/documents`)
|
||||
}
|
||||
|
||||
const isEmbedding = useMemo(() => {
|
||||
return indexingStatusBatchDetail.some((indexingStatusDetail: { indexing_status: any }) => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''))
|
||||
}, [indexingStatusBatchDetail])
|
||||
const isEmbeddingCompleted = useMemo(() => {
|
||||
return indexingStatusBatchDetail.every((indexingStatusDetail: { indexing_status: any }) => ['completed', 'error'].includes(indexingStatusDetail?.indexing_status || ''))
|
||||
}, [indexingStatusBatchDetail])
|
||||
|
||||
const getSourceName = (id: string) => {
|
||||
const doc = documents.find(document => document.id === id)
|
||||
return doc?.name
|
||||
}
|
||||
const getFileType = (name?: string) => name?.split('.').pop() || 'txt'
|
||||
const getSourcePercent = (detail: IndexingStatusResponse) => {
|
||||
const completedCount = detail.completed_segments || 0
|
||||
const totalCount = detail.total_segments || 0
|
||||
if (totalCount === 0)
|
||||
return 0
|
||||
const percent = Math.round(completedCount * 100 / totalCount)
|
||||
return percent > 100 ? 100 : percent
|
||||
}
|
||||
const getSourceType = (id: string) => {
|
||||
const doc = documents.find(document => document.id === id)
|
||||
return doc?.data_source_type as DataSourceType
|
||||
}
|
||||
const getIcon = (id: string) => {
|
||||
const doc = documents.find(document => document.id === id) as any // TODO type fix
|
||||
|
||||
return doc.data_source_info.notion_page_icon
|
||||
}
|
||||
const isSourceEmbedding = (detail: IndexingStatusResponse) => ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes(detail.indexing_status || '')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='h-5 flex justify-between items-center mb-5'>
|
||||
<div className={s.embeddingStatus}>
|
||||
{isEmbedding && t('datasetDocuments.embedding.processing')}
|
||||
{isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
|
||||
</div>
|
||||
<div className={s.cost}>
|
||||
{indexingType === 'high_quaility' && (
|
||||
<div className='flex items-center'>
|
||||
<div className={cn(s.commonIcon, s.highIcon)} />
|
||||
{t('datasetDocuments.embedding.highQuality')} · {t('datasetDocuments.embedding.estimate')}
|
||||
<span className={s.tokens}>{formatNumber(indexingEstimateDetail?.tokens || 0)}</span>tokens
|
||||
(<span className={s.price}>${formatNumber(indexingEstimateDetail?.total_price || 0)}</span>)
|
||||
</div>
|
||||
)}
|
||||
{indexingType === 'economy' && (
|
||||
<div className='flex items-center'>
|
||||
<div className={cn(s.commonIcon, s.economyIcon)} />
|
||||
{t('datasetDocuments.embedding.economy')} · {t('datasetDocuments.embedding.estimate')}
|
||||
<span className={s.tokens}>0</span>tokens
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.progressContainer}>
|
||||
{indexingStatusBatchDetail.map(indexingStatusDetail => (
|
||||
<div className={cn(
|
||||
s.sourceItem,
|
||||
indexingStatusDetail.indexing_status === 'error' && s.error,
|
||||
indexingStatusDetail.indexing_status === 'completed' && s.success,
|
||||
)}>
|
||||
{isSourceEmbedding(indexingStatusDetail) && (
|
||||
<div className={s.progressbar} style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }}/>
|
||||
)}
|
||||
<div className={s.info}>
|
||||
{getSourceType(indexingStatusDetail.id) === DataSourceType.FILE && (
|
||||
<div className={cn(s.fileIcon, s[getFileType(getSourceName(indexingStatusDetail.id))])}/>
|
||||
)}
|
||||
{getSourceType(indexingStatusDetail.id) === DataSourceType.NOTION && (
|
||||
<NotionIcon
|
||||
className='shrink-0 mr-1'
|
||||
type='page'
|
||||
src={getIcon(indexingStatusDetail.id)}
|
||||
/>
|
||||
)}
|
||||
<div className={s.name}>{getSourceName(indexingStatusDetail.id)}</div>
|
||||
</div>
|
||||
<div className='shrink-0'>
|
||||
{isSourceEmbedding(indexingStatusDetail) && (
|
||||
<div className={s.percent}>{`${getSourcePercent(indexingStatusDetail)}%`}</div>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'error' && (
|
||||
<div className={cn(s.percent, s.error)}>Error</div>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'completed' && (
|
||||
<div className={cn(s.percent, s.success)}>100%</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<RuleDetail sourceData={ruleDetail} />
|
||||
<div className='flex items-center gap-2 mt-10'>
|
||||
<Button className='w-fit' type='primary' onClick={navToDocumentList}>
|
||||
<span>{t('datasetCreation.stepThree.navTo')}</span>
|
||||
<ArrowRightIcon className='h-4 w-4 ml-2 stroke-current stroke-1' />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmbeddingProcess
|
||||
@ -0,0 +1,54 @@
|
||||
.filePreview {
|
||||
@apply flex flex-col border-l border-gray-200 shrink-0;
|
||||
width: 528px;
|
||||
background-color: #fcfcfd;
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
@apply border-b border-gray-200 shrink-0;
|
||||
margin: 42px 32px 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.previewHeader .title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #101828;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.previewHeader .fileName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #1D2939;
|
||||
}
|
||||
|
||||
.previewHeader .filetype {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.previewContent {
|
||||
@apply overflow-y-auto grow;
|
||||
padding: 20px 32px;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.previewContent .loading {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: #f9fafb center no-repeat url(../assets/Loading.svg);
|
||||
background-size: contain;
|
||||
}
|
||||
.fileContent {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { XMarkIcon } from '@heroicons/react/20/solid'
|
||||
import s from './index.module.css'
|
||||
import type { DataSourceNotionPage } from '@/models/common'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import { fetchNotionPagePreview } from '@/service/datasets'
|
||||
|
||||
type Page = DataSourceNotionPage & { workspace_id: string }
|
||||
type IProps = {
|
||||
currentPage?: Page
|
||||
hidePreview: () => void
|
||||
}
|
||||
|
||||
const NotionPagePreview = ({
|
||||
currentPage,
|
||||
hidePreview,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [previewContent, setPreviewContent] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const getPreviewContent = async () => {
|
||||
if (!currentPage)
|
||||
return
|
||||
try {
|
||||
const res = await fetchNotionPagePreview({
|
||||
workspaceID: currentPage.workspace_id,
|
||||
pageID: currentPage.page_id,
|
||||
pageType: currentPage.type,
|
||||
})
|
||||
setPreviewContent(res.content)
|
||||
setLoading(false)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage) {
|
||||
setLoading(true)
|
||||
getPreviewContent()
|
||||
}
|
||||
}, [currentPage])
|
||||
|
||||
return (
|
||||
<div className={cn(s.filePreview)}>
|
||||
<div className={cn(s.previewHeader)}>
|
||||
<div className={cn(s.title)}>
|
||||
<span>{t('datasetCreation.stepOne.pagePreview')}</span>
|
||||
<div className='flex items-center justify-center w-6 h-6 cursor-pointer' onClick={hidePreview}>
|
||||
<XMarkIcon className='h-4 w-4'></XMarkIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(s.fileName)}>
|
||||
<NotionIcon
|
||||
className='shrink-0 mr-1'
|
||||
type='page'
|
||||
src={currentPage?.page_icon}
|
||||
/>
|
||||
{currentPage?.page_name}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(s.previewContent)}>
|
||||
{loading && <div className={cn(s.loading)}/>}
|
||||
{!loading && (
|
||||
<div className={cn(s.fileContent)}>{previewContent}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotionPagePreview
|
||||
@ -0,0 +1,102 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { PlusIcon } from '@heroicons/react/24/solid'
|
||||
import cn from 'classnames'
|
||||
import Indicator from '../../../indicator'
|
||||
import Operate from './operate'
|
||||
import s from './style.module.css'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import { apiPrefix } from '@/config'
|
||||
import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
|
||||
|
||||
type DataSourceNotionProps = {
|
||||
workspaces: TDataSourceNotion[]
|
||||
}
|
||||
const DataSourceNotion = ({
|
||||
workspaces,
|
||||
}: DataSourceNotionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const connected = !!workspaces.length
|
||||
|
||||
return (
|
||||
<div className='mb-2 border-[0.5px] border-gray-200 bg-gray-50 rounded-xl'>
|
||||
<div className='flex items-center px-3 py-[9px]'>
|
||||
<div className={cn(s['notion-icon'], 'w-8 h-8 mr-3 border border-gray-100 rounded-lg')} />
|
||||
<div className='grow'>
|
||||
<div className='leading-5 text-sm font-medium text-gray-800'>
|
||||
{t('common.dataSource.notion.title')}
|
||||
</div>
|
||||
{
|
||||
!connected && (
|
||||
<div className='leading-5 text-xs text-gray-500'>
|
||||
{t('common.dataSource.notion.description')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!connected
|
||||
? (
|
||||
<Link
|
||||
className='flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 rounded-md text-xs font-medium text-gray-700 cursor-pointer'
|
||||
href={`${apiPrefix}/oauth/data-source/notion`}>
|
||||
{t('common.dataSource.connect')}
|
||||
</Link>
|
||||
)
|
||||
: (
|
||||
<Link
|
||||
href={`${apiPrefix}/oauth/data-source/notion`}
|
||||
className='flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md cursor-pointer'>
|
||||
<PlusIcon className='w-[14px] h-[14px] mr-[5px]' />
|
||||
{t('common.dataSource.notion.addWorkspace')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
connected && (
|
||||
<div className='flex items-center px-3 h-[18px]'>
|
||||
<div className='text-xs font-medium text-gray-500'>
|
||||
{t('common.dataSource.notion.connectedWorkspace')}
|
||||
</div>
|
||||
<div className='grow ml-3 border-t border-t-gray-100' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
connected && (
|
||||
<div className='px-3 pt-2 pb-3'>
|
||||
{
|
||||
workspaces.map(workspace => (
|
||||
<div className={cn(s['workspace-item'], 'flex items-center mb-1 py-1 pr-1 bg-white rounded-lg')} key={workspace.id}>
|
||||
<NotionIcon
|
||||
className='ml-3 mr-[6px]'
|
||||
src={workspace.source_info.workspace_icon}
|
||||
name={workspace.source_info.workspace_name}
|
||||
/>
|
||||
<div className='grow py-[7px] leading-[18px] text-[13px] font-medium text-gray-700 truncate' title={workspace.source_info.workspace_name}>{workspace.source_info.workspace_name}</div>
|
||||
{
|
||||
workspace.is_bound
|
||||
? <Indicator className='shrink-0 mr-[6px]' />
|
||||
: <Indicator className='shrink-0 mr-[6px]' color='yellow' />
|
||||
}
|
||||
<div className='shrink-0 mr-3 text-xs font-medium'>
|
||||
{
|
||||
workspace.is_bound
|
||||
? t('common.dataSource.notion.connected')
|
||||
: t('common.dataSource.notion.disconnected')
|
||||
}
|
||||
</div>
|
||||
<div className='mr-2 w-[1px] h-3 bg-gray-100' />
|
||||
<Operate workspace={workspace} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataSourceNotion
|
||||
@ -0,0 +1,14 @@
|
||||
.file-icon {
|
||||
background: url(../../../../assets/file.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.sync-icon {
|
||||
background: url(../../../../assets/sync.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.trash-icon {
|
||||
background: url(../../../../assets/trash.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { EllipsisHorizontalIcon } from '@heroicons/react/24/solid'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import cn from 'classnames'
|
||||
import s from './index.module.css'
|
||||
import { apiPrefix } from '@/config'
|
||||
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { DataSourceNotion } from '@/models/common'
|
||||
|
||||
type OperateProps = {
|
||||
workspace: DataSourceNotion
|
||||
}
|
||||
export default function Operate({
|
||||
workspace,
|
||||
}: OperateProps) {
|
||||
const itemClassName = `
|
||||
flex px-3 py-2 hover:bg-gray-50 text-sm text-gray-700
|
||||
cursor-pointer
|
||||
`
|
||||
const itemIconClassName = `
|
||||
mr-2 mt-[2px] w-4 h-4
|
||||
`
|
||||
const { t } = useTranslation()
|
||||
const { mutate } = useSWRConfig()
|
||||
|
||||
const updateIntegrates = () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
})
|
||||
mutate({ url: 'data-source/integrates' })
|
||||
}
|
||||
const handleSync = async () => {
|
||||
await syncDataSourceNotion({ url: `/oauth/data-source/notion/${workspace.id}/sync` })
|
||||
updateIntegrates()
|
||||
}
|
||||
const handleRemove = async () => {
|
||||
await updateDataSourceNotionAction({ url: `/data-source/integrates/${workspace.id}/disable` })
|
||||
updateIntegrates()
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<Menu.Button className={`flex items-center justify-center w-8 h-8 rounded-lg hover:bg-gray-100 ${open && 'bg-gray-100'}`}>
|
||||
<EllipsisHorizontalIcon className='w-4 h-4' />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="
|
||||
absolute right-0 top-9 w-60 max-w-80
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
|
||||
shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_rgba(0,0,0,0.05)]
|
||||
"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
<Link
|
||||
className={itemClassName}
|
||||
href={`${apiPrefix}/oauth/data-source/notion`}>
|
||||
<div className={cn(s['file-icon'], itemIconClassName)}></div>
|
||||
<div>
|
||||
<div className='leading-5'>{t('common.dataSource.notion.changeAuthorizedPages')}</div>
|
||||
<div className='leading-5 text-xs text-gray-500'>
|
||||
{workspace.source_info.total} {t('common.dataSource.notion.pagesAuthorized')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<div className={itemClassName} onClick={handleSync}>
|
||||
<div className={cn(s['sync-icon'], itemIconClassName)} />
|
||||
<div className='leading-5'>{t('common.dataSource.notion.sync')}</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<Menu.Item>
|
||||
<div className='p-1'>
|
||||
<div className={itemClassName} onClick={handleRemove}>
|
||||
<div className={cn(s['trash-icon'], itemIconClassName)} />
|
||||
<div className='leading-5'>{t('common.dataSource.notion.remove')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
.notion-icon {
|
||||
background: #ffffff url(../../../assets/notion.svg) center center no-repeat;
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.workspace-item {
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.workspace-item:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DataSourceNotion from './data-source-notion'
|
||||
import { fetchDataSource } from '@/service/common'
|
||||
|
||||
export default function DataSourcePage() {
|
||||
const { t } = useTranslation()
|
||||
const { data } = useSWR({ url: 'data-source/integrates' }, fetchDataSource)
|
||||
const notionWorkspaces = data?.data.filter(item => item.provider === 'notion') || []
|
||||
|
||||
return (
|
||||
<div className='mb-8'>
|
||||
<div className='mb-2 text-sm font-medium text-gray-900'>{t('common.dataSource.add')}</div>
|
||||
<DataSourceNotion workspaces={notionWorkspaces} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.66659 9.98845C10.1231 9.93732 11.4455 9.71981 12.461 9.38077C13.0418 9.18687 13.5593 8.94109 13.9452 8.6353C14.3245 8.33478 14.6666 7.89967 14.6666 7.33341V3.33341C14.6666 2.76545 14.3207 2.33047 13.9411 2.03132C13.5539 1.72624 13.0351 1.48065 12.4534 1.28675C11.283 0.896612 9.70849 0.666748 7.99992 0.666748C6.29135 0.666748 4.71686 0.896612 3.54646 1.28675C2.96474 1.48065 2.44589 1.72624 2.05878 2.03132C1.67918 2.33047 1.33325 2.76545 1.33325 3.33341V7.33341C1.33325 7.89967 1.67538 8.33478 2.05463 8.6353C2.44053 8.94109 2.958 9.18687 3.53881 9.38077C4.55435 9.71981 5.87675 9.93732 7.33325 9.98845V11.4472C6.76498 11.6481 6.31458 12.0985 6.11372 12.6667H1.99992C1.63173 12.6667 1.33325 12.9652 1.33325 13.3334C1.33325 13.7016 1.63173 14.0001 1.99992 14.0001H6.11372C6.38828 14.7769 7.12911 15.3334 7.99992 15.3334C8.87073 15.3334 9.61156 14.7769 9.88612 14.0001H13.9999C14.3681 14.0001 14.6666 13.7016 14.6666 13.3334C14.6666 12.9652 14.3681 12.6667 13.9999 12.6667H9.88612C9.68526 12.0985 9.23486 11.6481 8.66659 11.4472V9.98845ZM2.66659 3.33337C2.66659 3.33337 2.66657 3.32994 2.66862 3.32322C2.67089 3.31579 2.67647 3.30111 2.68997 3.27913C2.71827 3.23303 2.77597 3.16373 2.88408 3.07853C3.1042 2.90506 3.46403 2.71968 3.9681 2.55166C4.96927 2.21793 6.39478 2.00008 7.99992 2.00008C9.60506 2.00008 11.0306 2.21793 12.0317 2.55166C12.5358 2.71968 12.8956 2.90506 13.1158 3.07853C13.2239 3.16373 13.2816 3.23303 13.3099 3.27913C13.3234 3.30111 13.329 3.31579 13.3312 3.32322C13.3333 3.32994 13.3333 3.33337 13.3333 3.33337C13.3333 3.33337 13.3332 3.33722 13.3312 3.34361C13.329 3.35104 13.3234 3.36572 13.3099 3.3877C13.2816 3.4338 13.2239 3.5031 13.1158 3.5883C12.8956 3.76177 12.5358 3.94715 12.0317 4.11517C11.0306 4.4489 9.60506 4.66675 7.99992 4.66675C6.39478 4.66675 4.96927 4.4489 3.9681 4.11517C3.46403 3.94715 3.1042 3.76177 2.88408 3.5883C2.77597 3.5031 2.71827 3.4338 2.68997 3.3877C2.67647 3.36572 2.67089 3.35104 2.66862 3.34361C2.6667 3.33731 2.66659 3.33337 2.66659 3.33337Z" fill="#155EEF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.33333 13.3333C9.33333 14.0696 8.73638 14.6666 8 14.6666C7.26362 14.6666 6.66667 14.0696 6.66667 13.3333M9.33333 13.3333C9.33333 12.5969 8.73638 11.9999 8 11.9999M9.33333 13.3333H14M6.66667 13.3333C6.66667 12.5969 7.26362 11.9999 8 11.9999M6.66667 13.3333H2M8 11.9999V9.33325M14 3.33325C14 4.43782 11.3137 5.33325 8 5.33325C4.68629 5.33325 2 4.43782 2 3.33325M14 3.33325C14 2.22868 11.3137 1.33325 8 1.33325C4.68629 1.33325 2 2.22868 2 3.33325M14 3.33325V7.33325C14 8.43992 11.3333 9.33325 8 9.33325M2 3.33325V7.33325C2 8.43992 4.66667 9.33325 8 9.33325" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 755 B |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3333 6.99992V4.53325C13.3333 3.41315 13.3333 2.85309 13.1153 2.42527C12.9236 2.04895 12.6176 1.74299 12.2413 1.55124C11.8135 1.33325 11.2534 1.33325 10.1333 1.33325H5.86666C4.74655 1.33325 4.1865 1.33325 3.75868 1.55124C3.38235 1.74299 3.07639 2.04895 2.88464 2.42527C2.66666 2.85309 2.66666 3.41315 2.66666 4.53325V11.4666C2.66666 12.5867 2.66666 13.1467 2.88464 13.5746C3.07639 13.9509 3.38235 14.2569 3.75868 14.4486C4.1865 14.6666 4.74655 14.6666 5.86666 14.6666H7.99999M9.33332 7.33325H5.33332M6.66666 9.99992H5.33332M10.6667 4.66659H5.33332M12 13.9999V9.99992M9.99999 11.9999H14" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 787 B |
@ -0,0 +1,12 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5364_42310)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5725 18.2611L1.4229 15.5832C0.905706 14.9389 0.625 14.1466 0.625 13.3312V3.63437C0.625 2.4129 1.60224 1.39936 2.86295 1.31328L12.8326 0.632614C13.5569 0.583164 14.2768 0.775682 14.8717 1.17794L18.3745 3.5462C19.0015 3.97012 19.375 4.66312 19.375 5.40266V16.427C19.375 17.6223 18.4141 18.6121 17.1798 18.688L6.11458 19.3692C5.12958 19.4298 4.17749 19.0148 3.5725 18.2611Z" fill="white"/>
|
||||
<path d="M7.03006 8.48663V8.35968C7.03006 8.03787 7.28779 7.77098 7.61997 7.7488L10.0396 7.58726L13.3857 12.5146V8.19003L12.5244 8.07522V8.01492C12.5244 7.68933 12.788 7.42068 13.1244 7.40344L15.326 7.29066V7.60749C15.326 7.75622 15.2154 7.88343 15.0638 7.90907L14.534 7.99868V15.0022L13.8691 15.2309C13.3136 15.4219 12.6952 15.2174 12.3772 14.7376L9.12879 9.83568V14.5143L10.1287 14.7056L10.1147 14.7984C10.0711 15.0889 9.82028 15.3086 9.51687 15.3221L7.03006 15.4328C6.99718 15.1204 7.23132 14.8409 7.55431 14.807L7.88143 14.7726V8.53447L7.03006 8.48663Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9218 1.85418L2.95217 2.53485C2.35499 2.57562 1.89209 3.05572 1.89209 3.63431V13.3311C1.89209 13.8748 2.07923 14.4029 2.42402 14.8325L4.57362 17.5104C4.92117 17.9433 5.46812 18.1817 6.03397 18.1469L17.0991 17.4658C17.6663 17.4309 18.1078 16.9761 18.1078 16.4269V5.4026C18.1078 5.06281 17.9362 4.74441 17.6481 4.54963L14.1453 2.18137C13.7883 1.94002 13.3564 1.82451 12.9218 1.85418ZM3.44654 3.78556C3.30788 3.6829 3.37387 3.46903 3.54806 3.45654L12.9889 2.77938C13.2897 2.75781 13.5886 2.84064 13.8318 3.01299L15.7261 4.35502C15.798 4.40597 15.7642 4.51596 15.6752 4.5208L5.67742 5.06454C5.37485 5.081 5.0762 4.99211 4.83563 4.814L3.44654 3.78556ZM5.20848 6.76913C5.20848 6.44433 5.47088 6.17604 5.80642 6.15777L16.3769 5.5821C16.7039 5.56429 16.9792 5.81577 16.9792 6.13232V15.6782C16.9792 16.0024 16.7177 16.2705 16.3829 16.2895L5.8793 16.8871C5.51537 16.9079 5.20848 16.6282 5.20848 16.2759V6.76913Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5364_42310">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.69773 13.1783C7.29715 13.8879 9.20212 13.8494 10.8334 12.9075C13.5438 11.3427 14.4724 7.87704 12.9076 5.16672L12.7409 4.87804M3.09233 10.8335C1.52752 8.12314 2.45615 4.65746 5.16647 3.09265C6.7978 2.15081 8.70277 2.11227 10.3022 2.82185M1.66226 10.8892L3.48363 11.3773L3.97166 9.5559M12.0284 6.44393L12.5164 4.62256L14.3378 5.1106" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2H10M2 4H14M12.6667 4L12.1991 11.0129C12.129 12.065 12.0939 12.5911 11.8667 12.99C11.6666 13.3412 11.3648 13.6235 11.0011 13.7998C10.588 14 10.0607 14 9.00623 14H6.99377C5.93927 14 5.41202 14 4.99889 13.7998C4.63517 13.6235 4.33339 13.3412 4.13332 12.99C3.90607 12.5911 3.871 12.065 3.80086 11.0129L3.33333 4M6.66667 7V10.3333M9.33333 7V10.3333" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 546 B |
@ -1,6 +1,8 @@
|
||||
import { createContext } from 'use-context-selector'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
const DatasetDetailContext = createContext<{ indexingTechnique?: string; dataset?: DataSet }>({})
|
||||
|
||||
export const useDatasetDetailContext = () => useContext(DatasetDetailContext)
|
||||
|
||||
export default DatasetDetailContext
|
||||
|
||||