Compare commits
10 Commits
1f96c95b42
...
fa9b7b259c
| Author | SHA1 | Date | |
|---|---|---|---|
| fa9b7b259c | |||
| 14616cf845 | |||
| d2915f6984 | |||
| ccce8beeeb | |||
| 3d2e0f1a1b | |||
| 918d5a9ff8 | |||
| 7d05d4ced7 | |||
| dbdda0fbab | |||
| cf7fdd274b | |||
| 982ed233a2 |
2
.github/workflows/tests.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
name: ragflow_tests
|
||||
# https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution
|
||||
# https://github.com/orgs/community/discussions/26261
|
||||
if: ${{ github.event_name != 'pull_request_target' || (contains(github.event.pull_request.labels.*.name, 'ci') && github.event.pull_request.mergeable == true) }}
|
||||
if: ${{ github.event_name != 'pull_request_target' || (contains(github.event.pull_request.labels.*.name, 'ci') && github.event.pull_request.mergeable != false) }}
|
||||
runs-on: [ "self-hosted", "ragflow-test" ]
|
||||
steps:
|
||||
# https://github.com/hmarr/debug-action
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@ -25,6 +24,7 @@ from typing import Any, Union, Tuple
|
||||
|
||||
from agent.component import component_class
|
||||
from agent.component.base import ComponentBase
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.task_service import has_canceled
|
||||
from common.misc_utils import get_uuid, hash_str2int
|
||||
from common.exceptions import TaskCanceledException
|
||||
@ -372,7 +372,7 @@ class Canvas(Graph):
|
||||
for k in kwargs.keys():
|
||||
if k in ["query", "user_id", "files"] and kwargs[k]:
|
||||
if k == "files":
|
||||
self.globals[f"sys.{k}"] = self.get_files(kwargs[k])
|
||||
self.globals[f"sys.{k}"] = FileService.get_files(kwargs[k])
|
||||
else:
|
||||
self.globals[f"sys.{k}"] = kwargs[k]
|
||||
if not self.globals["sys.conversation_turns"] :
|
||||
@ -621,22 +621,6 @@ class Canvas(Graph):
|
||||
def get_component_input_elements(self, cpnnm):
|
||||
return self.components[cpnnm]["obj"].get_input_elements()
|
||||
|
||||
def get_files(self, files: Union[None, list[dict]]) -> list[str]:
|
||||
from api.db.services.file_service import FileService
|
||||
if not files:
|
||||
return []
|
||||
def image_to_base64(file):
|
||||
return "data:{};base64,{}".format(file["mime_type"],
|
||||
base64.b64encode(FileService.get_blob(file["created_by"], file["id"])).decode("utf-8"))
|
||||
exe = ThreadPoolExecutor(max_workers=5)
|
||||
threads = []
|
||||
for file in files:
|
||||
if file["mime_type"].find("image") >=0:
|
||||
threads.append(exe.submit(image_to_base64, file))
|
||||
continue
|
||||
threads.append(exe.submit(FileService.parse, file["name"], FileService.get_blob(file["created_by"], file["id"]), True, file["created_by"]))
|
||||
return [th.result() for th in threads]
|
||||
|
||||
def tool_use_callback(self, agent_id: str, func_name: str, params: dict, result: Any, elapsed_time=None):
|
||||
agent_ids = agent_id.split("-->")
|
||||
agent_name = self.get_component_name(agent_ids[0])
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
from agent.component.fillup import UserFillUpParam, UserFillUp
|
||||
from api.db.services.file_service import FileService
|
||||
|
||||
|
||||
class BeginParam(UserFillUpParam):
|
||||
@ -48,7 +49,7 @@ class Begin(UserFillUp):
|
||||
if v.get("optional") and v.get("value", None) is None:
|
||||
v = None
|
||||
else:
|
||||
v = self._canvas.get_files([v["value"]])
|
||||
v = FileService.get_files([v["value"]])
|
||||
else:
|
||||
v = v.get("value")
|
||||
self.set_output(k, v)
|
||||
|
||||
@ -15,13 +15,10 @@
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from functools import partial
|
||||
import trio
|
||||
from quart import request, Response, make_response
|
||||
from agent.component import LLM
|
||||
from api.db import CanvasCategory, FileType
|
||||
from api.db import CanvasCategory
|
||||
from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
@ -38,7 +35,6 @@ from peewee import MySQLDatabase, PostgresqlDatabase
|
||||
from api.db.db_models import APIToken, Task
|
||||
import time
|
||||
|
||||
from api.utils.file_utils import filename_type, read_potential_broken_pdf
|
||||
from rag.flow.pipeline import Pipeline
|
||||
from rag.nlp import search
|
||||
from rag.utils.redis_conn import REDIS_CONN
|
||||
@ -250,71 +246,10 @@ async def upload(canvas_id):
|
||||
return get_data_error_result(message="canvas not found.")
|
||||
|
||||
user_id = cvs["user_id"]
|
||||
def structured(filename, filetype, blob, content_type):
|
||||
nonlocal user_id
|
||||
if filetype == FileType.PDF.value:
|
||||
blob = read_potential_broken_pdf(blob)
|
||||
|
||||
location = get_uuid()
|
||||
FileService.put_blob(user_id, location, blob)
|
||||
|
||||
return {
|
||||
"id": location,
|
||||
"name": filename,
|
||||
"size": sys.getsizeof(blob),
|
||||
"extension": filename.split(".")[-1].lower(),
|
||||
"mime_type": content_type,
|
||||
"created_by": user_id,
|
||||
"created_at": time.time(),
|
||||
"preview_url": None
|
||||
}
|
||||
|
||||
if request.args.get("url"):
|
||||
from crawl4ai import (
|
||||
AsyncWebCrawler,
|
||||
BrowserConfig,
|
||||
CrawlerRunConfig,
|
||||
DefaultMarkdownGenerator,
|
||||
PruningContentFilter,
|
||||
CrawlResult
|
||||
)
|
||||
try:
|
||||
url = request.args.get("url")
|
||||
filename = re.sub(r"\?.*", "", url.split("/")[-1])
|
||||
async def adownload():
|
||||
browser_config = BrowserConfig(
|
||||
headless=True,
|
||||
verbose=False,
|
||||
)
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
crawler_config = CrawlerRunConfig(
|
||||
markdown_generator=DefaultMarkdownGenerator(
|
||||
content_filter=PruningContentFilter()
|
||||
),
|
||||
pdf=True,
|
||||
screenshot=False
|
||||
)
|
||||
result: CrawlResult = await crawler.arun(
|
||||
url=url,
|
||||
config=crawler_config
|
||||
)
|
||||
return result
|
||||
page = trio.run(adownload())
|
||||
if page.pdf:
|
||||
if filename.split(".")[-1].lower() != "pdf":
|
||||
filename += ".pdf"
|
||||
return get_json_result(data=structured(filename, "pdf", page.pdf, page.response_headers["content-type"]))
|
||||
|
||||
return get_json_result(data=structured(filename, "html", str(page.markdown).encode("utf-8"), page.response_headers["content-type"], user_id))
|
||||
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
files = await request.files
|
||||
file = files['file']
|
||||
file = files['file'] if files and files.get("file") else None
|
||||
try:
|
||||
DocumentService.check_doc_health(user_id, file.filename)
|
||||
return get_json_result(data=structured(file.filename, filename_type(file.filename), file.read(), file.content_type))
|
||||
return get_json_result(data=FileService.upload_info(user_id, file, request.args.get("url")))
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@ -28,8 +28,8 @@ from api.db import InputType
|
||||
from api.db.services.connector_service import ConnectorService, SyncLogsService
|
||||
from api.utils.api_utils import get_data_error_result, get_json_result, validate_request
|
||||
from common.constants import RetCode, TaskStatus
|
||||
from common.data_source.config import GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI, DocumentSource
|
||||
from common.data_source.google_util.constant import GOOGLE_DRIVE_WEB_OAUTH_POPUP_TEMPLATE, GOOGLE_SCOPES
|
||||
from common.data_source.config import GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI, GMAIL_WEB_OAUTH_REDIRECT_URI, DocumentSource
|
||||
from common.data_source.google_util.constant import GOOGLE_WEB_OAUTH_POPUP_TEMPLATE, GOOGLE_SCOPES
|
||||
from common.misc_utils import get_uuid
|
||||
from rag.utils.redis_conn import REDIS_CONN
|
||||
from api.apps import login_required, current_user
|
||||
@ -122,12 +122,30 @@ GOOGLE_WEB_FLOW_RESULT_PREFIX = "google_drive_web_flow_result"
|
||||
WEB_FLOW_TTL_SECS = 15 * 60
|
||||
|
||||
|
||||
def _web_state_cache_key(flow_id: str) -> str:
|
||||
return f"{GOOGLE_WEB_FLOW_STATE_PREFIX}:{flow_id}"
|
||||
def _web_state_cache_key(flow_id: str, source_type: str | None = None) -> str:
|
||||
"""Return Redis key for web OAuth state.
|
||||
|
||||
The default prefix keeps backward compatibility for Google Drive.
|
||||
When source_type == "gmail", a different prefix is used so that
|
||||
Drive/Gmail flows don't clash in Redis.
|
||||
"""
|
||||
if source_type == "gmail":
|
||||
prefix = "gmail_web_flow_state"
|
||||
else:
|
||||
prefix = GOOGLE_WEB_FLOW_STATE_PREFIX
|
||||
return f"{prefix}:{flow_id}"
|
||||
|
||||
|
||||
def _web_result_cache_key(flow_id: str) -> str:
|
||||
return f"{GOOGLE_WEB_FLOW_RESULT_PREFIX}:{flow_id}"
|
||||
def _web_result_cache_key(flow_id: str, source_type: str | None = None) -> str:
|
||||
"""Return Redis key for web OAuth result.
|
||||
|
||||
Mirrors _web_state_cache_key logic for result storage.
|
||||
"""
|
||||
if source_type == "gmail":
|
||||
prefix = "gmail_web_flow_result"
|
||||
else:
|
||||
prefix = GOOGLE_WEB_FLOW_RESULT_PREFIX
|
||||
return f"{prefix}:{flow_id}"
|
||||
|
||||
|
||||
def _load_credentials(payload: str | dict[str, Any]) -> dict[str, Any]:
|
||||
@ -146,19 +164,22 @@ def _get_web_client_config(credentials: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"web": web_section}
|
||||
|
||||
|
||||
async def _render_web_oauth_popup(flow_id: str, success: bool, message: str):
|
||||
async def _render_web_oauth_popup(flow_id: str, success: bool, message: str, source="drive"):
|
||||
status = "success" if success else "error"
|
||||
auto_close = "window.close();" if success else ""
|
||||
escaped_message = escape(message)
|
||||
payload_json = json.dumps(
|
||||
{
|
||||
"type": "ragflow-google-drive-oauth",
|
||||
# TODO(google-oauth): include connector type (drive/gmail) in payload type if needed
|
||||
"type": f"ragflow-google-{source}-oauth",
|
||||
"status": status,
|
||||
"flowId": flow_id or "",
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
html = GOOGLE_DRIVE_WEB_OAUTH_POPUP_TEMPLATE.format(
|
||||
# TODO(google-oauth): title/heading/message may need to reflect drive/gmail based on cached type
|
||||
html = GOOGLE_WEB_OAUTH_POPUP_TEMPLATE.format(
|
||||
title=f"Google {source.capitalize()} Authorization",
|
||||
heading="Authorization complete" if success else "Authorization failed",
|
||||
message=escaped_message,
|
||||
payload_json=payload_json,
|
||||
@ -169,20 +190,33 @@ async def _render_web_oauth_popup(flow_id: str, success: bool, message: str):
|
||||
return response
|
||||
|
||||
|
||||
@manager.route("/google-drive/oauth/web/start", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/google/oauth/web/start", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("credentials")
|
||||
async def start_google_drive_web_oauth():
|
||||
if not GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI:
|
||||
async def start_google_web_oauth():
|
||||
source = request.args.get("type", "google-drive")
|
||||
if source not in ("google-drive", "gmail"):
|
||||
return get_json_result(code=RetCode.ARGUMENT_ERROR, message="Invalid Google OAuth type.")
|
||||
|
||||
if source == "gmail":
|
||||
redirect_uri = GMAIL_WEB_OAUTH_REDIRECT_URI
|
||||
scopes = GOOGLE_SCOPES[DocumentSource.GMAIL]
|
||||
else:
|
||||
redirect_uri = GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI if source == "google-drive" else GMAIL_WEB_OAUTH_REDIRECT_URI
|
||||
scopes = GOOGLE_SCOPES[DocumentSource.GOOGLE_DRIVE if source == "google-drive" else DocumentSource.GMAIL]
|
||||
|
||||
if not redirect_uri:
|
||||
return get_json_result(
|
||||
code=RetCode.SERVER_ERROR,
|
||||
message="Google Drive OAuth redirect URI is not configured on the server.",
|
||||
message="Google OAuth redirect URI is not configured on the server.",
|
||||
)
|
||||
|
||||
req = await request.json or {}
|
||||
raw_credentials = req.get("credentials", "")
|
||||
|
||||
try:
|
||||
credentials = _load_credentials(raw_credentials)
|
||||
print(credentials)
|
||||
except ValueError as exc:
|
||||
return get_json_result(code=RetCode.ARGUMENT_ERROR, message=str(exc))
|
||||
|
||||
@ -199,8 +233,8 @@ async def start_google_drive_web_oauth():
|
||||
|
||||
flow_id = str(uuid.uuid4())
|
||||
try:
|
||||
flow = Flow.from_client_config(client_config, scopes=GOOGLE_SCOPES[DocumentSource.GOOGLE_DRIVE])
|
||||
flow.redirect_uri = GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI
|
||||
flow = Flow.from_client_config(client_config, scopes=scopes)
|
||||
flow.redirect_uri = redirect_uri
|
||||
authorization_url, _ = flow.authorization_url(
|
||||
access_type="offline",
|
||||
include_granted_scopes="true",
|
||||
@ -219,7 +253,7 @@ async def start_google_drive_web_oauth():
|
||||
"client_config": client_config,
|
||||
"created_at": int(time.time()),
|
||||
}
|
||||
REDIS_CONN.set_obj(_web_state_cache_key(flow_id), cache_payload, WEB_FLOW_TTL_SECS)
|
||||
REDIS_CONN.set_obj(_web_state_cache_key(flow_id, source), cache_payload, WEB_FLOW_TTL_SECS)
|
||||
|
||||
return get_json_result(
|
||||
data={
|
||||
@ -230,60 +264,122 @@ async def start_google_drive_web_oauth():
|
||||
)
|
||||
|
||||
|
||||
@manager.route("/google-drive/oauth/web/callback", methods=["GET"]) # noqa: F821
|
||||
async def google_drive_web_oauth_callback():
|
||||
@manager.route("/gmail/oauth/web/callback", methods=["GET"]) # noqa: F821
|
||||
async def google_gmail_web_oauth_callback():
|
||||
state_id = request.args.get("state")
|
||||
error = request.args.get("error")
|
||||
source = "gmail"
|
||||
if source != 'gmail':
|
||||
return await _render_web_oauth_popup("", False, "Invalid Google OAuth type.", source)
|
||||
|
||||
error_description = request.args.get("error_description") or error
|
||||
|
||||
if not state_id:
|
||||
return await _render_web_oauth_popup("", False, "Missing OAuth state parameter.")
|
||||
return await _render_web_oauth_popup("", False, "Missing OAuth state parameter.", source)
|
||||
|
||||
state_cache = REDIS_CONN.get(_web_state_cache_key(state_id))
|
||||
state_cache = REDIS_CONN.get(_web_state_cache_key(state_id, source))
|
||||
if not state_cache:
|
||||
return await _render_web_oauth_popup(state_id, False, "Authorization session expired. Please restart from the main window.")
|
||||
return await _render_web_oauth_popup(state_id, False, "Authorization session expired. Please restart from the main window.", source)
|
||||
|
||||
state_obj = json.loads(state_cache)
|
||||
client_config = state_obj.get("client_config")
|
||||
if not client_config:
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id))
|
||||
return await _render_web_oauth_popup(state_id, False, "Authorization session was invalid. Please retry.")
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
return await _render_web_oauth_popup(state_id, False, "Authorization session was invalid. Please retry.", source)
|
||||
|
||||
if error:
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id))
|
||||
return await _render_web_oauth_popup(state_id, False, error_description or "Authorization was cancelled.")
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
return await _render_web_oauth_popup(state_id, False, error_description or "Authorization was cancelled.", source)
|
||||
|
||||
code = request.args.get("code")
|
||||
if not code:
|
||||
return await _render_web_oauth_popup(state_id, False, "Missing authorization code from Google.")
|
||||
return await _render_web_oauth_popup(state_id, False, "Missing authorization code from Google.", source)
|
||||
|
||||
try:
|
||||
flow = Flow.from_client_config(client_config, scopes=GOOGLE_SCOPES[DocumentSource.GOOGLE_DRIVE])
|
||||
flow.redirect_uri = GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI
|
||||
# TODO(google-oauth): branch scopes/redirect_uri based on source_type (drive vs gmail)
|
||||
flow = Flow.from_client_config(client_config, scopes=GOOGLE_SCOPES[DocumentSource.GMAIL])
|
||||
flow.redirect_uri = GMAIL_WEB_OAUTH_REDIRECT_URI
|
||||
flow.fetch_token(code=code)
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
logging.exception("Failed to exchange Google OAuth code: %s", exc)
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id))
|
||||
return await _render_web_oauth_popup(state_id, False, "Failed to exchange tokens with Google. Please retry.")
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
return await _render_web_oauth_popup(state_id, False, "Failed to exchange tokens with Google. Please retry.", source)
|
||||
|
||||
creds_json = flow.credentials.to_json()
|
||||
result_payload = {
|
||||
"user_id": state_obj.get("user_id"),
|
||||
"credentials": creds_json,
|
||||
}
|
||||
REDIS_CONN.set_obj(_web_result_cache_key(state_id), result_payload, WEB_FLOW_TTL_SECS)
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id))
|
||||
REDIS_CONN.set_obj(_web_result_cache_key(state_id, source), result_payload, WEB_FLOW_TTL_SECS)
|
||||
|
||||
return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.")
|
||||
print("\n\n", _web_result_cache_key(state_id, source), "\n\n")
|
||||
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
|
||||
return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.", source)
|
||||
|
||||
|
||||
@manager.route("/google-drive/oauth/web/result", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/google-drive/oauth/web/callback", methods=["GET"]) # noqa: F821
|
||||
async def google_drive_web_oauth_callback():
|
||||
state_id = request.args.get("state")
|
||||
error = request.args.get("error")
|
||||
source = "google-drive"
|
||||
if source not in ("google-drive", "gmail"):
|
||||
return await _render_web_oauth_popup("", False, "Invalid Google OAuth type.", source)
|
||||
|
||||
error_description = request.args.get("error_description") or error
|
||||
|
||||
if not state_id:
|
||||
return await _render_web_oauth_popup("", False, "Missing OAuth state parameter.", source)
|
||||
|
||||
state_cache = REDIS_CONN.get(_web_state_cache_key(state_id, source))
|
||||
if not state_cache:
|
||||
return await _render_web_oauth_popup(state_id, False, "Authorization session expired. Please restart from the main window.", source)
|
||||
|
||||
state_obj = json.loads(state_cache)
|
||||
client_config = state_obj.get("client_config")
|
||||
if not client_config:
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
return await _render_web_oauth_popup(state_id, False, "Authorization session was invalid. Please retry.", source)
|
||||
|
||||
if error:
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
return await _render_web_oauth_popup(state_id, False, error_description or "Authorization was cancelled.", source)
|
||||
|
||||
code = request.args.get("code")
|
||||
if not code:
|
||||
return await _render_web_oauth_popup(state_id, False, "Missing authorization code from Google.", source)
|
||||
|
||||
try:
|
||||
# TODO(google-oauth): branch scopes/redirect_uri based on source_type (drive vs gmail)
|
||||
flow = Flow.from_client_config(client_config, scopes=GOOGLE_SCOPES[DocumentSource.GOOGLE_DRIVE])
|
||||
flow.redirect_uri = GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI
|
||||
flow.fetch_token(code=code)
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
logging.exception("Failed to exchange Google OAuth code: %s", exc)
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
return await _render_web_oauth_popup(state_id, False, "Failed to exchange tokens with Google. Please retry.", source)
|
||||
|
||||
creds_json = flow.credentials.to_json()
|
||||
result_payload = {
|
||||
"user_id": state_obj.get("user_id"),
|
||||
"credentials": creds_json,
|
||||
}
|
||||
REDIS_CONN.set_obj(_web_result_cache_key(state_id, source), result_payload, WEB_FLOW_TTL_SECS)
|
||||
REDIS_CONN.delete(_web_state_cache_key(state_id, source))
|
||||
|
||||
return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.", source)
|
||||
|
||||
@manager.route("/google/oauth/web/result", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("flow_id")
|
||||
async def poll_google_drive_web_result():
|
||||
async def poll_google_web_result():
|
||||
req = await request.json or {}
|
||||
source = request.args.get("type")
|
||||
if source not in ("google-drive", "gmail"):
|
||||
return get_json_result(code=RetCode.ARGUMENT_ERROR, message="Invalid Google OAuth type.")
|
||||
flow_id = req.get("flow_id")
|
||||
cache_raw = REDIS_CONN.get(_web_result_cache_key(flow_id))
|
||||
cache_raw = REDIS_CONN.get(_web_result_cache_key(flow_id, source))
|
||||
if not cache_raw:
|
||||
return get_json_result(code=RetCode.RUNNING, message="Authorization is still pending.")
|
||||
|
||||
@ -291,5 +387,5 @@ async def poll_google_drive_web_result():
|
||||
if result.get("user_id") != current_user.id:
|
||||
return get_json_result(code=RetCode.PERMISSION_ERROR, message="You are not allowed to access this authorization result.")
|
||||
|
||||
REDIS_CONN.delete(_web_result_cache_key(flow_id))
|
||||
REDIS_CONN.delete(_web_result_cache_key(flow_id, source))
|
||||
return get_json_result(data={"credentials": result.get("credentials")})
|
||||
|
||||
@ -607,7 +607,7 @@ async def get_image(image_id):
|
||||
@login_required
|
||||
@validate_request("conversation_id")
|
||||
async def upload_and_parse():
|
||||
files = await request.file
|
||||
files = await request.files
|
||||
if "file" not in files:
|
||||
return get_json_result(data=False, message="No file part!", code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
@ -705,3 +705,12 @@ async def set_meta():
|
||||
return get_json_result(data=True)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
@manager.route("/upload_info", methods=["POST"]) # noqa: F821
|
||||
async def upload_info():
|
||||
files = await request.files
|
||||
file = files['file'] if files and files.get("file") else None
|
||||
try:
|
||||
return get_json_result(data=FileService.upload_info(current_user.id, file, request.args.get("url")))
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
@ -1446,6 +1446,9 @@ async def retrieval_test(tenant_id):
|
||||
metadata_condition = req.get("metadata_condition", {}) or {}
|
||||
metas = DocumentService.get_meta_by_kbs(kb_ids)
|
||||
doc_ids = meta_filter(metas, convert_conditions(metadata_condition), metadata_condition.get("logic", "and"))
|
||||
# If metadata_condition has conditions but no docs match, return empty result
|
||||
if not doc_ids and metadata_condition.get("conditions"):
|
||||
return get_result(data={"total": 0, "chunks": [], "doc_aggs": {}})
|
||||
if metadata_condition and not doc_ids:
|
||||
doc_ids = ["-999"]
|
||||
similarity_threshold = float(req.get("similarity_threshold", 0.2))
|
||||
|
||||
@ -121,8 +121,8 @@ async def login():
|
||||
response_data = user.to_json()
|
||||
user.access_token = get_uuid()
|
||||
login_user(user)
|
||||
user.update_time = (current_timestamp(),)
|
||||
user.update_date = (datetime_format(datetime.now()),)
|
||||
user.update_time = current_timestamp()
|
||||
user.update_date = datetime_format(datetime.now())
|
||||
user.save()
|
||||
msg = "Welcome back!"
|
||||
|
||||
@ -1002,8 +1002,8 @@ async def forget():
|
||||
# Auto login (reuse login flow)
|
||||
user.access_token = get_uuid()
|
||||
login_user(user)
|
||||
user.update_time = (current_timestamp(),)
|
||||
user.update_date = (datetime_format(datetime.now()),)
|
||||
user.update_time = current_timestamp()
|
||||
user.update_date = datetime_format(datetime.now())
|
||||
user.save()
|
||||
msg = "Password reset successful. Logged in."
|
||||
return construct_response(data=user.to_json(), auth=user.get_id(), message=msg)
|
||||
|
||||
@ -25,6 +25,7 @@ import trio
|
||||
from langfuse import Langfuse
|
||||
from peewee import fn
|
||||
from agentic_reasoning import DeepResearcher
|
||||
from api.db.services.file_service import FileService
|
||||
from common.constants import LLMType, ParserType, StatusEnum
|
||||
from api.db.db_models import DB, Dialog
|
||||
from api.db.services.common_service import CommonService
|
||||
@ -380,8 +381,11 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
retriever = settings.retriever
|
||||
questions = [m["content"] for m in messages if m["role"] == "user"][-3:]
|
||||
attachments = kwargs["doc_ids"].split(",") if "doc_ids" in kwargs else []
|
||||
attachments_= ""
|
||||
if "doc_ids" in messages[-1]:
|
||||
attachments = messages[-1]["doc_ids"]
|
||||
if "files" in messages[-1]:
|
||||
attachments_ = "\n\n".join(FileService.get_files(messages[-1]["files"]))
|
||||
|
||||
prompt_config = dialog.prompt_config
|
||||
field_map = KnowledgebaseService.get_field_map(dialog.kb_ids)
|
||||
@ -451,7 +455,7 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
),
|
||||
)
|
||||
|
||||
for think in reasoner.thinking(kbinfos, " ".join(questions)):
|
||||
for think in reasoner.thinking(kbinfos, attachments_ + " ".join(questions)):
|
||||
if isinstance(think, str):
|
||||
thought = think
|
||||
knowledges = [t for t in think.split("\n") if t]
|
||||
@ -503,7 +507,7 @@ def chat(dialog, messages, stream=True, **kwargs):
|
||||
kwargs["knowledge"] = "\n------\n" + "\n\n------\n\n".join(knowledges)
|
||||
gen_conf = dialog.llm_setting
|
||||
|
||||
msg = [{"role": "system", "content": prompt_config["system"].format(**kwargs)}]
|
||||
msg = [{"role": "system", "content": prompt_config["system"].format(**kwargs)+attachments_}]
|
||||
prompt4citation = ""
|
||||
if knowledges and (prompt_config.get("quote", True) and kwargs.get("quote", True)):
|
||||
prompt4citation = citation_prompt()
|
||||
|
||||
@ -13,10 +13,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import asyncio
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from peewee import fn
|
||||
|
||||
@ -520,7 +525,7 @@ class FileService(CommonService):
|
||||
if img_base64 and file_type == FileType.VISUAL.value:
|
||||
return GptV4.image2base64(blob)
|
||||
cks = FACTORY.get(FileService.get_parser(filename_type(filename), filename, ""), naive).chunk(filename, blob, **kwargs)
|
||||
return "\n".join([ck["content_with_weight"] for ck in cks])
|
||||
return f"\n -----------------\nFile: {filename}\nContent as following: \n" + "\n".join([ck["content_with_weight"] for ck in cks])
|
||||
|
||||
@staticmethod
|
||||
def get_parser(doc_type, filename, default):
|
||||
@ -588,3 +593,80 @@ class FileService(CommonService):
|
||||
errors += str(e)
|
||||
|
||||
return errors
|
||||
|
||||
@staticmethod
|
||||
def upload_info(user_id, file, url: str|None=None):
|
||||
def structured(filename, filetype, blob, content_type):
|
||||
nonlocal user_id
|
||||
if filetype == FileType.PDF.value:
|
||||
blob = read_potential_broken_pdf(blob)
|
||||
|
||||
location = get_uuid()
|
||||
FileService.put_blob(user_id, location, blob)
|
||||
|
||||
return {
|
||||
"id": location,
|
||||
"name": filename,
|
||||
"size": sys.getsizeof(blob),
|
||||
"extension": filename.split(".")[-1].lower(),
|
||||
"mime_type": content_type,
|
||||
"created_by": user_id,
|
||||
"created_at": time.time(),
|
||||
"preview_url": None
|
||||
}
|
||||
|
||||
if url:
|
||||
from crawl4ai import (
|
||||
AsyncWebCrawler,
|
||||
BrowserConfig,
|
||||
CrawlerRunConfig,
|
||||
DefaultMarkdownGenerator,
|
||||
PruningContentFilter,
|
||||
CrawlResult
|
||||
)
|
||||
filename = re.sub(r"\?.*", "", url.split("/")[-1])
|
||||
async def adownload():
|
||||
browser_config = BrowserConfig(
|
||||
headless=True,
|
||||
verbose=False,
|
||||
)
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
crawler_config = CrawlerRunConfig(
|
||||
markdown_generator=DefaultMarkdownGenerator(
|
||||
content_filter=PruningContentFilter()
|
||||
),
|
||||
pdf=True,
|
||||
screenshot=False
|
||||
)
|
||||
result: CrawlResult = await crawler.arun(
|
||||
url=url,
|
||||
config=crawler_config
|
||||
)
|
||||
return result
|
||||
page = asyncio.run(adownload())
|
||||
if page.pdf:
|
||||
if filename.split(".")[-1].lower() != "pdf":
|
||||
filename += ".pdf"
|
||||
return structured(filename, "pdf", page.pdf, page.response_headers["content-type"])
|
||||
|
||||
return structured(filename, "html", str(page.markdown).encode("utf-8"), page.response_headers["content-type"], user_id)
|
||||
|
||||
DocumentService.check_doc_health(user_id, file.filename)
|
||||
return structured(file.filename, filename_type(file.filename), file.read(), file.content_type)
|
||||
|
||||
@staticmethod
|
||||
def get_files(self, files: Union[None, list[dict]]) -> list[str]:
|
||||
if not files:
|
||||
return []
|
||||
def image_to_base64(file):
|
||||
return "data:{};base64,{}".format(file["mime_type"],
|
||||
base64.b64encode(FileService.get_blob(file["created_by"], file["id"])).decode("utf-8"))
|
||||
exe = ThreadPoolExecutor(max_workers=5)
|
||||
threads = []
|
||||
for file in files:
|
||||
if file["mime_type"].find("image") >=0:
|
||||
threads.append(exe.submit(image_to_base64, file))
|
||||
continue
|
||||
threads.append(exe.submit(FileService.parse, file["name"], FileService.get_blob(file["created_by"], file["id"]), True, file["created_by"]))
|
||||
return [th.result() for th in threads]
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
from collections import Counter
|
||||
import string
|
||||
from typing import Annotated, Any, Literal
|
||||
from uuid import UUID
|
||||
|
||||
@ -25,6 +26,7 @@ from pydantic import (
|
||||
StringConstraints,
|
||||
ValidationError,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_core import PydanticCustomError
|
||||
from werkzeug.exceptions import BadRequest, UnsupportedMediaType
|
||||
@ -361,10 +363,9 @@ class CreateDatasetReq(Base):
|
||||
description: Annotated[str | None, Field(default=None, max_length=65535)]
|
||||
embedding_model: Annotated[str | None, Field(default=None, max_length=255, serialization_alias="embd_id")]
|
||||
permission: Annotated[Literal["me", "team"], Field(default="me", min_length=1, max_length=16)]
|
||||
chunk_method: Annotated[
|
||||
Literal["naive", "book", "email", "laws", "manual", "one", "paper", "picture", "presentation", "qa", "table", "tag"],
|
||||
Field(default="naive", min_length=1, max_length=32, serialization_alias="parser_id"),
|
||||
]
|
||||
chunk_method: Annotated[str | None, Field(default=None, serialization_alias="parser_id")]
|
||||
parse_type: Annotated[int | None, Field(default=None, ge=0, le=64)]
|
||||
pipeline_id: Annotated[str | None, Field(default=None, min_length=32, max_length=32, serialization_alias="pipeline_id")]
|
||||
parser_config: Annotated[ParserConfig | None, Field(default=None)]
|
||||
|
||||
@field_validator("avatar", mode="after")
|
||||
@ -525,6 +526,93 @@ class CreateDatasetReq(Base):
|
||||
raise PydanticCustomError("string_too_long", "Parser config exceeds size limit (max 65,535 characters). Current size: {actual}", {"actual": len(json_str)})
|
||||
return v
|
||||
|
||||
@field_validator("pipeline_id", mode="after")
|
||||
@classmethod
|
||||
def validate_pipeline_id(cls, v: str | None) -> str | None:
|
||||
"""Validate pipeline_id as 32-char lowercase hex string if provided.
|
||||
|
||||
Rules:
|
||||
- None or empty string: treat as None (not set)
|
||||
- Must be exactly length 32
|
||||
- Must contain only hex digits (0-9a-fA-F); normalized to lowercase
|
||||
"""
|
||||
if v is None:
|
||||
return None
|
||||
if v == "":
|
||||
return None
|
||||
if len(v) != 32:
|
||||
raise PydanticCustomError("format_invalid", "pipeline_id must be 32 hex characters")
|
||||
if any(ch not in string.hexdigits for ch in v):
|
||||
raise PydanticCustomError("format_invalid", "pipeline_id must be hexadecimal")
|
||||
return v.lower()
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_parser_dependency(self) -> "CreateDatasetReq":
|
||||
"""
|
||||
Mixed conditional validation:
|
||||
- If parser_id is omitted (field not set):
|
||||
* If both parse_type and pipeline_id are omitted → default chunk_method = "naive"
|
||||
* If both parse_type and pipeline_id are provided → allow ingestion pipeline mode
|
||||
- If parser_id is provided (valid enum) → parse_type and pipeline_id must be None (disallow mixed usage)
|
||||
|
||||
Raises:
|
||||
PydanticCustomError with code 'dependency_error' on violation.
|
||||
"""
|
||||
# Omitted chunk_method (not in fields) logic
|
||||
if self.chunk_method is None and "chunk_method" not in self.model_fields_set:
|
||||
# All three absent → default naive
|
||||
if self.parse_type is None and self.pipeline_id is None:
|
||||
object.__setattr__(self, "chunk_method", "naive")
|
||||
return self
|
||||
# parser_id omitted: require BOTH parse_type & pipeline_id present (no partial allowed)
|
||||
if self.parse_type is None or self.pipeline_id is None:
|
||||
missing = []
|
||||
if self.parse_type is None:
|
||||
missing.append("parse_type")
|
||||
if self.pipeline_id is None:
|
||||
missing.append("pipeline_id")
|
||||
raise PydanticCustomError(
|
||||
"dependency_error",
|
||||
"parser_id omitted → required fields missing: {fields}",
|
||||
{"fields": ", ".join(missing)},
|
||||
)
|
||||
# Both provided → allow pipeline mode
|
||||
return self
|
||||
|
||||
# parser_id provided (valid): MUST NOT have parse_type or pipeline_id
|
||||
if isinstance(self.chunk_method, str):
|
||||
if self.parse_type is not None or self.pipeline_id is not None:
|
||||
invalid = []
|
||||
if self.parse_type is not None:
|
||||
invalid.append("parse_type")
|
||||
if self.pipeline_id is not None:
|
||||
invalid.append("pipeline_id")
|
||||
raise PydanticCustomError(
|
||||
"dependency_error",
|
||||
"parser_id provided → disallowed fields present: {fields}",
|
||||
{"fields": ", ".join(invalid)},
|
||||
)
|
||||
return self
|
||||
|
||||
@field_validator("chunk_method", mode="wrap")
|
||||
@classmethod
|
||||
def validate_chunk_method(cls, v: Any, handler) -> Any:
|
||||
"""Wrap validation to unify error messages, including type errors (e.g. list)."""
|
||||
allowed = {"naive", "book", "email", "laws", "manual", "one", "paper", "picture", "presentation", "qa", "table", "tag"}
|
||||
error_msg = "Input should be 'naive', 'book', 'email', 'laws', 'manual', 'one', 'paper', 'picture', 'presentation', 'qa', 'table' or 'tag'"
|
||||
# Omitted field: handler won't be invoked (wrap still gets value); None treated as explicit invalid
|
||||
if v is None:
|
||||
raise PydanticCustomError("literal_error", error_msg)
|
||||
try:
|
||||
# Run inner validation (type checking)
|
||||
result = handler(v)
|
||||
except Exception:
|
||||
raise PydanticCustomError("literal_error", error_msg)
|
||||
# After handler, enforce enumeration
|
||||
if not isinstance(result, str) or result == "" or result not in allowed:
|
||||
raise PydanticCustomError("literal_error", error_msg)
|
||||
return result
|
||||
|
||||
|
||||
class UpdateDatasetReq(CreateDatasetReq):
|
||||
dataset_id: Annotated[str, Field(...)]
|
||||
|
||||
@ -217,6 +217,7 @@ OAUTH_GOOGLE_DRIVE_CLIENT_SECRET = os.environ.get(
|
||||
"OAUTH_GOOGLE_DRIVE_CLIENT_SECRET", ""
|
||||
)
|
||||
GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI = os.environ.get("GOOGLE_DRIVE_WEB_OAUTH_REDIRECT_URI", "http://localhost:9380/v1/connector/google-drive/oauth/web/callback")
|
||||
GMAIL_WEB_OAUTH_REDIRECT_URI = os.environ.get("GMAIL_WEB_OAUTH_REDIRECT_URI", "http://localhost:9380/v1/connector/gmail/oauth/web/callback")
|
||||
|
||||
CONFLUENCE_OAUTH_TOKEN_URL = "https://auth.atlassian.com/oauth/token"
|
||||
RATE_LIMIT_MESSAGE_LOWERCASE = "Rate limit exceeded".lower()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from google.oauth2.credentials import Credentials as OAuthCredentials
|
||||
from google.oauth2.service_account import Credentials as ServiceAccountCredentials
|
||||
from googleapiclient.errors import HttpError
|
||||
@ -9,10 +9,10 @@ from common.data_source.config import INDEX_BATCH_SIZE, SLIM_BATCH_SIZE, Documen
|
||||
from common.data_source.google_util.auth import get_google_creds
|
||||
from common.data_source.google_util.constant import DB_CREDENTIALS_PRIMARY_ADMIN_KEY, MISSING_SCOPES_ERROR_STR, SCOPE_INSTRUCTIONS, USER_FIELDS
|
||||
from common.data_source.google_util.resource import get_admin_service, get_gmail_service
|
||||
from common.data_source.google_util.util import _execute_single_retrieval, execute_paginated_retrieval
|
||||
from common.data_source.google_util.util import _execute_single_retrieval, execute_paginated_retrieval, sanitize_filename, clean_string
|
||||
from common.data_source.interfaces import LoadConnector, PollConnector, SecondsSinceUnixEpoch, SlimConnectorWithPermSync
|
||||
from common.data_source.models import BasicExpertInfo, Document, ExternalAccess, GenerateDocumentsOutput, GenerateSlimDocumentOutput, SlimDocument, TextSection
|
||||
from common.data_source.utils import build_time_range_query, clean_email_and_extract_name, get_message_body, is_mail_service_disabled_error, time_str_to_utc
|
||||
from common.data_source.utils import build_time_range_query, clean_email_and_extract_name, get_message_body, is_mail_service_disabled_error, gmail_time_str_to_utc
|
||||
|
||||
# Constants for Gmail API fields
|
||||
THREAD_LIST_FIELDS = "nextPageToken, threads(id)"
|
||||
@ -67,7 +67,6 @@ def message_to_section(message: dict[str, Any]) -> tuple[TextSection, dict[str,
|
||||
message_data += f"{name}: {value}\n"
|
||||
|
||||
message_body_text: str = get_message_body(payload)
|
||||
|
||||
return TextSection(link=link, text=message_body_text + message_data), metadata
|
||||
|
||||
|
||||
@ -97,13 +96,15 @@ def thread_to_document(full_thread: dict[str, Any], email_used_to_fetch_thread:
|
||||
|
||||
if not semantic_identifier:
|
||||
semantic_identifier = message_metadata.get("subject", "")
|
||||
semantic_identifier = clean_string(semantic_identifier)
|
||||
semantic_identifier = sanitize_filename(semantic_identifier)
|
||||
|
||||
if message_metadata.get("updated_at"):
|
||||
updated_at = message_metadata.get("updated_at")
|
||||
|
||||
|
||||
updated_at_datetime = None
|
||||
if updated_at:
|
||||
updated_at_datetime = time_str_to_utc(updated_at)
|
||||
updated_at_datetime = gmail_time_str_to_utc(updated_at)
|
||||
|
||||
thread_id = full_thread.get("id")
|
||||
if not thread_id:
|
||||
@ -115,15 +116,24 @@ def thread_to_document(full_thread: dict[str, Any], email_used_to_fetch_thread:
|
||||
if not semantic_identifier:
|
||||
semantic_identifier = "(no subject)"
|
||||
|
||||
combined_sections = "\n\n".join(
|
||||
sec.text for sec in sections if hasattr(sec, "text")
|
||||
)
|
||||
blob = combined_sections
|
||||
size_bytes = len(blob)
|
||||
extension = '.txt'
|
||||
|
||||
return Document(
|
||||
id=thread_id,
|
||||
semantic_identifier=semantic_identifier,
|
||||
sections=sections,
|
||||
blob=blob,
|
||||
size_bytes=size_bytes,
|
||||
extension=extension,
|
||||
source=DocumentSource.GMAIL,
|
||||
primary_owners=primary_owners,
|
||||
secondary_owners=secondary_owners,
|
||||
doc_updated_at=updated_at_datetime,
|
||||
metadata={},
|
||||
metadata=message_metadata,
|
||||
external_access=ExternalAccess(
|
||||
external_user_emails={email_used_to_fetch_thread},
|
||||
external_user_group_ids=set(),
|
||||
@ -214,15 +224,13 @@ class GmailConnector(LoadConnector, PollConnector, SlimConnectorWithPermSync):
|
||||
q=query,
|
||||
continue_on_404_or_403=True,
|
||||
):
|
||||
full_threads = _execute_single_retrieval(
|
||||
full_thread = _execute_single_retrieval(
|
||||
retrieval_function=gmail_service.users().threads().get,
|
||||
list_key=None,
|
||||
userId=user_email,
|
||||
fields=THREAD_FIELDS,
|
||||
id=thread["id"],
|
||||
continue_on_404_or_403=True,
|
||||
)
|
||||
full_thread = list(full_threads)[0]
|
||||
doc = thread_to_document(full_thread, user_email)
|
||||
if doc is None:
|
||||
continue
|
||||
@ -310,4 +318,30 @@ class GmailConnector(LoadConnector, PollConnector, SlimConnectorWithPermSync):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
import time
|
||||
import os
|
||||
from common.data_source.google_util.util import get_credentials_from_env
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
try:
|
||||
email = os.environ.get("GMAIL_TEST_EMAIL", "newyorkupperbay@gmail.com")
|
||||
creds = get_credentials_from_env(email, oauth=True, source="gmail")
|
||||
print("Credentials loaded successfully")
|
||||
print(f"{creds=}")
|
||||
|
||||
connector = GmailConnector(batch_size=2)
|
||||
print("GmailConnector initialized")
|
||||
connector.load_credentials(creds)
|
||||
print("Credentials loaded into connector")
|
||||
|
||||
print("Gmail is ready to use")
|
||||
|
||||
for file in connector._fetch_threads(
|
||||
int(time.time()) - 1 * 24 * 60 * 60,
|
||||
int(time.time()),
|
||||
):
|
||||
print("new batch","-"*80)
|
||||
for f in file:
|
||||
print(f)
|
||||
print("\n\n")
|
||||
except Exception as e:
|
||||
logging.exception(f"Error loading credentials: {e}")
|
||||
@ -1,7 +1,6 @@
|
||||
"""Google Drive connector"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@ -32,7 +31,6 @@ from common.data_source.google_drive.file_retrieval import (
|
||||
from common.data_source.google_drive.model import DriveRetrievalStage, GoogleDriveCheckpoint, GoogleDriveFileType, RetrievedDriveFile, StageCompletion
|
||||
from common.data_source.google_util.auth import get_google_creds
|
||||
from common.data_source.google_util.constant import DB_CREDENTIALS_PRIMARY_ADMIN_KEY, MISSING_SCOPES_ERROR_STR, USER_FIELDS
|
||||
from common.data_source.google_util.oauth_flow import ensure_oauth_token_dict
|
||||
from common.data_source.google_util.resource import GoogleDriveService, get_admin_service, get_drive_service
|
||||
from common.data_source.google_util.util import GoogleFields, execute_paginated_retrieval, get_file_owners
|
||||
from common.data_source.google_util.util_threadpool_concurrency import ThreadSafeDict
|
||||
@ -1138,39 +1136,6 @@ class GoogleDriveConnector(SlimConnectorWithPermSync, CheckpointedConnectorWithP
|
||||
return GoogleDriveCheckpoint.model_validate_json(checkpoint_json)
|
||||
|
||||
|
||||
def get_credentials_from_env(email: str, oauth: bool = False) -> dict:
|
||||
try:
|
||||
if oauth:
|
||||
raw_credential_string = os.environ["GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR"]
|
||||
else:
|
||||
raw_credential_string = os.environ["GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON_STR"]
|
||||
except KeyError:
|
||||
raise ValueError("Missing Google Drive credentials in environment variables")
|
||||
|
||||
try:
|
||||
credential_dict = json.loads(raw_credential_string)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError("Invalid JSON in Google Drive credentials")
|
||||
|
||||
if oauth:
|
||||
credential_dict = ensure_oauth_token_dict(credential_dict, DocumentSource.GOOGLE_DRIVE)
|
||||
|
||||
refried_credential_string = json.dumps(credential_dict)
|
||||
|
||||
DB_CREDENTIALS_DICT_TOKEN_KEY = "google_tokens"
|
||||
DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY = "google_service_account_key"
|
||||
DB_CREDENTIALS_PRIMARY_ADMIN_KEY = "google_primary_admin"
|
||||
DB_CREDENTIALS_AUTHENTICATION_METHOD = "authentication_method"
|
||||
|
||||
cred_key = DB_CREDENTIALS_DICT_TOKEN_KEY if oauth else DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY
|
||||
|
||||
return {
|
||||
cred_key: refried_credential_string,
|
||||
DB_CREDENTIALS_PRIMARY_ADMIN_KEY: email,
|
||||
DB_CREDENTIALS_AUTHENTICATION_METHOD: "uploaded",
|
||||
}
|
||||
|
||||
|
||||
class CheckpointOutputWrapper:
|
||||
"""
|
||||
Wraps a CheckpointOutput generator to give things back in a more digestible format.
|
||||
@ -1236,7 +1201,7 @@ def yield_all_docs_from_checkpoint_connector(
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
|
||||
from common.data_source.google_util.util import get_credentials_from_env
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
try:
|
||||
@ -1245,7 +1210,7 @@ if __name__ == "__main__":
|
||||
creds = get_credentials_from_env(email, oauth=True)
|
||||
print("Credentials loaded successfully")
|
||||
print(f"{creds=}")
|
||||
|
||||
sys.exit(0)
|
||||
connector = GoogleDriveConnector(
|
||||
include_shared_drives=False,
|
||||
shared_drive_urls=None,
|
||||
|
||||
@ -49,11 +49,11 @@ MISSING_SCOPES_ERROR_STR = "client not authorized for any of the scopes requeste
|
||||
SCOPE_INSTRUCTIONS = ""
|
||||
|
||||
|
||||
GOOGLE_DRIVE_WEB_OAUTH_POPUP_TEMPLATE = """<!DOCTYPE html>
|
||||
GOOGLE_WEB_OAUTH_POPUP_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Google Drive Authorization</title>
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from collections.abc import Callable, Iterator
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import unicodedata
|
||||
from googleapiclient.errors import HttpError # type: ignore # type: ignore
|
||||
|
||||
from common.data_source.config import DocumentSource
|
||||
from common.data_source.google_drive.model import GoogleDriveFileType
|
||||
from common.data_source.google_util.oauth_flow import ensure_oauth_token_dict
|
||||
|
||||
|
||||
# See https://developers.google.com/drive/api/reference/rest/v3/files/list for more
|
||||
@ -117,6 +122,7 @@ def _execute_single_retrieval(
|
||||
"""Execute a single retrieval from Google Drive API"""
|
||||
try:
|
||||
results = retrieval_function(**request_kwargs).execute()
|
||||
|
||||
except HttpError as e:
|
||||
if e.resp.status >= 500:
|
||||
results = retrieval_function()
|
||||
@ -148,5 +154,110 @@ def _execute_single_retrieval(
|
||||
error,
|
||||
)
|
||||
results = retrieval_function()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_credentials_from_env(email: str, oauth: bool = False, source="drive") -> dict:
|
||||
try:
|
||||
if oauth:
|
||||
raw_credential_string = os.environ["GOOGLE_OAUTH_CREDENTIALS_JSON_STR"]
|
||||
else:
|
||||
raw_credential_string = os.environ["GOOGLE_SERVICE_ACCOUNT_JSON_STR"]
|
||||
except KeyError:
|
||||
raise ValueError("Missing Google Drive credentials in environment variables")
|
||||
|
||||
try:
|
||||
credential_dict = json.loads(raw_credential_string)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError("Invalid JSON in Google Drive credentials")
|
||||
|
||||
if oauth and source == "drive":
|
||||
credential_dict = ensure_oauth_token_dict(credential_dict, DocumentSource.GOOGLE_DRIVE)
|
||||
else:
|
||||
credential_dict = ensure_oauth_token_dict(credential_dict, DocumentSource.GMAIL)
|
||||
|
||||
refried_credential_string = json.dumps(credential_dict)
|
||||
|
||||
DB_CREDENTIALS_DICT_TOKEN_KEY = "google_tokens"
|
||||
DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY = "google_service_account_key"
|
||||
DB_CREDENTIALS_PRIMARY_ADMIN_KEY = "google_primary_admin"
|
||||
DB_CREDENTIALS_AUTHENTICATION_METHOD = "authentication_method"
|
||||
|
||||
cred_key = DB_CREDENTIALS_DICT_TOKEN_KEY if oauth else DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY
|
||||
|
||||
return {
|
||||
cred_key: refried_credential_string,
|
||||
DB_CREDENTIALS_PRIMARY_ADMIN_KEY: email,
|
||||
DB_CREDENTIALS_AUTHENTICATION_METHOD: "uploaded",
|
||||
}
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""
|
||||
Soft sanitize for MinIO/S3:
|
||||
- Replace only prohibited characters with a space.
|
||||
- Preserve readability (no ugly underscores).
|
||||
- Collapse multiple spaces.
|
||||
"""
|
||||
if name is None:
|
||||
return "file.txt"
|
||||
|
||||
name = str(name).strip()
|
||||
|
||||
# Characters that MUST NOT appear in S3/MinIO object keys
|
||||
# Replace them with a space (not underscore)
|
||||
forbidden = r'[\\\?\#\%\*\:\|\<\>"]'
|
||||
name = re.sub(forbidden, " ", name)
|
||||
|
||||
# Replace slashes "/" (S3 interprets as folder) with space
|
||||
name = name.replace("/", " ")
|
||||
|
||||
# Collapse multiple spaces into one
|
||||
name = re.sub(r"\s+", " ", name)
|
||||
|
||||
# Trim both ends
|
||||
name = name.strip()
|
||||
|
||||
# Enforce reasonable max length
|
||||
if len(name) > 200:
|
||||
base, ext = os.path.splitext(name)
|
||||
name = base[:180].rstrip() + ext
|
||||
|
||||
# Ensure there is an extension (your original logic)
|
||||
if not os.path.splitext(name)[1]:
|
||||
name += ".txt"
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def clean_string(text: str | None) -> str | None:
|
||||
"""
|
||||
Clean a string to make it safe for insertion into MySQL (utf8mb4).
|
||||
- Normalize Unicode
|
||||
- Remove control characters / zero-width characters
|
||||
- Optionally remove high-plane emoji and symbols
|
||||
"""
|
||||
if text is None:
|
||||
return None
|
||||
|
||||
# 0. Ensure the value is a string
|
||||
text = str(text)
|
||||
|
||||
# 1. Normalize Unicode (NFC)
|
||||
text = unicodedata.normalize("NFC", text)
|
||||
|
||||
# 2. Remove ASCII control characters (except tab, newline, carriage return)
|
||||
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text)
|
||||
|
||||
# 3. Remove zero-width characters / BOM
|
||||
text = re.sub(r"[\u200b-\u200d\uFEFF]", "", text)
|
||||
|
||||
# 4. Remove high Unicode characters (emoji, special symbols)
|
||||
text = re.sub(r"[\U00010000-\U0010FFFF]", "", text)
|
||||
|
||||
# 5. Final fallback: strip any invalid UTF-8 sequences
|
||||
try:
|
||||
text.encode("utf-8")
|
||||
except UnicodeEncodeError:
|
||||
text = text.encode("utf-8", errors="ignore").decode("utf-8")
|
||||
|
||||
return text
|
||||
@ -30,7 +30,6 @@ class LoadConnector(ABC):
|
||||
"""Load documents from state"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_connector_settings(self) -> None:
|
||||
"""Validate connector settings"""
|
||||
pass
|
||||
|
||||
@ -733,7 +733,7 @@ def build_time_range_query(
|
||||
"""Build time range query for Gmail API"""
|
||||
query = ""
|
||||
if time_range_start is not None and time_range_start != 0:
|
||||
query += f"after:{int(time_range_start)}"
|
||||
query += f"after:{int(time_range_start) + 1}"
|
||||
if time_range_end is not None and time_range_end != 0:
|
||||
query += f" before:{int(time_range_end)}"
|
||||
query = query.strip()
|
||||
@ -778,6 +778,15 @@ def time_str_to_utc(time_str: str):
|
||||
return datetime.fromisoformat(time_str.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def gmail_time_str_to_utc(time_str: str):
|
||||
"""Convert Gmail RFC 2822 time string to UTC."""
|
||||
from email.utils import parsedate_to_datetime
|
||||
from datetime import timezone
|
||||
|
||||
dt = parsedate_to_datetime(time_str)
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
|
||||
# Notion Utilities
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@ -419,7 +419,15 @@ Creates a dataset.
|
||||
- `"embedding_model"`: `string`
|
||||
- `"permission"`: `string`
|
||||
- `"chunk_method"`: `string`
|
||||
- `"parser_config"`: `object`
|
||||
- "parser_config": `object`
|
||||
- "parse_type": `int`
|
||||
- "pipeline_id": `string`
|
||||
|
||||
Note: Choose exactly one ingestion mode when creating a dataset.
|
||||
- Chunking method: provide `"chunk_method"` (optionally with `"parser_config"`).
|
||||
- Ingestion pipeline: provide both `"parse_type"` and `"pipeline_id"` and do not provide `"chunk_method"`.
|
||||
|
||||
These options are mutually exclusive. If all three of `chunk_method`, `parse_type`, and `pipeline_id` are omitted, the system defaults to `chunk_method = "naive"`.
|
||||
|
||||
##### Request example
|
||||
|
||||
@ -433,6 +441,26 @@ curl --request POST \
|
||||
}'
|
||||
```
|
||||
|
||||
##### Request example (ingestion pipeline)
|
||||
|
||||
Use this form when specifying an ingestion pipeline (do not include `chunk_method`).
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/datasets \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||
--data '{
|
||||
"name": "test-sdk",
|
||||
"parse_type": <NUMBER_OF_FORMATS_IN_PARSE>,
|
||||
"pipeline_id": "<PIPELINE_ID_32_HEX>"
|
||||
}'
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `parse_type` is an integer. Replace `<NUMBER_OF_FORMATS_IN_PARSE>` with your pipeline's parse-type value.
|
||||
- `pipeline_id` must be a 32-character lowercase hexadecimal string.
|
||||
|
||||
##### Request parameters
|
||||
|
||||
- `"name"`: (*Body parameter*), `string`, *Required*
|
||||
@ -473,6 +501,7 @@ curl --request POST \
|
||||
- `"qa"`: Q&A
|
||||
- `"table"`: Table
|
||||
- `"tag"`: Tag
|
||||
- Mutually exclusive with `parse_type` and `pipeline_id`. If you set `chunk_method`, do not include `parse_type` or `pipeline_id`.
|
||||
|
||||
- `"parser_config"`: (*Body parameter*), `object`
|
||||
The configuration settings for the dataset parser. The attributes in this JSON object vary with the selected `"chunk_method"`:
|
||||
@ -509,6 +538,15 @@ curl --request POST \
|
||||
- Defaults to: `{"use_raptor": false}`.
|
||||
- If `"chunk_method"` is `"table"`, `"picture"`, `"one"`, or `"email"`, `"parser_config"` is an empty JSON object.
|
||||
|
||||
- "parse_type": (*Body parameter*), `int`
|
||||
The ingestion pipeline parse type identifier. Required if and only if you are using an ingestion pipeline (together with `"pipeline_id"`). Must not be provided when `"chunk_method"` is set.
|
||||
|
||||
- "pipeline_id": (*Body parameter*), `string`
|
||||
The ingestion pipeline ID. Required if and only if you are using an ingestion pipeline (together with `"parse_type"`).
|
||||
- Must not be provided when `"chunk_method"` is set.
|
||||
|
||||
Note: If none of `chunk_method`, `parse_type`, and `pipeline_id` are provided, the system will default to `chunk_method = "naive"`.
|
||||
|
||||
#### Response
|
||||
|
||||
Success:
|
||||
|
||||
@ -39,6 +39,7 @@ from deepdoc.parser.docling_parser import DoclingParser
|
||||
from deepdoc.parser.tcadp_parser import TCADPParser
|
||||
from rag.nlp import concat_img, find_codec, naive_merge, naive_merge_with_images, naive_merge_docx, rag_tokenizer, tokenize_chunks, tokenize_chunks_with_images, tokenize_table, attach_media_context
|
||||
|
||||
|
||||
def by_deepdoc(filename, binary=None, from_page=0, to_page=100000, lang="Chinese", callback=None, pdf_cls = None ,**kwargs):
|
||||
callback = callback
|
||||
binary = binary
|
||||
@ -600,8 +601,7 @@ def load_from_xml_v2(baseURI, rels_item_xml):
|
||||
srels._srels.append(_SerializedRelationship(baseURI, rel_elm))
|
||||
return srels
|
||||
|
||||
def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
lang="Chinese", callback=None, **kwargs):
|
||||
def chunk(filename, binary=None, from_page=0, to_page=100000, lang="Chinese", callback=None, **kwargs):
|
||||
"""
|
||||
Supported file formats are docx, pdf, excel, txt.
|
||||
This method apply the naive ways to chunk files.
|
||||
@ -611,14 +611,18 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
urls = set()
|
||||
url_res = []
|
||||
|
||||
|
||||
is_english = lang.lower() == "english" # is_english(cks)
|
||||
parser_config = kwargs.get(
|
||||
"parser_config", {
|
||||
"chunk_token_num": 512, "delimiter": "\n!?。;!?", "layout_recognize": "DeepDOC", "analyze_hyperlink": True})
|
||||
|
||||
child_deli = re.findall(r"`([^`]+)`", parser_config.get("children_delimiter", ""))
|
||||
child_deli = sorted(set(child_deli), key=lambda x: -len(x))
|
||||
child_deli = "|".join(re.escape(t) for t in child_deli if t)
|
||||
is_markdown = False
|
||||
table_context_size = max(0, int(parser_config.get("table_context_size", 0) or 0))
|
||||
image_context_size = max(0, int(parser_config.get("image_context_size", 0) or 0))
|
||||
final_sections = False
|
||||
|
||||
doc = {
|
||||
"docnm_kwd": filename,
|
||||
"title_tks": rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", filename))
|
||||
@ -679,12 +683,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
"chunk_token_num", 128)), parser_config.get(
|
||||
"delimiter", "\n!?。;!?"))
|
||||
|
||||
if kwargs.get("section_only", False):
|
||||
chunks.extend(embed_res)
|
||||
chunks.extend(url_res)
|
||||
return chunks
|
||||
|
||||
res.extend(tokenize_chunks_with_images(chunks, doc, is_english, images))
|
||||
res.extend(tokenize_chunks_with_images(chunks, doc, is_english, images, child_delimiters_pattern=child_deli))
|
||||
logging.info("naive_merge({}): {}".format(filename, timer() - st))
|
||||
res.extend(embed_res)
|
||||
res.extend(url_res)
|
||||
@ -780,7 +779,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
return_section_images=True,
|
||||
)
|
||||
|
||||
final_sections = True
|
||||
is_markdown = True
|
||||
|
||||
try:
|
||||
vision_model = LLMBundle(kwargs["tenant_id"], LLMType.IMAGE2TEXT)
|
||||
@ -857,7 +856,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
"file type not supported yet(pdf, xlsx, doc, docx, txt supported)")
|
||||
|
||||
st = timer()
|
||||
if final_sections:
|
||||
if is_markdown:
|
||||
merged_chunks = []
|
||||
merged_images = []
|
||||
chunk_limit = max(0, int(parser_config.get("chunk_token_num", 128)))
|
||||
@ -900,13 +899,11 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
|
||||
chunks = merged_chunks
|
||||
has_images = merged_images and any(img is not None for img in merged_images)
|
||||
if kwargs.get("section_only", False):
|
||||
chunks.extend(embed_res)
|
||||
return chunks
|
||||
|
||||
if has_images:
|
||||
res.extend(tokenize_chunks_with_images(chunks, doc, is_english, merged_images))
|
||||
res.extend(tokenize_chunks_with_images(chunks, doc, is_english, merged_images, child_delimiters_pattern=child_deli))
|
||||
else:
|
||||
res.extend(tokenize_chunks(chunks, doc, is_english, pdf_parser))
|
||||
res.extend(tokenize_chunks(chunks, doc, is_english, pdf_parser, child_delimiters_pattern=child_deli))
|
||||
else:
|
||||
if section_images:
|
||||
if all(image is None for image in section_images):
|
||||
@ -917,21 +914,14 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
int(parser_config.get(
|
||||
"chunk_token_num", 128)), parser_config.get(
|
||||
"delimiter", "\n!?。;!?"))
|
||||
if kwargs.get("section_only", False):
|
||||
chunks.extend(embed_res)
|
||||
return chunks
|
||||
|
||||
res.extend(tokenize_chunks_with_images(chunks, doc, is_english, images))
|
||||
res.extend(tokenize_chunks_with_images(chunks, doc, is_english, images, child_delimiters_pattern=child_deli))
|
||||
else:
|
||||
chunks = naive_merge(
|
||||
sections, int(parser_config.get(
|
||||
"chunk_token_num", 128)), parser_config.get(
|
||||
"delimiter", "\n!?。;!?"))
|
||||
if kwargs.get("section_only", False):
|
||||
chunks.extend(embed_res)
|
||||
return chunks
|
||||
|
||||
res.extend(tokenize_chunks(chunks, doc, is_english, pdf_parser))
|
||||
res.extend(tokenize_chunks(chunks, doc, is_english, pdf_parser, child_delimiters_pattern=child_deli))
|
||||
|
||||
if urls and parser_config.get("analyze_hyperlink", False) and is_root:
|
||||
for index, url in enumerate(urls):
|
||||
|
||||
@ -13,10 +13,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import random
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
|
||||
import trio
|
||||
|
||||
from common.misc_utils import get_uuid
|
||||
from rag.utils.base64_image import id2image, image2id
|
||||
from deepdoc.parser.pdf_parser import RAGFlowPdfParser
|
||||
@ -32,6 +32,7 @@ class SplitterParam(ProcessParamBase):
|
||||
self.chunk_token_size = 512
|
||||
self.delimiters = ["\n"]
|
||||
self.overlapped_percent = 0
|
||||
self.children_delimiters = []
|
||||
|
||||
def check(self):
|
||||
self.check_empty(self.delimiters, "Delimiters.")
|
||||
@ -58,6 +59,14 @@ class Splitter(ProcessBase):
|
||||
deli += f"`{d}`"
|
||||
else:
|
||||
deli += d
|
||||
child_deli = ""
|
||||
for d in self._param.children_delimiters:
|
||||
if len(d) > 1:
|
||||
child_deli += f"`{d}`"
|
||||
else:
|
||||
child_deli += d
|
||||
child_deli = [m.group(1) for m in re.finditer(r"`([^`]+)`", child_deli)]
|
||||
custom_pattern = "|".join(re.escape(t) for t in sorted(set(child_deli), key=len, reverse=True))
|
||||
|
||||
self.set_output("output_format", "chunks")
|
||||
self.callback(random.randint(1, 5) / 100.0, "Start to split into chunks.")
|
||||
@ -78,7 +87,23 @@ class Splitter(ProcessBase):
|
||||
deli,
|
||||
self._param.overlapped_percent,
|
||||
)
|
||||
self.set_output("chunks", [{"text": c.strip()} for c in cks if c.strip()])
|
||||
if custom_pattern:
|
||||
docs = []
|
||||
for c in cks:
|
||||
if not c.strip():
|
||||
continue
|
||||
split_sec = re.split(r"(%s)" % custom_pattern, c, flags=re.DOTALL)
|
||||
if split_sec:
|
||||
for txt in split_sec:
|
||||
docs.append({
|
||||
"text": txt,
|
||||
"mom": c
|
||||
})
|
||||
else:
|
||||
docs.append({"text": c})
|
||||
self.set_output("chunks", docs)
|
||||
else:
|
||||
self.set_output("chunks", [{"text": c.strip()} for c in cks if c.strip()])
|
||||
|
||||
self.callback(1, "Done.")
|
||||
return
|
||||
@ -100,12 +125,27 @@ class Splitter(ProcessBase):
|
||||
{
|
||||
"text": RAGFlowPdfParser.remove_tag(c),
|
||||
"image": img,
|
||||
"positions": [[pos[0][-1]+1, *pos[1:]] for pos in RAGFlowPdfParser.extract_positions(c)],
|
||||
"positions": [[pos[0][-1]+1, *pos[1:]] for pos in RAGFlowPdfParser.extract_positions(c)]
|
||||
}
|
||||
for c, img in zip(chunks, images) if c.strip()
|
||||
]
|
||||
async with trio.open_nursery() as nursery:
|
||||
for d in cks:
|
||||
nursery.start_soon(image2id, d, partial(settings.STORAGE_IMPL.put, tenant_id=self._canvas._tenant_id), get_uuid())
|
||||
self.set_output("chunks", cks)
|
||||
|
||||
if custom_pattern:
|
||||
docs = []
|
||||
for c in cks:
|
||||
split_sec = re.split(r"(%s)" % custom_pattern, c["text"], flags=re.DOTALL)
|
||||
if split_sec:
|
||||
c["mom"] = c["text"]
|
||||
for txt in split_sec:
|
||||
cc = deepcopy(c)
|
||||
cc["text"] = txt
|
||||
docs.append(cc)
|
||||
else:
|
||||
docs.append(c)
|
||||
self.set_output("chunks", docs)
|
||||
else:
|
||||
self.set_output("chunks", cks)
|
||||
self.callback(1, "Done.")
|
||||
|
||||
@ -264,14 +264,14 @@ def is_chinese(text):
|
||||
return False
|
||||
|
||||
|
||||
def tokenize(d, t, eng):
|
||||
d["content_with_weight"] = t
|
||||
t = re.sub(r"</?(table|td|caption|tr|th)( [^<>]{0,12})?>", " ", t)
|
||||
def tokenize(d, txt, eng):
|
||||
d["content_with_weight"] = txt
|
||||
t = re.sub(r"</?(table|td|caption|tr|th)( [^<>]{0,12})?>", " ", txt)
|
||||
d["content_ltks"] = rag_tokenizer.tokenize(t)
|
||||
d["content_sm_ltks"] = rag_tokenizer.fine_grained_tokenize(d["content_ltks"])
|
||||
|
||||
|
||||
def tokenize_chunks(chunks, doc, eng, pdf_parser=None):
|
||||
def tokenize_chunks(chunks, doc, eng, pdf_parser=None, child_delimiters_pattern=None):
|
||||
res = []
|
||||
# wrap up as es documents
|
||||
for ii, ck in enumerate(chunks):
|
||||
@ -288,12 +288,21 @@ def tokenize_chunks(chunks, doc, eng, pdf_parser=None):
|
||||
pass
|
||||
else:
|
||||
add_positions(d, [[ii]*5])
|
||||
|
||||
if child_delimiters_pattern:
|
||||
d["mom_with_weight"] = ck
|
||||
for txt in re.split(r"(%s)" % child_delimiters_pattern, ck, flags=re.DOTALL):
|
||||
dd = copy.deepcopy(d)
|
||||
tokenize(dd, txt, eng)
|
||||
res.append(dd)
|
||||
continue
|
||||
|
||||
tokenize(d, ck, eng)
|
||||
res.append(d)
|
||||
return res
|
||||
|
||||
|
||||
def tokenize_chunks_with_images(chunks, doc, eng, images):
|
||||
def tokenize_chunks_with_images(chunks, doc, eng, images, child_delimiters_pattern=None):
|
||||
res = []
|
||||
# wrap up as es documents
|
||||
for ii, (ck, image) in enumerate(zip(chunks, images)):
|
||||
@ -303,6 +312,13 @@ def tokenize_chunks_with_images(chunks, doc, eng, images):
|
||||
d = copy.deepcopy(doc)
|
||||
d["image"] = image
|
||||
add_positions(d, [[ii]*5])
|
||||
if child_delimiters_pattern:
|
||||
d["mom_with_weight"] = ck
|
||||
for txt in re.split(r"(%s)" % child_delimiters_pattern, ck, flags=re.DOTALL):
|
||||
dd = copy.deepcopy(d)
|
||||
tokenize(dd, txt, eng)
|
||||
res.append(dd)
|
||||
continue
|
||||
tokenize(d, ck, eng)
|
||||
res.append(d)
|
||||
return res
|
||||
|
||||
@ -424,6 +424,7 @@ class Dealer:
|
||||
|
||||
sim_np = np.array(sim, dtype=np.float64)
|
||||
if sim_np.size == 0:
|
||||
ranks["doc_aggs"] = []
|
||||
return ranks
|
||||
|
||||
sorted_idx = np.argsort(sim_np * -1)
|
||||
@ -433,6 +434,7 @@ class Dealer:
|
||||
ranks["total"] = int(filtered_count)
|
||||
|
||||
if filtered_count == 0:
|
||||
ranks["doc_aggs"] = []
|
||||
return ranks
|
||||
|
||||
max_pages = max(RERANK_LIMIT // max(page_size, 1), 1)
|
||||
|
||||
@ -430,9 +430,13 @@ def rank_memories(chat_mdl, goal:str, sub_goal:str, tool_call_summaries: list[st
|
||||
|
||||
|
||||
def gen_meta_filter(chat_mdl, meta_data:dict, query: str) -> dict:
|
||||
meta_data_structure = {}
|
||||
for key, values in meta_data.items():
|
||||
meta_data_structure[key] = list(values.keys()) if isinstance(values, dict) else values
|
||||
|
||||
sys_prompt = PROMPT_JINJA_ENV.from_string(META_FILTER).render(
|
||||
current_date=datetime.datetime.today().strftime('%Y-%m-%d'),
|
||||
metadata_keys=json.dumps(meta_data),
|
||||
metadata_keys=json.dumps(meta_data_structure),
|
||||
user_question=query
|
||||
)
|
||||
user_prompt = "Generate filters:"
|
||||
|
||||
@ -41,6 +41,7 @@ from common.data_source import BlobStorageConnector, NotionConnector, DiscordCon
|
||||
from common.constants import FileSource, TaskStatus
|
||||
from common.data_source.config import INDEX_BATCH_SIZE
|
||||
from common.data_source.confluence_connector import ConfluenceConnector
|
||||
from common.data_source.gmail_connector import GmailConnector
|
||||
from common.data_source.interfaces import CheckpointOutputWrapper
|
||||
from common.data_source.utils import load_all_docs_from_checkpoint_connector
|
||||
from common.log_utils import init_root_logger
|
||||
@ -230,7 +231,64 @@ class Gmail(SyncBase):
|
||||
SOURCE_NAME: str = FileSource.GMAIL
|
||||
|
||||
async def _generate(self, task: dict):
|
||||
pass
|
||||
# Gmail sync reuses the generic LoadConnector/PollConnector interface
|
||||
# implemented by common.data_source.gmail_connector.GmailConnector.
|
||||
#
|
||||
# Config expectations (self.conf):
|
||||
# credentials: Gmail / Workspace OAuth JSON (with primary admin email)
|
||||
# batch_size: optional, defaults to INDEX_BATCH_SIZE
|
||||
batch_size = self.conf.get("batch_size", INDEX_BATCH_SIZE)
|
||||
|
||||
self.connector = GmailConnector(batch_size=batch_size)
|
||||
|
||||
credentials = self.conf.get("credentials")
|
||||
if not credentials:
|
||||
raise ValueError("Gmail connector is missing credentials.")
|
||||
|
||||
new_credentials = self.connector.load_credentials(credentials)
|
||||
if new_credentials:
|
||||
# Persist rotated / refreshed credentials back to connector config
|
||||
try:
|
||||
updated_conf = copy.deepcopy(self.conf)
|
||||
updated_conf["credentials"] = new_credentials
|
||||
ConnectorService.update_by_id(task["connector_id"], {"config": updated_conf})
|
||||
self.conf = updated_conf
|
||||
logging.info(
|
||||
"Persisted refreshed Gmail credentials for connector %s",
|
||||
task["connector_id"],
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to persist refreshed Gmail credentials for connector %s",
|
||||
task["connector_id"],
|
||||
)
|
||||
|
||||
# Decide between full reindex and incremental polling by time range.
|
||||
if task["reindex"] == "1" or not task.get("poll_range_start"):
|
||||
start_time = None
|
||||
end_time = None
|
||||
begin_info = "totally"
|
||||
document_generator = self.connector.load_from_state()
|
||||
else:
|
||||
poll_start = task["poll_range_start"]
|
||||
# Defensive: if poll_start is somehow None, fall back to full load
|
||||
if poll_start is None:
|
||||
start_time = None
|
||||
end_time = None
|
||||
begin_info = "totally"
|
||||
document_generator = self.connector.load_from_state()
|
||||
else:
|
||||
start_time = poll_start.timestamp()
|
||||
end_time = datetime.now(timezone.utc).timestamp()
|
||||
begin_info = f"from {poll_start}"
|
||||
document_generator = self.connector.poll_source(start_time, end_time)
|
||||
|
||||
try:
|
||||
admin_email = self.connector.primary_admin_email
|
||||
except RuntimeError:
|
||||
admin_email = "unknown"
|
||||
logging.info(f"Connect to Gmail as {admin_email} {begin_info}")
|
||||
return document_generator
|
||||
|
||||
|
||||
class Dropbox(SyncBase):
|
||||
|
||||
@ -128,9 +128,6 @@ def signal_handler(sig, frame):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def set_progress(task_id, from_page=0, to_page=-1, prog=None, msg="Processing..."):
|
||||
try:
|
||||
if prog is not None and prog < 0:
|
||||
@ -720,6 +717,34 @@ async def delete_image(kb_id, chunk_id):
|
||||
|
||||
|
||||
async def insert_es(task_id, task_tenant_id, task_dataset_id, chunks, progress_callback):
|
||||
mothers = []
|
||||
mother_ids = set([])
|
||||
for ck in chunks:
|
||||
mom = ck.get("mom") or ck.get("mom_with_weight") or ""
|
||||
if not mom:
|
||||
continue
|
||||
id = xxhash.xxh64(mom.encode("utf-8")).hexdigest()
|
||||
if id in mother_ids:
|
||||
continue
|
||||
mother_ids.add(id)
|
||||
ck["mom_id"] = id
|
||||
mom_ck = copy.deepcopy(ck)
|
||||
mom_ck["id"] = id
|
||||
mom_ck["content_with_weight"] = mom
|
||||
mom_ck["available_int"] = 0
|
||||
flds = list(mom_ck.keys())
|
||||
for fld in flds:
|
||||
if fld not in ["id", "content_with_weight", "doc_id", "kb_id", "available_int"]:
|
||||
del mom_ck[fld]
|
||||
mothers.append(mom_ck)
|
||||
|
||||
for b in range(0, len(mothers), settings.DOC_BULK_SIZE):
|
||||
await trio.to_thread.run_sync(lambda: settings.docStoreConn.insert(mothers[b:b + settings.DOC_BULK_SIZE], search.index_name(task_tenant_id), task_dataset_id))
|
||||
task_canceled = has_canceled(task_id)
|
||||
if task_canceled:
|
||||
progress_callback(-1, msg="Task has been canceled.")
|
||||
return False
|
||||
|
||||
for b in range(0, len(chunks), settings.DOC_BULK_SIZE):
|
||||
doc_store_result = await trio.to_thread.run_sync(lambda: settings.docStoreConn.insert(chunks[b:b + settings.DOC_BULK_SIZE], search.index_name(task_tenant_id), task_dataset_id))
|
||||
task_canceled = has_canceled(task_id)
|
||||
|
||||
7
web/src/assets/svg/data-source/gmail.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="52 42 88 66">
|
||||
<path fill="#4285f4" d="M58 108h14V74L52 59v43c0 3.32 2.69 6 6 6"/>
|
||||
<path fill="#34a853" d="M120 108h14c3.32 0 6-2.69 6-6V59l-20 15"/>
|
||||
<path fill="#fbbc04" d="M120 48v26l20-15v-8c0-7.42-8.47-11.65-14.4-7.2"/>
|
||||
<path fill="#ea4335" d="M72 74V48l24 18 24-18v26L96 92"/>
|
||||
<path fill="#c5221f" d="M52 51v8l20 15V48l-5.6-4.2c-5.94-4.45-14.4-.22-14.4 7.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
35
web/src/assets/svg/empty/no-search-data-bri.svg
Normal file
@ -0,0 +1,35 @@
|
||||
<svg width="84" height="104" viewBox="0 0 84 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="search nothing">
|
||||
<path id="Vector" d="M60.6654 71.3333V39.3333C60.6654 37.9188 60.1035 36.5623 59.1033 35.5621C58.1031 34.5619 56.7465 34 55.332 34H20.6654M20.6654 34C22.0799 34 23.4364 34.5619 24.4366 35.5621C25.4368 36.5623 25.9987 37.9188 25.9987 39.3333V76.6667C25.9987 78.0812 26.5606 79.4377 27.5608 80.4379C28.561 81.4381 29.9175 82 31.332 82M20.6654 34C19.2509 34 17.8943 34.5619 16.8941 35.5621C15.8939 36.5623 15.332 37.9188 15.332 39.3333V44.6667C15.332 45.3739 15.613 46.0522 16.1131 46.5523C16.6132 47.0524 17.2915 47.3333 17.9987 47.3333H25.9987M31.332 82H63.332C64.7465 82 66.1031 81.4381 67.1033 80.4379C68.1035 79.4377 68.6654 78.0812 68.6654 76.6667V74C68.6654 73.2928 68.3844 72.6145 67.8843 72.1144C67.3842 71.6143 66.7059 71.3333 65.9987 71.3333H39.332C38.6248 71.3333 37.9465 71.6143 37.4464 72.1144C36.9463 72.6145 36.6654 73.2928 36.6654 74V76.6667C36.6654 78.0812 36.1035 79.4377 35.1033 80.4379C34.1031 81.4381 32.7465 82 31.332 82Z" stroke="url(#paint0_linear_493_43922)" stroke-opacity="0.2" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="Vector_2">
|
||||
<path d="M60.6654 65.3333V33.3333C60.6654 31.9188 60.1035 30.5623 59.1033 29.5621C58.1031 28.5619 56.7465 28 55.332 28H20.6654" fill="white"/>
|
||||
<path d="M31.332 76H63.332C64.7465 76 66.1031 75.4381 67.1033 74.4379C68.1035 73.4377 68.6654 72.0812 68.6654 70.6667V68C68.6654 67.2928 68.3844 66.6145 67.8843 66.1144C67.3842 65.6143 66.7059 65.3333 65.9987 65.3333H39.332C38.6248 65.3333 37.9465 65.6143 37.4464 66.1144C36.9463 66.6145 36.6654 67.2928 36.6654 68V70.6667C36.6654 72.0812 36.1035 73.4377 35.1033 74.4379C34.1031 75.4381 32.7465 76 31.332 76C29.9175 76 28.561 75.4381 27.5608 74.4379C26.5606 73.4377 25.9987 72.0812 25.9987 70.6667V33.3333C25.9987 31.9188 25.4368 30.5623 24.4366 29.5621C23.4364 28.5619 22.0799 28 20.6654 28C19.2509 28 17.8943 28.5619 16.8941 29.5621C15.8939 30.5623 15.332 31.9188 15.332 33.3333V38.6667C15.332 39.3739 15.613 40.0522 16.1131 40.5523C16.6132 41.0524 17.2915 41.3333 17.9987 41.3333H25.9987" fill="white"/>
|
||||
<path d="M60.6654 65.3333V33.3333C60.6654 31.9188 60.1035 30.5623 59.1033 29.5621C58.1031 28.5619 56.7465 28 55.332 28H20.6654M20.6654 28C22.0799 28 23.4364 28.5619 24.4366 29.5621C25.4368 30.5623 25.9987 31.9188 25.9987 33.3333V70.6667C25.9987 72.0812 26.5606 73.4377 27.5608 74.4379C28.561 75.4381 29.9175 76 31.332 76M20.6654 28C19.2509 28 17.8943 28.5619 16.8941 29.5621C15.8939 30.5623 15.332 31.9188 15.332 33.3333V38.6667C15.332 39.3739 15.613 40.0522 16.1131 40.5523C16.6132 41.0524 17.2915 41.3333 17.9987 41.3333H25.9987M31.332 76H63.332C64.7465 76 66.1031 75.4381 67.1033 74.4379C68.1035 73.4377 68.6654 72.0812 68.6654 70.6667V68C68.6654 67.2928 68.3844 66.6145 67.8843 66.1144C67.3842 65.6143 66.7059 65.3333 65.9987 65.3333H39.332C38.6248 65.3333 37.9465 65.6143 37.4464 66.1144C36.9463 66.6145 36.6654 67.2928 36.6654 68V70.6667C36.6654 72.0812 36.1035 73.4377 35.1033 74.4379C34.1031 75.4381 32.7465 76 31.332 76Z" stroke="url(#paint1_linear_493_43922)" stroke-opacity="0.6" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<path id="Vector_3" d="M61.3333 59.3333V27.3333C61.3333 25.9188 60.7714 24.5623 59.7712 23.5621C58.771 22.5619 57.4145 22 56 22H21.3333M21.3333 22C22.7478 22 24.1044 22.5619 25.1046 23.5621C26.1048 24.5623 26.6667 25.9188 26.6667 27.3333V64.6667C26.6667 66.0812 27.2286 67.4377 28.2288 68.4379C29.229 69.4381 30.5855 70 32 70M21.3333 22C19.9188 22 18.5623 22.5619 17.5621 23.5621C16.5619 24.5623 16 25.9188 16 27.3333V32.6667C16 33.3739 16.281 34.0522 16.781 34.5523C17.2811 35.0524 17.9594 35.3333 18.6667 35.3333H26.6667M32 70H64C65.4145 70 66.771 69.4381 67.7712 68.4379C68.7714 67.4377 69.3333 66.0812 69.3333 64.6667V62C69.3333 61.2928 69.0524 60.6145 68.5523 60.1144C68.0522 59.6143 67.3739 59.3333 66.6667 59.3333H40C39.2928 59.3333 38.6145 59.6143 38.1144 60.1144C37.6143 60.6145 37.3333 61.2928 37.3333 62V64.6667C37.3333 66.0812 36.7714 67.4377 35.7712 68.4379C34.771 69.4381 33.4145 70 32 70Z" stroke="url(#paint2_linear_493_43922)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M61.3333 59.3333V27.3333C61.3333 25.9188 60.7714 24.5623 59.7712 23.5621C58.771 22.5619 57.4145 22 56 22H21.3333M21.3333 22C22.7478 22 24.1044 22.5619 25.1046 23.5621C26.1048 24.5623 26.6667 25.9188 26.6667 27.3333V64.6667C26.6667 66.0812 27.2286 67.4377 28.2288 68.4379C29.229 69.4381 30.5855 70 32 70M21.3333 22C19.9188 22 18.5623 22.5619 17.5621 23.5621C16.5619 24.5623 16 25.9188 16 27.3333V32.6667C16 33.3739 16.281 34.0522 16.781 34.5523C17.2811 35.0524 17.9594 35.3333 18.6667 35.3333H26.6667M32 70H64C65.4145 70 66.771 69.4381 67.7712 68.4379C68.7714 67.4377 69.3333 66.0812 69.3333 64.6667V62C69.3333 61.2928 69.0524 60.6145 68.5523 60.1144C68.0522 59.6143 67.3739 59.3333 66.6667 59.3333H40C39.2928 59.3333 38.6145 59.6143 38.1144 60.1144C37.6143 60.6145 37.3333 61.2928 37.3333 62V64.6667C37.3333 66.0812 36.7714 67.4377 35.7712 68.4379C34.771 69.4381 33.4145 70 32 70Z" stroke="url(#paint3_linear_493_43922)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_5" d="M56 52L51.1778 47.1778M53.7778 40.8889C53.7778 45.7981 49.7981 49.7778 44.8889 49.7778C39.9797 49.7778 36 45.7981 36 40.8889C36 35.9797 39.9797 32 44.8889 32C49.7981 32 53.7778 35.9797 53.7778 40.8889Z" stroke="url(#paint4_linear_493_43922)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_493_43922" x1="41.9987" y1="34" x2="41.9987" y2="82" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_493_43922" x1="41.9987" y1="28" x2="41.9987" y2="76" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_493_43922" x1="42.6667" y1="22" x2="42.6667" y2="70" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_493_43922" x1="42.6667" y1="22" x2="42.6667" y2="70" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_493_43922" x1="46" y1="32" x2="46" y2="52" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
39
web/src/assets/svg/empty/no-search-data-dark.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<svg width="84" height="104" viewBox="0 0 84 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="search nothing">
|
||||
<path id="Vector" d="M60.6654 71.3333V39.3333C60.6654 37.9188 60.1035 36.5623 59.1033 35.5621C58.1031 34.5619 56.7465 34 55.332 34H20.6654M20.6654 34C22.0799 34 23.4364 34.5619 24.4366 35.5621C25.4368 36.5623 25.9987 37.9188 25.9987 39.3333V76.6667C25.9987 78.0812 26.5606 79.4377 27.5608 80.4379C28.561 81.4381 29.9175 82 31.332 82M20.6654 34C19.2509 34 17.8943 34.5619 16.8941 35.5621C15.8939 36.5623 15.332 37.9188 15.332 39.3333V44.6667C15.332 45.3739 15.613 46.0522 16.1131 46.5523C16.6132 47.0524 17.2915 47.3333 17.9987 47.3333H25.9987M31.332 82H63.332C64.7465 82 66.1031 81.4381 67.1033 80.4379C68.1035 79.4377 68.6654 78.0812 68.6654 76.6667V74C68.6654 73.2928 68.3844 72.6145 67.8843 72.1144C67.3842 71.6143 66.7059 71.3333 65.9987 71.3333H39.332C38.6248 71.3333 37.9465 71.6143 37.4464 72.1144C36.9463 72.6145 36.6654 73.2928 36.6654 74V76.6667C36.6654 78.0812 36.1035 79.4377 35.1033 80.4379C34.1031 81.4381 32.7465 82 31.332 82Z" stroke="url(#paint0_linear_486_6812)" stroke-opacity="0.2" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="Vector_2">
|
||||
<path d="M60.6654 65.3333V33.3333C60.6654 31.9188 60.1035 30.5623 59.1033 29.5621C58.1031 28.5619 56.7465 28 55.332 28H20.6654" fill="#161618"/>
|
||||
<path d="M31.332 76H63.332C64.7465 76 66.1031 75.4381 67.1033 74.4379C68.1035 73.4377 68.6654 72.0812 68.6654 70.6667V68C68.6654 67.2928 68.3844 66.6145 67.8843 66.1144C67.3842 65.6143 66.7059 65.3333 65.9987 65.3333H39.332C38.6248 65.3333 37.9465 65.6143 37.4464 66.1144C36.9463 66.6145 36.6654 67.2928 36.6654 68V70.6667C36.6654 72.0812 36.1035 73.4377 35.1033 74.4379C34.1031 75.4381 32.7465 76 31.332 76C29.9175 76 28.561 75.4381 27.5608 74.4379C26.5606 73.4377 25.9987 72.0812 25.9987 70.6667V33.3333C25.9987 31.9188 25.4368 30.5623 24.4366 29.5621C23.4364 28.5619 22.0799 28 20.6654 28C19.2509 28 17.8943 28.5619 16.8941 29.5621C15.8939 30.5623 15.332 31.9188 15.332 33.3333V38.6667C15.332 39.3739 15.613 40.0522 16.1131 40.5523C16.6132 41.0524 17.2915 41.3333 17.9987 41.3333H25.9987" fill="#161618"/>
|
||||
<path d="M60.6654 65.3333V33.3333C60.6654 31.9188 60.1035 30.5623 59.1033 29.5621C58.1031 28.5619 56.7465 28 55.332 28H20.6654M20.6654 28C22.0799 28 23.4364 28.5619 24.4366 29.5621C25.4368 30.5623 25.9987 31.9188 25.9987 33.3333V70.6667C25.9987 72.0812 26.5606 73.4377 27.5608 74.4379C28.561 75.4381 29.9175 76 31.332 76M20.6654 28C19.2509 28 17.8943 28.5619 16.8941 29.5621C15.8939 30.5623 15.332 31.9188 15.332 33.3333V38.6667C15.332 39.3739 15.613 40.0522 16.1131 40.5523C16.6132 41.0524 17.2915 41.3333 17.9987 41.3333H25.9987M31.332 76H63.332C64.7465 76 66.1031 75.4381 67.1033 74.4379C68.1035 73.4377 68.6654 72.0812 68.6654 70.6667V68C68.6654 67.2928 68.3844 66.6145 67.8843 66.1144C67.3842 65.6143 66.7059 65.3333 65.9987 65.3333H39.332C38.6248 65.3333 37.9465 65.6143 37.4464 66.1144C36.9463 66.6145 36.6654 67.2928 36.6654 68V70.6667C36.6654 72.0812 36.1035 73.4377 35.1033 74.4379C34.1031 75.4381 32.7465 76 31.332 76Z" stroke="url(#paint1_linear_486_6812)" stroke-opacity="0.6" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<path id="Vector_3" d="M61.3333 59.3333V27.3333C61.3333 25.9188 60.7714 24.5623 59.7712 23.5621C58.771 22.5619 57.4145 22 56 22H21.3333M21.3333 22C22.7478 22 24.1044 22.5619 25.1046 23.5621C26.1048 24.5623 26.6667 25.9188 26.6667 27.3333V64.6667C26.6667 66.0812 27.2286 67.4377 28.2288 68.4379C29.229 69.4381 30.5855 70 32 70M21.3333 22C19.9188 22 18.5623 22.5619 17.5621 23.5621C16.5619 24.5623 16 25.9188 16 27.3333V32.6667C16 33.3739 16.281 34.0522 16.781 34.5523C17.2811 35.0524 17.9594 35.3333 18.6667 35.3333H26.6667M32 70H64C65.4145 70 66.771 69.4381 67.7712 68.4379C68.7714 67.4377 69.3333 66.0812 69.3333 64.6667V62C69.3333 61.2928 69.0524 60.6145 68.5523 60.1144C68.0522 59.6143 67.3739 59.3333 66.6667 59.3333H40C39.2928 59.3333 38.6145 59.6143 38.1144 60.1144C37.6143 60.6145 37.3333 61.2928 37.3333 62V64.6667C37.3333 66.0812 36.7714 67.4377 35.7712 68.4379C34.771 69.4381 33.4145 70 32 70Z" stroke="url(#paint2_linear_486_6812)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="Vector_4">
|
||||
<path d="M61.3333 59.3333V27.3333C61.3333 25.9188 60.7714 24.5623 59.7712 23.5621C58.771 22.5619 57.4145 22 56 22H21.3333" fill="#161618"/>
|
||||
<path d="M32 70H64C65.4145 70 66.771 69.4381 67.7712 68.4379C68.7714 67.4377 69.3333 66.0812 69.3333 64.6667V62C69.3333 61.2928 69.0524 60.6145 68.5523 60.1144C68.0522 59.6143 67.3739 59.3333 66.6667 59.3333H40C39.2928 59.3333 38.6145 59.6143 38.1144 60.1144C37.6143 60.6145 37.3333 61.2928 37.3333 62V64.6667C37.3333 66.0812 36.7714 67.4377 35.7712 68.4379C34.771 69.4381 33.4145 70 32 70C30.5855 70 29.229 69.4381 28.2288 68.4379C27.2286 67.4377 26.6667 66.0812 26.6667 64.6667V27.3333C26.6667 25.9188 26.1048 24.5623 25.1046 23.5621C24.1044 22.5619 22.7478 22 21.3333 22C19.9188 22 18.5623 22.5619 17.5621 23.5621C16.5619 24.5623 16 25.9188 16 27.3333V32.6667C16 33.3739 16.281 34.0522 16.781 34.5523C17.2811 35.0524 17.9594 35.3333 18.6667 35.3333H26.6667" fill="#161618"/>
|
||||
<path d="M61.3333 59.3333V27.3333C61.3333 25.9188 60.7714 24.5623 59.7712 23.5621C58.771 22.5619 57.4145 22 56 22H21.3333M21.3333 22C22.7478 22 24.1044 22.5619 25.1046 23.5621C26.1048 24.5623 26.6667 25.9188 26.6667 27.3333V64.6667C26.6667 66.0812 27.2286 67.4377 28.2288 68.4379C29.229 69.4381 30.5855 70 32 70M21.3333 22C19.9188 22 18.5623 22.5619 17.5621 23.5621C16.5619 24.5623 16 25.9188 16 27.3333V32.6667C16 33.3739 16.281 34.0522 16.781 34.5523C17.2811 35.0524 17.9594 35.3333 18.6667 35.3333H26.6667M32 70H64C65.4145 70 66.771 69.4381 67.7712 68.4379C68.7714 67.4377 69.3333 66.0812 69.3333 64.6667V62C69.3333 61.2928 69.0524 60.6145 68.5523 60.1144C68.0522 59.6143 67.3739 59.3333 66.6667 59.3333H40C39.2928 59.3333 38.6145 59.6143 38.1144 60.1144C37.6143 60.6145 37.3333 61.2928 37.3333 62V64.6667C37.3333 66.0812 36.7714 67.4377 35.7712 68.4379C34.771 69.4381 33.4145 70 32 70Z" stroke="url(#paint3_linear_486_6812)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<path id="Vector_5" d="M56 52L51.1778 47.1778M53.7778 40.8889C53.7778 45.7981 49.7981 49.7778 44.8889 49.7778C39.9797 49.7778 36 45.7981 36 40.8889C36 35.9797 39.9797 32 44.8889 32C49.7981 32 53.7778 35.9797 53.7778 40.8889Z" stroke="url(#paint4_linear_486_6812)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_486_6812" x1="41.9987" y1="34" x2="41.9987" y2="82" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_486_6812" x1="41.9987" y1="28" x2="41.9987" y2="76" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_486_6812" x1="42.6667" y1="22" x2="42.6667" y2="70" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_486_6812" x1="42.6667" y1="22" x2="42.6667" y2="70" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_486_6812" x1="46" y1="32" x2="46" y2="52" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#7B7B7C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
33
web/src/components/empty/constant.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { t } from 'i18next';
|
||||
import { HomeIcon } from '../svg-icon';
|
||||
|
||||
export enum EmptyType {
|
||||
Data = 'data',
|
||||
SearchData = 'search-data',
|
||||
}
|
||||
|
||||
export enum EmptyCardType {
|
||||
Agent = 'agent',
|
||||
Dataset = 'dataset',
|
||||
Chat = 'chat',
|
||||
Search = 'search',
|
||||
}
|
||||
|
||||
export const EmptyCardData = {
|
||||
[EmptyCardType.Agent]: {
|
||||
icon: <HomeIcon name="agents" width={'24'} />,
|
||||
title: t('empty.agentTitle'),
|
||||
},
|
||||
[EmptyCardType.Dataset]: {
|
||||
icon: <HomeIcon name="datasets" width={'24'} />,
|
||||
title: t('empty.datasetTitle'),
|
||||
},
|
||||
[EmptyCardType.Chat]: {
|
||||
icon: <HomeIcon name="chats" width={'24'} />,
|
||||
title: t('empty.chatTitle'),
|
||||
},
|
||||
[EmptyCardType.Search]: {
|
||||
icon: <HomeIcon name="searches" width={'24'} />,
|
||||
title: t('empty.searchTitle'),
|
||||
},
|
||||
};
|
||||
@ -2,79 +2,33 @@ import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { useIsDarkTheme } from '../theme-provider';
|
||||
|
||||
import noDataIcon from './no data bri.svg';
|
||||
import noDataIconDark from './no data.svg';
|
||||
|
||||
type EmptyProps = {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const EmptyIcon = () => {
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import SvgIcon from '../svg-icon';
|
||||
import { EmptyCardData, EmptyCardType, EmptyType } from './constant';
|
||||
import { EmptyCardProps, EmptyProps } from './interface';
|
||||
|
||||
const EmptyIcon = ({ name, width }: { name: string; width?: number }) => {
|
||||
return (
|
||||
<img
|
||||
className="h-20"
|
||||
src={isDarkTheme ? noDataIconDark : noDataIcon}
|
||||
alt={t('common.noData')}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="184"
|
||||
height="152"
|
||||
viewBox="0 0 184 152"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>{t('common.noData')}</title>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g transform="translate(24 31.67)">
|
||||
<ellipse
|
||||
fillOpacity=".8"
|
||||
fill={isDarkTheme ? '#28282A' : '#F5F5F7'}
|
||||
cx="67.797"
|
||||
cy="106.89"
|
||||
rx="67.797"
|
||||
ry="12.668"
|
||||
></ellipse>
|
||||
<path
|
||||
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||
fill={isDarkTheme ? '#736960' : '#AEB8C2'}
|
||||
></path>
|
||||
<path
|
||||
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||
fill="url(#linearGradient-1)"
|
||||
transform="translate(13.56)"
|
||||
></path>
|
||||
<path
|
||||
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||
fill={isDarkTheme ? '#28282A' : '#F5F5F7'}
|
||||
></path>
|
||||
<path
|
||||
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||
fill={isDarkTheme ? '#45413A' : '#DCE0E6'}
|
||||
></path>
|
||||
</g>
|
||||
<path
|
||||
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||
fill={isDarkTheme ? '#45413A' : '#DCE0E6'}
|
||||
></path>
|
||||
<g
|
||||
transform="translate(149.65 15.383)"
|
||||
fill={isDarkTheme ? '#222' : '#FFF'}
|
||||
>
|
||||
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"></ellipse>
|
||||
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
// <img
|
||||
// className="h-20"
|
||||
// src={isDarkTheme ? noDataIconDark : noDataIcon}
|
||||
// alt={t('common.noData')}
|
||||
// />
|
||||
<SvgIcon name={name || 'empty/no-data-dark'} width={width || 42} />
|
||||
);
|
||||
};
|
||||
|
||||
const Empty = (props: EmptyProps) => {
|
||||
const { className, children } = props;
|
||||
const { className, children, type, text, iconWidth } = props;
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
const name = useMemo(() => {
|
||||
return isDarkTheme
|
||||
? `empty/no-${type || EmptyType.Data}-dark`
|
||||
: `empty/no-${type || EmptyType.Data}-bri`;
|
||||
}, [isDarkTheme, type]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -82,11 +36,12 @@ const Empty = (props: EmptyProps) => {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<EmptyIcon />
|
||||
<EmptyIcon name={name} width={iconWidth} />
|
||||
|
||||
{!children && (
|
||||
<div className="empty-text text-text-secondary">
|
||||
{t('common.noData')}
|
||||
<div className="empty-text text-text-secondary text-sm">
|
||||
{text ||
|
||||
(type === 'data' ? t('common.noData') : t('common.noResults'))}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
@ -95,3 +50,65 @@ const Empty = (props: EmptyProps) => {
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
|
||||
export const EmptyCard = (props: EmptyCardProps) => {
|
||||
const { icon, className, children, title, description, style } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-3 items-start justify-start border border-dashed border-border-button rounded-md p-5 w-fit',
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{icon}
|
||||
{title && <div className="text-text-primary text-base">{title}</div>}
|
||||
{description && (
|
||||
<div className="text-text-secondary text-sm">{description}</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyAppCard = (props: {
|
||||
type: EmptyCardType;
|
||||
onClick?: () => void;
|
||||
showIcon?: boolean;
|
||||
className?: string;
|
||||
size?: 'small' | 'large';
|
||||
}) => {
|
||||
const { type, showIcon, className } = props;
|
||||
let defaultClass = '';
|
||||
let style = {};
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
style = { width: '256px' };
|
||||
defaultClass = 'mt-1';
|
||||
break;
|
||||
case 'large':
|
||||
style = { width: '480px' };
|
||||
defaultClass = 'mt-5';
|
||||
break;
|
||||
default:
|
||||
defaultClass = '';
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div className=" cursor-pointer " onClick={props.onClick}>
|
||||
<EmptyCard
|
||||
icon={showIcon ? EmptyCardData[type].icon : undefined}
|
||||
title={EmptyCardData[type].title}
|
||||
className={className}
|
||||
style={style}
|
||||
// description={EmptyCardData[type].description}
|
||||
>
|
||||
<div
|
||||
className={cn(defaultClass, 'flex items-center justify-start w-full')}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</div>
|
||||
</EmptyCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
18
web/src/components/empty/interface.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { EmptyType } from './constant';
|
||||
|
||||
export type EmptyProps = {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
type?: EmptyType;
|
||||
text?: string;
|
||||
iconWidth?: number;
|
||||
};
|
||||
|
||||
export type EmptyCardProps = {
|
||||
icon?: React.ReactNode;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { MessageType, SharedFrom } from '@/constants/chat';
|
||||
import { useFetchExternalAgentInputs } from '@/hooks/use-agent-request';
|
||||
import { useFetchExternalChatInfo } from '@/hooks/use-chat-request';
|
||||
import i18n from '@/locales/config';
|
||||
import { useSendNextSharedMessage } from '@/pages/agent/hooks/use-send-shared-message';
|
||||
import { MessageCircle, Minimize2, Send, X } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
@ -20,7 +22,13 @@ const FloatingChatWidget = () => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { sharedId: conversationId, locale } = useGetSharedChatSearchParams();
|
||||
const {
|
||||
sharedId: conversationId,
|
||||
locale,
|
||||
from,
|
||||
} = useGetSharedChatSearchParams();
|
||||
|
||||
const isFromAgent = from === SharedFrom.Agent;
|
||||
|
||||
// Check if we're in button-only mode or window-only mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@ -34,7 +42,7 @@ const FloatingChatWidget = () => {
|
||||
sendLoading,
|
||||
derivedMessages,
|
||||
hasError,
|
||||
} = useSendSharedMessage();
|
||||
} = (isFromAgent ? useSendNextSharedMessage : useSendSharedMessage)(() => {});
|
||||
|
||||
// Sync our local input with the hook's value when needed
|
||||
useEffect(() => {
|
||||
@ -43,7 +51,11 @@ const FloatingChatWidget = () => {
|
||||
}
|
||||
}, [hookValue, inputValue]);
|
||||
|
||||
const { data: chatInfo } = useFetchExternalChatInfo();
|
||||
const { data } = (
|
||||
isFromAgent ? useFetchExternalAgentInputs : useFetchExternalChatInfo
|
||||
)();
|
||||
|
||||
const title = data.title;
|
||||
|
||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||
useClickDrawer();
|
||||
@ -372,7 +384,7 @@ const FloatingChatWidget = () => {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">
|
||||
{chatInfo?.title || 'Chat Support'}
|
||||
{title || 'Chat Support'}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-100">
|
||||
We typically reply instantly
|
||||
@ -494,14 +506,16 @@ const FloatingChatWidget = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PdfDrawer
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
width={'100vw'}
|
||||
height={'100vh'}
|
||||
/>
|
||||
{visible && (
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
width={'100vw'}
|
||||
height={'100vh'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} // Full mode - render everything together (original behavior)
|
||||
@ -524,7 +538,7 @@ const FloatingChatWidget = () => {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">
|
||||
{chatInfo?.title || 'Chat Support'}
|
||||
{title || 'Chat Support'}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-100">
|
||||
We typically reply instantly
|
||||
@ -695,7 +709,7 @@ const FloatingChatWidget = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PdfDrawer
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
|
||||
@ -2,8 +2,6 @@ import Image from '@/components/image';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Popover } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
@ -27,10 +25,16 @@ import {
|
||||
replaceThinkToSection,
|
||||
showImage,
|
||||
} from '@/utils/chat';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
import { pipe } from 'lodash/fp';
|
||||
import { CircleAlert } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '../ui/hover-card';
|
||||
import styles from './index.less';
|
||||
|
||||
const getChunkIndex = (match: string) => Number(match);
|
||||
@ -145,20 +149,20 @@ const MarkdownContent = ({
|
||||
return (
|
||||
<div key={chunkItem?.id} className="flex gap-2">
|
||||
{imageId && (
|
||||
<Popover
|
||||
placement="left"
|
||||
content={
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceChunkImage}
|
||||
></Image>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceImagePreview}
|
||||
></Image>
|
||||
}
|
||||
>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceChunkImage}
|
||||
></Image>
|
||||
</Popover>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
<div className={'space-y-2 max-w-[40vw]'}>
|
||||
<div
|
||||
@ -168,7 +172,7 @@ const MarkdownContent = ({
|
||||
className={classNames(styles.chunkContentText)}
|
||||
></div>
|
||||
{documentId && (
|
||||
<Flex gap={'small'}>
|
||||
<section className="flex gap-1">
|
||||
{fileThumbnail ? (
|
||||
<img
|
||||
src={fileThumbnail}
|
||||
@ -182,8 +186,8 @@ const MarkdownContent = ({
|
||||
></SvgIcon>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
className={classNames(styles.documentLink, 'text-wrap')}
|
||||
variant="link"
|
||||
className={'text-wrap p-0'}
|
||||
onClick={handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
@ -193,7 +197,7 @@ const MarkdownContent = ({
|
||||
>
|
||||
{document?.doc_name}
|
||||
</Button>
|
||||
</Flex>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -228,9 +232,14 @@ const MarkdownContent = ({
|
||||
}
|
||||
></Image>
|
||||
) : (
|
||||
<Popover content={getPopoverContent(chunkIndex)} key={i}>
|
||||
<InfoCircleOutlined className={styles.referenceIcon} />
|
||||
</Popover>
|
||||
<HoverCard key={i}>
|
||||
<HoverCardTrigger>
|
||||
<CircleAlert className="size-4 inline-block" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-w-3xl">
|
||||
{getPopoverContent(chunkIndex)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -14,10 +14,10 @@ import {
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar, Flex, Space } from 'antd';
|
||||
import MarkdownContent from '../markdown-content';
|
||||
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
|
||||
import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files';
|
||||
import { RAGFlowAvatar } from '../ragflow-avatar';
|
||||
import { useTheme } from '../theme-provider';
|
||||
import { AssistantGroupButton, UserGroupButton } from './group-button';
|
||||
import styles from './index.less';
|
||||
@ -98,40 +98,43 @@ const MessageItem = ({
|
||||
>
|
||||
{visibleAvatar &&
|
||||
(item.role === MessageType.User ? (
|
||||
<Avatar size={40} src={avatar ?? '/logo.svg'} />
|
||||
<RAGFlowAvatar
|
||||
className="size-10"
|
||||
avatar={avatar ?? '/logo.svg'}
|
||||
isPerson
|
||||
/>
|
||||
) : avatarDialog ? (
|
||||
<Avatar size={40} src={avatarDialog} />
|
||||
<RAGFlowAvatar
|
||||
className="size-10"
|
||||
avatar={avatarDialog}
|
||||
isPerson
|
||||
/>
|
||||
) : (
|
||||
<AssistantIcon />
|
||||
))}
|
||||
|
||||
<Flex vertical gap={8} flex={1}>
|
||||
<Space>
|
||||
{isAssistant ? (
|
||||
index !== 0 && (
|
||||
<AssistantGroupButton
|
||||
messageId={item.id}
|
||||
content={item.content}
|
||||
prompt={item.prompt}
|
||||
showLikeButton={showLikeButton}
|
||||
audioBinary={item.audio_binary}
|
||||
showLoudspeaker={showLoudspeaker}
|
||||
></AssistantGroupButton>
|
||||
)
|
||||
) : (
|
||||
<UserGroupButton
|
||||
content={item.content}
|
||||
<section className="flex gap-2 flex-1 flex-col">
|
||||
{isAssistant ? (
|
||||
index !== 0 && (
|
||||
<AssistantGroupButton
|
||||
messageId={item.id}
|
||||
removeMessageById={removeMessageById}
|
||||
regenerateMessage={
|
||||
regenerateMessage && handleRegenerateMessage
|
||||
}
|
||||
sendLoading={sendLoading}
|
||||
></UserGroupButton>
|
||||
)}
|
||||
content={item.content}
|
||||
prompt={item.prompt}
|
||||
showLikeButton={showLikeButton}
|
||||
audioBinary={item.audio_binary}
|
||||
showLoudspeaker={showLoudspeaker}
|
||||
></AssistantGroupButton>
|
||||
)
|
||||
) : (
|
||||
<UserGroupButton
|
||||
content={item.content}
|
||||
messageId={item.id}
|
||||
removeMessageById={removeMessageById}
|
||||
regenerateMessage={regenerateMessage && handleRegenerateMessage}
|
||||
sendLoading={sendLoading}
|
||||
></UserGroupButton>
|
||||
)}
|
||||
|
||||
{/* <b>{isAssistant ? '' : nickname}</b> */}
|
||||
</Space>
|
||||
<div
|
||||
className={cn(
|
||||
isAssistant
|
||||
@ -159,7 +162,7 @@ const MessageItem = ({
|
||||
files={documentList}
|
||||
></InnerUploadedMessageFiles>
|
||||
)}
|
||||
</Flex>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { Drawer } from 'antd';
|
||||
import { cn } from '@/lib/utils';
|
||||
import DocumentPreviewer from '../pdf-previewer';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '../ui/sheet';
|
||||
|
||||
interface IProps extends IModalProps<any> {
|
||||
documentId: string;
|
||||
@ -11,7 +12,7 @@ interface IProps extends IModalProps<any> {
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
export const PdfDrawer = ({
|
||||
export const PdfSheet = ({
|
||||
visible = false,
|
||||
hideModal,
|
||||
documentId,
|
||||
@ -20,20 +21,25 @@ export const PdfDrawer = ({
|
||||
height,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<Drawer
|
||||
title="Document Previewer"
|
||||
onClose={hideModal}
|
||||
open={visible}
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
<DocumentPreviewer
|
||||
documentId={documentId}
|
||||
chunk={chunk}
|
||||
visible={visible}
|
||||
></DocumentPreviewer>
|
||||
</Drawer>
|
||||
<Sheet open onOpenChange={hideModal}>
|
||||
<SheetContent
|
||||
className={cn(`max-w-full`)}
|
||||
style={{
|
||||
width: width,
|
||||
height: height ? height : undefined,
|
||||
}}
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Document Previewer</SheetTitle>
|
||||
</SheetHeader>
|
||||
<DocumentPreviewer
|
||||
documentId={documentId}
|
||||
chunk={chunk}
|
||||
visible={visible}
|
||||
></DocumentPreviewer>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default PdfDrawer;
|
||||
export default PdfSheet;
|
||||
|
||||
@ -58,7 +58,7 @@ export const useSelectLlmOptions = () => {
|
||||
function buildLlmOptionsWithIcon(x: IThirdOAIModel) {
|
||||
return {
|
||||
label: (
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<LlmIcon
|
||||
name={getLLMIconName(x.fid, x.llm_name)}
|
||||
width={24}
|
||||
|
||||
@ -14,9 +14,16 @@ export const useNavigatePage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { id } = useParams();
|
||||
|
||||
const navigateToDatasetList = useCallback(() => {
|
||||
navigate(Routes.Datasets);
|
||||
}, [navigate]);
|
||||
const navigateToDatasetList = useCallback(
|
||||
({ isCreate = false }: { isCreate?: boolean }) => {
|
||||
if (isCreate) {
|
||||
navigate(Routes.Datasets + '?isCreate=true');
|
||||
} else {
|
||||
navigate(Routes.Datasets);
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateToDataset = useCallback(
|
||||
(id: string) => () => {
|
||||
|
||||
@ -54,7 +54,7 @@ export const useFetchTenantInfo = (
|
||||
): ResponseGetType<ITenantInfo> => {
|
||||
const { t } = useTranslation();
|
||||
const { data, isFetching: loading } = useQuery({
|
||||
queryKey: ['tenantInfo'],
|
||||
queryKey: ['tenantInfo', showEmptyModelWarn],
|
||||
initialData: {},
|
||||
gcTime: 0,
|
||||
queryFn: async () => {
|
||||
@ -99,7 +99,6 @@ export const useSelectParserList = (): Array<{
|
||||
label: string;
|
||||
}> => {
|
||||
const { data: tenantInfo } = useFetchTenantInfo(true);
|
||||
|
||||
const parserList = useMemo(() => {
|
||||
const parserArray: Array<string> = tenantInfo?.parser_ids?.split(',') ?? [];
|
||||
return parserArray.map((x) => {
|
||||
|
||||
@ -3,7 +3,7 @@ export default {
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
back: 'Back',
|
||||
noResults: 'No results.',
|
||||
noResults: 'No results found',
|
||||
selectPlaceholder: 'select value',
|
||||
selectAll: 'Select all',
|
||||
delete: 'Delete',
|
||||
@ -51,7 +51,7 @@ export default {
|
||||
remove: 'Remove',
|
||||
search: 'Search',
|
||||
noDataFound: 'No data found.',
|
||||
noData: 'No data',
|
||||
noData: 'No data available',
|
||||
promptPlaceholder: `Please input or use / to quickly insert variables.`,
|
||||
mcp: {
|
||||
namePlaceholder: 'My MCP Server',
|
||||
@ -739,6 +739,7 @@ Example: Virtual Hosted Style`,
|
||||
'Sync pages and databases from Notion for knowledge retrieval.',
|
||||
google_driveDescription:
|
||||
'Connect your Google Drive via OAuth and sync specific folders or drives.',
|
||||
gmailDescription: 'Connect your Gmail via OAuth to sync emails.',
|
||||
webdavDescription: 'Connect to WebDAV servers to sync files.',
|
||||
webdavRemotePathTip:
|
||||
'Optional: Specify a folder path on the WebDAV server (e.g., /Documents). Leave empty to sync from root.',
|
||||
@ -750,6 +751,10 @@ Example: Virtual Hosted Style`,
|
||||
'Comma-separated emails whose "My Drive" contents should be indexed (include the primary admin).',
|
||||
google_driveSharedFoldersTip:
|
||||
'Comma-separated Google Drive folder links to crawl.',
|
||||
gmailPrimaryAdminTip:
|
||||
'Primary admin email with Gmail / Workspace access, used to enumerate domain users and as the default sync account.',
|
||||
gmailTokenTip:
|
||||
'Upload the OAuth JSON generated from Google Console. If it only contains client credentials, run the browser-based verification once to mint long-lived refresh tokens.',
|
||||
dropboxDescription:
|
||||
'Connect your Dropbox to sync files and folders from a chosen account.',
|
||||
dropboxAccessTokenTip:
|
||||
@ -2021,6 +2026,7 @@ Important structured information may include: names, dates, locations, events, k
|
||||
processingSuccessTip: 'Total successfully processed files',
|
||||
processingFailedTip: 'Total failed processes',
|
||||
processing: 'Processing',
|
||||
noData: 'No log yet',
|
||||
},
|
||||
|
||||
deleteModal: {
|
||||
@ -2034,6 +2040,15 @@ Important structured information may include: names, dates, locations, events, k
|
||||
delMember: 'Delete member',
|
||||
},
|
||||
|
||||
empty: {
|
||||
noMCP: 'No MCP servers available',
|
||||
agentTitle: 'No agent app created yet',
|
||||
datasetTitle: 'No dataset created yet',
|
||||
chatTitle: 'No chat app created yet',
|
||||
searchTitle: 'No search app created yet',
|
||||
addNow: 'Add Now',
|
||||
},
|
||||
|
||||
admin: {
|
||||
loginTitle: 'Admin Console',
|
||||
title: 'RAGFlow',
|
||||
|
||||
@ -736,6 +736,8 @@ export default {
|
||||
'Синхронизируйте страницы и базы данных из Notion для извлечения знаний.',
|
||||
google_driveDescription:
|
||||
'Подключите ваш Google Drive через OAuth и синхронизируйте определенные папки или диски.',
|
||||
gmailDescription:
|
||||
'Подключите ваш Gmail / Google Workspace аккаунт для синхронизации писем и их метаданных, чтобы построить корпоративную почтовую базу знаний и поиск с учетом прав доступа.',
|
||||
google_driveTokenTip:
|
||||
'Загрузите JSON токена OAuth, сгенерированный из помощника OAuth или Google Cloud Console. Вы также можете загрузить client_secret JSON из "установленного" или "веб" приложения. Если это ваша первая синхронизация, откроется окно браузера для завершения согласия OAuth. Если JSON уже содержит токен обновления, он будет автоматически повторно использован.',
|
||||
google_drivePrimaryAdminTip:
|
||||
@ -744,6 +746,10 @@ export default {
|
||||
'Электронные почты через запятую, чье содержимое "Мой диск" должно индексироваться (включите основного администратора).',
|
||||
google_driveSharedFoldersTip:
|
||||
'Ссылки на папки Google Drive через запятую для обхода.',
|
||||
gmailPrimaryAdminTip:
|
||||
'Основной административный email с доступом к Gmail / Workspace, используется для перечисления пользователей домена и как аккаунт синхронизации по умолчанию.',
|
||||
gmailTokenTip:
|
||||
'Загрузите OAuth JSON, сгенерированный в Google Console. Если он содержит только учетные данные клиента, выполните одноразовое подтверждение в браузере, чтобы получить долгоживущие токены обновления.',
|
||||
jiraDescription:
|
||||
'Подключите ваше рабочее пространство Jira для синхронизации задач, комментариев и вложений.',
|
||||
jiraBaseUrlTip:
|
||||
|
||||
@ -3,7 +3,7 @@ export default {
|
||||
common: {
|
||||
confirm: '确定',
|
||||
back: '返回',
|
||||
noResults: '无结果。',
|
||||
noResults: '未查到结果',
|
||||
selectPlaceholder: '请选择',
|
||||
selectAll: '全选',
|
||||
delete: '删除',
|
||||
@ -718,6 +718,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
notionDescription: ' 同步 Notion 页面与数据库,用于知识检索。',
|
||||
google_driveDescription:
|
||||
'通过 OAuth 连接 Google Drive,并同步指定的文件夹或云端硬盘。',
|
||||
gmailDescription: '通过 OAuth 连接 Gmail,用于同步邮件。',
|
||||
google_driveTokenTip:
|
||||
'请上传由 OAuth helper 或 Google Cloud Console 导出的 OAuth token JSON。也支持上传 “installed” 或 “web” 类型的 client_secret JSON。若为首次同步,将自动弹出浏览器完成 OAuth 授权流程;如果该 JSON 已包含 refresh token,将会被自动复用。',
|
||||
google_drivePrimaryAdminTip: '拥有相应 Drive 访问权限的管理员邮箱。',
|
||||
@ -725,6 +726,10 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
'需要索引其 “我的云端硬盘” 的邮箱,多个邮箱用逗号分隔(建议包含管理员)。',
|
||||
google_driveSharedFoldersTip:
|
||||
'需要同步的 Google Drive 文件夹链接,多个链接用逗号分隔。',
|
||||
gmailPrimaryAdminTip:
|
||||
'拥有 Gmail / Workspace 访问权限的主要管理员邮箱,用于列出域内用户并作为默认同步账号。',
|
||||
gmailTokenTip:
|
||||
'请上传由 Google Console 生成的 OAuth JSON。如果仅包含 client credentials,请通过浏览器授权一次以获取长期有效的刷新 Token。',
|
||||
dropboxDescription: '连接 Dropbox,同步指定账号下的文件与文件夹。',
|
||||
dropboxAccessTokenTip:
|
||||
'请在 Dropbox App Console 生成 Access Token,并勾选 files.metadata.read、files.content.read、sharing.read 等必要权限。',
|
||||
@ -1873,6 +1878,27 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
downloadFailedTip: '下载失败总数',
|
||||
processingSuccessTip: '处理成功的文件总数',
|
||||
processingFailedTip: '处理失败的文件总数',
|
||||
noData: '暂无日志',
|
||||
},
|
||||
|
||||
deleteModal: {
|
||||
delAgent: '删除智能体',
|
||||
delDataset: '删除知识库',
|
||||
delSearch: '删除搜索',
|
||||
delFile: '删除文件',
|
||||
delFiles: '删除文件',
|
||||
delFilesContent: '已选择 {{count}} 个文件',
|
||||
delChat: '删除聊天',
|
||||
delMember: '删除成员',
|
||||
},
|
||||
|
||||
empty: {
|
||||
noMCP: '暂无 MCP 服务器可用',
|
||||
agentTitle: '尚未创建智能体',
|
||||
datasetTitle: '尚未创建数据集',
|
||||
chatTitle: '尚未创建聊天应用',
|
||||
searchTitle: '尚未创建搜索应用',
|
||||
addNow: '立即添加',
|
||||
},
|
||||
|
||||
deleteModal: {
|
||||
|
||||
@ -5,7 +5,7 @@ import { useSendAgentMessage } from './use-send-agent-message';
|
||||
import { FileUploadProps } from '@/components/file-upload';
|
||||
import { NextMessageInput } from '@/components/message-input/next';
|
||||
import MessageItem from '@/components/next-message-item';
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import {
|
||||
useFetchAgent,
|
||||
@ -127,12 +127,14 @@ function AgentChatBox() {
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
<PdfDrawer
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
></PdfDrawer>
|
||||
{visible && (
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
></PdfSheet>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { EmbedContainer } from '@/components/embed-container';
|
||||
import { FileUploadProps } from '@/components/file-upload';
|
||||
import { NextMessageInput } from '@/components/message-input/next';
|
||||
import MessageItem from '@/components/next-message-item';
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { useUploadCanvasFileWithProgress } from '@/hooks/use-agent-request';
|
||||
@ -204,12 +204,12 @@ const ChatContainer = () => {
|
||||
</div>
|
||||
</EmbedContainer>
|
||||
{visible && (
|
||||
<PdfDrawer
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
></PdfDrawer>
|
||||
></PdfSheet>
|
||||
)}
|
||||
{parameterDialogVisible && (
|
||||
<ParameterDialog
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { CardContainer } from '@/components/card-container';
|
||||
import { EmptyCardType } from '@/components/empty/constant';
|
||||
import { EmptyAppCard } from '@/components/empty/empty';
|
||||
import ListFilterBar from '@/components/list-filter-bar';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -14,7 +16,8 @@ import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
|
||||
import { t } from 'i18next';
|
||||
import { pick } from 'lodash';
|
||||
import { Clipboard, ClipboardPlus, FileInput, Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'umi';
|
||||
import { AgentCard } from './agent-card';
|
||||
import { CreateAgentDialog } from './create-agent-dialog';
|
||||
import { useCreateAgentOrPipeline } from './hooks/use-create-agent';
|
||||
@ -67,95 +70,120 @@ export default function Agents() {
|
||||
},
|
||||
[setPagination],
|
||||
);
|
||||
|
||||
const [searchUrl, setSearchUrl] = useSearchParams();
|
||||
const isCreate = searchUrl.get('isCreate') === 'true';
|
||||
useEffect(() => {
|
||||
if (isCreate) {
|
||||
showCreatingModal();
|
||||
searchUrl.delete('isCreate');
|
||||
setSearchUrl(searchUrl);
|
||||
}
|
||||
}, [isCreate, showCreatingModal, searchUrl, setSearchUrl]);
|
||||
return (
|
||||
<section className="flex flex-col w-full flex-1">
|
||||
<div className="px-8 pt-8 ">
|
||||
<ListFilterBar
|
||||
title={t('flow.agents')}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
icon="agents"
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('flow.createGraph')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={showCreatingModal}
|
||||
>
|
||||
<Clipboard />
|
||||
{t('flow.createFromBlank')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={navigateToAgentTemplates}
|
||||
>
|
||||
<ClipboardPlus />
|
||||
{t('flow.createFromTemplate')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={handleImportJson}
|
||||
>
|
||||
<FileInput />
|
||||
{t('flow.importJsonFile')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{data.map((x) => {
|
||||
return (
|
||||
<AgentCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showAgentRenameModal={showAgentRenameModal}
|
||||
></AgentCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
<div className="mt-8 px-8 pb-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
{agentRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideAgentRenameModal}
|
||||
onOk={onAgentRenameOk}
|
||||
initialName={initialAgentName}
|
||||
loading={agentRenameLoading}
|
||||
></RenameDialog>
|
||||
<>
|
||||
{(!data?.length || data?.length <= 0) && (
|
||||
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14"
|
||||
type={EmptyCardType.Agent}
|
||||
onClick={() => showCreatingModal()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{creatingVisible && (
|
||||
<CreateAgentDialog
|
||||
loading={loading}
|
||||
visible={creatingVisible}
|
||||
hideModal={hideCreatingModal}
|
||||
shouldChooseAgent
|
||||
onOk={handleCreateAgentOrPipeline}
|
||||
></CreateAgentDialog>
|
||||
)}
|
||||
{fileUploadVisible && (
|
||||
<UploadAgentDialog
|
||||
hideModal={hideFileUploadModal}
|
||||
onOk={onFileUploadOk}
|
||||
></UploadAgentDialog>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-col w-full flex-1">
|
||||
{!!data?.length && (
|
||||
<>
|
||||
<div className="px-8 pt-8 ">
|
||||
<ListFilterBar
|
||||
title={t('flow.agents')}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
icon="agents"
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('flow.createGraph')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={showCreatingModal}
|
||||
>
|
||||
<Clipboard />
|
||||
{t('flow.createFromBlank')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={navigateToAgentTemplates}
|
||||
>
|
||||
<ClipboardPlus />
|
||||
{t('flow.createFromTemplate')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={handleImportJson}
|
||||
>
|
||||
<FileInput />
|
||||
{t('flow.importJsonFile')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{data.map((x) => {
|
||||
return (
|
||||
<AgentCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showAgentRenameModal={showAgentRenameModal}
|
||||
></AgentCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
<div className="mt-8 px-8 pb-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{agentRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideAgentRenameModal}
|
||||
onOk={onAgentRenameOk}
|
||||
initialName={initialAgentName}
|
||||
loading={agentRenameLoading}
|
||||
></RenameDialog>
|
||||
)}
|
||||
{creatingVisible && (
|
||||
<CreateAgentDialog
|
||||
loading={loading}
|
||||
visible={creatingVisible}
|
||||
hideModal={hideCreatingModal}
|
||||
shouldChooseAgent
|
||||
onOk={handleCreateAgentOrPipeline}
|
||||
></CreateAgentDialog>
|
||||
)}
|
||||
{fileUploadVisible && (
|
||||
<UploadAgentDialog
|
||||
hideModal={hideFileUploadModal}
|
||||
onOk={onFileUploadOk}
|
||||
></UploadAgentDialog>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { EmptyType } from '@/components/empty/constant';
|
||||
import Empty from '@/components/empty/empty';
|
||||
import FileStatusBadge from '@/components/file-status-badge';
|
||||
import { FileIcon, IconFontFill } from '@/components/icon-font';
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
@ -344,6 +346,7 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const { t: tDatasetOverview } = useTranslate('datasetOverview');
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { navigateToDataflowResult } = useNavigatePage();
|
||||
const [logInfo, setLogInfo] = useState<IFileLogItem>();
|
||||
@ -445,7 +448,10 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
<Empty
|
||||
type={EmptyType.Data}
|
||||
text={tDatasetOverview('noData')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
import * as React from 'react';
|
||||
|
||||
import { ChunkMethodDialog } from '@/components/chunk-method-dialog';
|
||||
import { EmptyType } from '@/components/empty/constant';
|
||||
import Empty from '@/components/empty/empty';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import {
|
||||
@ -164,7 +166,7 @@ export function DatasetTable({
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
<Empty type={EmptyType.Data} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@ -136,7 +136,7 @@ export function useDatasetTableColumns({
|
||||
{
|
||||
DataSourceInfo[
|
||||
row.original.source_type as keyof typeof DataSourceInfo
|
||||
].icon
|
||||
]?.icon
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { EmptyType } from '@/components/empty/constant';
|
||||
import Empty from '@/components/empty/empty';
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import { FilterButton } from '@/components/list-filter-bar';
|
||||
@ -101,7 +102,7 @@ export function TestingResult({
|
||||
{!data.chunks?.length && !loading && (
|
||||
<div className="flex justify-center items-center w-full h-[calc(100vh-241px)]">
|
||||
<div>
|
||||
<Empty>
|
||||
<Empty type={EmptyType.SearchData}>
|
||||
{data.isRuned && (
|
||||
<div className="text-text-secondary">
|
||||
{t('knowledgeDetails.noTestResultsForRuned')}
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { CardContainer } from '@/components/card-container';
|
||||
import { EmptyCardType } from '@/components/empty/constant';
|
||||
import { EmptyAppCard } from '@/components/empty/empty';
|
||||
import ListFilterBar from '@/components/list-filter-bar';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { pick } from 'lodash';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'umi';
|
||||
import { DatasetCard } from './dataset-card';
|
||||
import { DatasetCreatingDialog } from './dataset-creating-dialog';
|
||||
import { useSaveKnowledge } from './hooks';
|
||||
@ -52,59 +56,86 @@ export default function Datasets() {
|
||||
},
|
||||
[setPagination],
|
||||
);
|
||||
|
||||
const [searchUrl, setSearchUrl] = useSearchParams();
|
||||
const isCreate = searchUrl.get('isCreate') === 'true';
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
if (isCreate) {
|
||||
queryClient.invalidateQueries({ queryKey: ['tenantInfo'] });
|
||||
showModal();
|
||||
searchUrl.delete('isCreate');
|
||||
setSearchUrl(searchUrl);
|
||||
}
|
||||
}, [isCreate, showModal, searchUrl, setSearchUrl]);
|
||||
return (
|
||||
<section className="py-4 flex-1 flex flex-col">
|
||||
<ListFilterBar
|
||||
title={t('header.dataset')}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
value={filterValue}
|
||||
filters={owners}
|
||||
onChange={handleFilterSubmit}
|
||||
className="px-8"
|
||||
icon={'datasets'}
|
||||
>
|
||||
<Button onClick={showModal}>
|
||||
<Plus className=" size-2.5" />
|
||||
{t('knowledgeList.createKnowledgeBase')}
|
||||
</Button>
|
||||
</ListFilterBar>
|
||||
<div className="flex-1">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{kbs.map((dataset) => {
|
||||
return (
|
||||
<DatasetCard
|
||||
dataset={dataset}
|
||||
key={dataset.id}
|
||||
showDatasetRenameModal={showDatasetRenameModal}
|
||||
></DatasetCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
<div className="mt-8 px-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
{visible && (
|
||||
<DatasetCreatingDialog
|
||||
hideModal={hideModal}
|
||||
onOk={onCreateOk}
|
||||
loading={creatingLoading}
|
||||
></DatasetCreatingDialog>
|
||||
)}
|
||||
{datasetRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideDatasetRenameModal}
|
||||
onOk={onDatasetRenameOk}
|
||||
initialName={initialDatasetName}
|
||||
loading={datasetRenameLoading}
|
||||
></RenameDialog>
|
||||
)}
|
||||
</section>
|
||||
<>
|
||||
<section className="py-4 flex-1 flex flex-col">
|
||||
{(!kbs?.length || kbs?.length <= 0) && (
|
||||
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14"
|
||||
type={EmptyCardType.Dataset}
|
||||
onClick={() => showModal()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!!kbs?.length && (
|
||||
<>
|
||||
<ListFilterBar
|
||||
title={t('header.dataset')}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
value={filterValue}
|
||||
filters={owners}
|
||||
onChange={handleFilterSubmit}
|
||||
className="px-8"
|
||||
icon={'datasets'}
|
||||
>
|
||||
<Button onClick={showModal}>
|
||||
<Plus className=" size-2.5" />
|
||||
{t('knowledgeList.createKnowledgeBase')}
|
||||
</Button>
|
||||
</ListFilterBar>
|
||||
<div className="flex-1">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{kbs.map((dataset) => {
|
||||
return (
|
||||
<DatasetCard
|
||||
dataset={dataset}
|
||||
key={dataset.id}
|
||||
showDatasetRenameModal={showDatasetRenameModal}
|
||||
></DatasetCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
<div className="mt-8 px-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{visible && (
|
||||
<DatasetCreatingDialog
|
||||
hideModal={hideModal}
|
||||
onOk={onCreateOk}
|
||||
loading={creatingLoading}
|
||||
></DatasetCreatingDialog>
|
||||
)}
|
||||
{datasetRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideDatasetRenameModal}
|
||||
onOk={onDatasetRenameOk}
|
||||
initialName={initialDatasetName}
|
||||
loading={datasetRenameLoading}
|
||||
></RenameDialog>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,11 +3,18 @@ import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
|
||||
import { useEffect } from 'react';
|
||||
import { AgentDropdown } from '../agents/agent-dropdown';
|
||||
import { useRenameAgent } from '../agents/use-rename-agent';
|
||||
|
||||
export function Agents() {
|
||||
const { data } = useFetchAgentListByPage();
|
||||
export function Agents({
|
||||
setListLength,
|
||||
setLoading,
|
||||
}: {
|
||||
setListLength: (length: number) => void;
|
||||
setLoading?: (loading: boolean) => void;
|
||||
}) {
|
||||
const { data, loading } = useFetchAgentListByPage();
|
||||
const { navigateToAgent } = useNavigatePage();
|
||||
const {
|
||||
agentRenameLoading,
|
||||
@ -18,6 +25,11 @@ export function Agents() {
|
||||
showAgentRenameModal,
|
||||
} = useRenameAgent();
|
||||
|
||||
useEffect(() => {
|
||||
setListLength(data?.length || 0);
|
||||
setLoading?.(loading || false);
|
||||
}, [data, setListLength, loading, setLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.slice(0, 10).map((x) => (
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { CardSineLineContainer } from '@/components/card-singleline-container';
|
||||
import { EmptyCardType } from '@/components/empty/constant';
|
||||
import { EmptyAppCard } from '@/components/empty/empty';
|
||||
import { HomeIcon } from '@/components/svg-icon';
|
||||
import { Segmented, SegmentedValue } from '@/components/ui/segmented';
|
||||
import { Routes } from '@/routes';
|
||||
@ -16,14 +18,29 @@ const IconMap = {
|
||||
[Routes.Agents]: 'agents',
|
||||
};
|
||||
|
||||
const EmptyTypeMap = {
|
||||
[Routes.Chats]: EmptyCardType.Chat,
|
||||
[Routes.Searches]: EmptyCardType.Search,
|
||||
[Routes.Agents]: EmptyCardType.Agent,
|
||||
};
|
||||
|
||||
export function Applications() {
|
||||
const [val, setVal] = useState(Routes.Chats);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [listLength, setListLength] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleNavigate = useCallback(() => {
|
||||
navigate(val);
|
||||
}, [navigate, val]);
|
||||
const handleNavigate = useCallback(
|
||||
({ isCreate }: { isCreate?: boolean }) => {
|
||||
if (isCreate) {
|
||||
navigate(val + '?isCreate=true');
|
||||
} else {
|
||||
navigate(val);
|
||||
}
|
||||
},
|
||||
[navigate, val],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
@ -36,16 +53,14 @@ export function Applications() {
|
||||
|
||||
const handleChange = (path: SegmentedValue) => {
|
||||
setVal(path as Routes);
|
||||
setListLength(0);
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="mt-12">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h2 className="text-2xl font-semibold flex gap-2.5">
|
||||
{/* <IconFont
|
||||
name={IconMap[val as keyof typeof IconMap]}
|
||||
className="size-8"
|
||||
></IconFont> */}
|
||||
<HomeIcon
|
||||
name={`${IconMap[val as keyof typeof IconMap]}`}
|
||||
width={'32'}
|
||||
@ -63,11 +78,36 @@ export function Applications() {
|
||||
</div>
|
||||
{/* <div className="flex flex-wrap gap-4"> */}
|
||||
<CardSineLineContainer>
|
||||
{val === Routes.Agents && <Agents></Agents>}
|
||||
{val === Routes.Chats && <ChatList></ChatList>}
|
||||
{val === Routes.Searches && <SearchList></SearchList>}
|
||||
{<SeeAllAppCard click={handleNavigate}></SeeAllAppCard>}
|
||||
{val === Routes.Agents && (
|
||||
<Agents
|
||||
setListLength={(length: number) => setListLength(length)}
|
||||
setLoading={(loading: boolean) => setLoading(loading)}
|
||||
></Agents>
|
||||
)}
|
||||
{val === Routes.Chats && (
|
||||
<ChatList
|
||||
setListLength={(length: number) => setListLength(length)}
|
||||
setLoading={(loading: boolean) => setLoading(loading)}
|
||||
></ChatList>
|
||||
)}
|
||||
{val === Routes.Searches && (
|
||||
<SearchList
|
||||
setListLength={(length: number) => setListLength(length)}
|
||||
setLoading={(loading: boolean) => setLoading(loading)}
|
||||
></SearchList>
|
||||
)}
|
||||
{listLength > 0 && (
|
||||
<SeeAllAppCard
|
||||
click={() => handleNavigate({ isCreate: false })}
|
||||
></SeeAllAppCard>
|
||||
)}
|
||||
</CardSineLineContainer>
|
||||
{listLength <= 0 && !loading && (
|
||||
<EmptyAppCard
|
||||
type={EmptyTypeMap[val as keyof typeof EmptyTypeMap]}
|
||||
onClick={() => handleNavigate({ isCreate: true })}
|
||||
/>
|
||||
)}
|
||||
{/* </div> */}
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -3,13 +3,20 @@ import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchDialogList } from '@/hooks/use-chat-request';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChatDropdown } from '../next-chats/chat-dropdown';
|
||||
import { useRenameChat } from '../next-chats/hooks/use-rename-chat';
|
||||
|
||||
export function ChatList() {
|
||||
export function ChatList({
|
||||
setListLength,
|
||||
setLoading,
|
||||
}: {
|
||||
setListLength: (length: number) => void;
|
||||
setLoading?: (loading: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useFetchDialogList();
|
||||
const { data, loading } = useFetchDialogList();
|
||||
const { navigateToChat } = useNavigatePage();
|
||||
|
||||
const {
|
||||
@ -20,7 +27,10 @@ export function ChatList() {
|
||||
onChatRenameOk,
|
||||
chatRenameLoading,
|
||||
} = useRenameChat();
|
||||
|
||||
useEffect(() => {
|
||||
setListLength(data?.dialogs?.length || 0);
|
||||
setLoading?.(loading || false);
|
||||
}, [data, setListLength, loading, setLoading]);
|
||||
return (
|
||||
<>
|
||||
{data.dialogs.slice(0, 10).map((x) => (
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { CardSineLineContainer } from '@/components/card-singleline-container';
|
||||
import { EmptyCardType } from '@/components/empty/constant';
|
||||
import { EmptyAppCard } from '@/components/empty/empty';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { HomeIcon } from '@/components/svg-icon';
|
||||
import { CardSkeleton } from '@/components/ui/skeleton';
|
||||
@ -35,18 +37,32 @@ export function Datasets() {
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<CardSineLineContainer>
|
||||
{kbs
|
||||
?.slice(0, 6)
|
||||
.map((dataset) => (
|
||||
<DatasetCard
|
||||
key={dataset.id}
|
||||
dataset={dataset}
|
||||
showDatasetRenameModal={showDatasetRenameModal}
|
||||
></DatasetCard>
|
||||
))}
|
||||
{<SeeAllAppCard click={navigateToDatasetList}></SeeAllAppCard>}
|
||||
</CardSineLineContainer>
|
||||
<>
|
||||
{kbs?.length > 0 && (
|
||||
<CardSineLineContainer>
|
||||
{kbs
|
||||
?.slice(0, 6)
|
||||
.map((dataset) => (
|
||||
<DatasetCard
|
||||
key={dataset.id}
|
||||
dataset={dataset}
|
||||
showDatasetRenameModal={showDatasetRenameModal}
|
||||
></DatasetCard>
|
||||
))}
|
||||
{
|
||||
<SeeAllAppCard
|
||||
click={() => navigateToDatasetList({ isCreate: false })}
|
||||
></SeeAllAppCard>
|
||||
}
|
||||
</CardSineLineContainer>
|
||||
)}
|
||||
{kbs?.length <= 0 && (
|
||||
<EmptyAppCard
|
||||
type={EmptyCardType.Dataset}
|
||||
onClick={() => navigateToDatasetList({ isCreate: true })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
// </div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -3,11 +3,18 @@ import { IconFont } from '@/components/icon-font';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useEffect } from 'react';
|
||||
import { useFetchSearchList, useRenameSearch } from '../next-searches/hooks';
|
||||
import { SearchDropdown } from '../next-searches/search-dropdown';
|
||||
|
||||
export function SearchList() {
|
||||
const { data, refetch: refetchList } = useFetchSearchList();
|
||||
export function SearchList({
|
||||
setListLength,
|
||||
setLoading,
|
||||
}: {
|
||||
setListLength: (length: number) => void;
|
||||
setLoading?: (loading: boolean) => void;
|
||||
}) {
|
||||
const { data, refetch: refetchList, isLoading } = useFetchSearchList();
|
||||
const { navigateToSearch } = useNavigatePage();
|
||||
const {
|
||||
openCreateModal,
|
||||
@ -22,6 +29,11 @@ export function SearchList() {
|
||||
refetchList();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setListLength(data?.data?.search_apps?.length || 0);
|
||||
setLoading?.(isLoading || false);
|
||||
}, [data, setListLength, isLoading, setLoading]);
|
||||
return (
|
||||
<>
|
||||
{data?.data.search_apps.slice(0, 10).map((x) => (
|
||||
|
||||
@ -2,7 +2,7 @@ import { LargeModelFormFieldWithoutFilter } from '@/components/large-model-form-
|
||||
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||
import { NextMessageInput } from '@/components/message-input/next';
|
||||
import MessageItem from '@/components/message-item';
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -257,12 +257,12 @@ export function MultipleChatBox({
|
||||
/>
|
||||
</div>
|
||||
{visible && (
|
||||
<PdfDrawer
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
></PdfDrawer>
|
||||
></PdfSheet>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextMessageInput } from '@/components/message-input/next';
|
||||
import MessageItem from '@/components/message-item';
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import {
|
||||
@ -101,12 +101,12 @@ export function SingleChatBox({ controller, stopOutputMessage }: IProps) {
|
||||
removeFile={removeFile}
|
||||
/>
|
||||
{visible && (
|
||||
<PdfDrawer
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
></PdfDrawer>
|
||||
></PdfSheet>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { CardContainer } from '@/components/card-container';
|
||||
import { EmptyCardType } from '@/components/empty/constant';
|
||||
import { EmptyAppCard } from '@/components/empty/empty';
|
||||
import ListFilterBar from '@/components/list-filter-bar';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -6,8 +8,9 @@ import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { useFetchDialogList } from '@/hooks/use-chat-request';
|
||||
import { pick } from 'lodash';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'umi';
|
||||
import { ChatCard } from './chat-card';
|
||||
import { useRenameChat } from './hooks/use-rename-chat';
|
||||
|
||||
@ -35,41 +38,66 @@ export default function ChatList() {
|
||||
showChatRenameModal();
|
||||
}, [showChatRenameModal]);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isCreate = searchParams.get('isCreate') === 'true';
|
||||
useEffect(() => {
|
||||
if (isCreate) {
|
||||
handleShowCreateModal();
|
||||
searchParams.delete('isCreate');
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
}, [isCreate, handleShowCreateModal, searchParams, setSearchParams]);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col w-full flex-1">
|
||||
<div className="px-8 pt-8">
|
||||
<ListFilterBar
|
||||
title={t('chat.chatApps')}
|
||||
icon="chats"
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
>
|
||||
<Button onClick={handleShowCreateModal}>
|
||||
<Plus className="size-2.5" />
|
||||
{t('chat.createChat')}
|
||||
</Button>
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{data.dialogs.map((x) => {
|
||||
return (
|
||||
<ChatCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showChatRenameModal={showChatRenameModal}
|
||||
></ChatCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
<div className="mt-8 px-8 pb-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
{data.dialogs?.length <= 0 && (
|
||||
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14"
|
||||
type={EmptyCardType.Chat}
|
||||
onClick={() => handleShowCreateModal()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{data.dialogs?.length > 0 && (
|
||||
<>
|
||||
<div className="px-8 pt-8">
|
||||
<ListFilterBar
|
||||
title={t('chat.chatApps')}
|
||||
icon="chats"
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
>
|
||||
<Button onClick={handleShowCreateModal}>
|
||||
<Plus className="size-2.5" />
|
||||
{t('chat.createChat')}
|
||||
</Button>
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{data.dialogs.map((x) => {
|
||||
return (
|
||||
<ChatCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showChatRenameModal={showChatRenameModal}
|
||||
></ChatCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
<div className="mt-8 px-8 pb-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{chatRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideChatRenameModal}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { EmbedContainer } from '@/components/embed-container';
|
||||
import { NextMessageInput } from '@/components/message-input/next';
|
||||
import MessageItem from '@/components/message-item';
|
||||
import PdfDrawer from '@/components/pdf-drawer';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType, SharedFrom } from '@/constants/chat';
|
||||
import { useFetchNextConversationSSE } from '@/hooks/chat-hooks';
|
||||
@ -123,12 +123,12 @@ const ChatContainer = () => {
|
||||
</div>
|
||||
</EmbedContainer>
|
||||
{visible && (
|
||||
<PdfDrawer
|
||||
<PdfSheet
|
||||
visible={visible}
|
||||
hideModal={hideModal}
|
||||
documentId={documentId}
|
||||
chunk={selectedChunk}
|
||||
></PdfDrawer>
|
||||
></PdfSheet>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -17,26 +17,39 @@ import {
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
useAllTestingResult,
|
||||
useChunkIsTesting,
|
||||
useSelectTestingResult,
|
||||
} from '@/hooks/knowledge-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, ChevronDown, Files, XIcon } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface IProps {
|
||||
onTesting(documentIds: string[]): void;
|
||||
setSelectedDocumentIds(documentIds: string[]): void;
|
||||
selectedDocumentIds: string[];
|
||||
setLoading?: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
const RetrievalDocuments = ({
|
||||
onTesting,
|
||||
selectedDocumentIds,
|
||||
setSelectedDocumentIds,
|
||||
setLoading,
|
||||
}: IProps) => {
|
||||
const { documents: documentsAll } = useAllTestingResult();
|
||||
const { documents } = useSelectTestingResult();
|
||||
const isTesting = useChunkIsTesting();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTesting) {
|
||||
setLoading?.(true);
|
||||
} else {
|
||||
setLoading?.(false);
|
||||
}
|
||||
}, [isTesting, setLoading]);
|
||||
|
||||
const { documents: useDocuments } = {
|
||||
documents:
|
||||
documentsAll?.length > documents?.length ? documentsAll : documents,
|
||||
@ -45,6 +58,9 @@ const RetrievalDocuments = ({
|
||||
useState<string[]>(selectedDocumentIds);
|
||||
|
||||
const multiOptions = useMemo(() => {
|
||||
if (!useDocuments || !useDocuments.length) {
|
||||
return [];
|
||||
}
|
||||
return useDocuments?.map((item) => {
|
||||
return {
|
||||
label: item.doc_name,
|
||||
@ -97,36 +113,38 @@ const RetrievalDocuments = ({
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
'flex w-full p-1 rounded-md text-base text-text-primary border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Files />
|
||||
<span>
|
||||
{selectedDocumentIds?.length ?? 0}/{useDocuments?.length ?? 0}
|
||||
</span>
|
||||
Files
|
||||
{useDocuments?.length && (
|
||||
<Button
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
'flex w-full p-1 rounded-md text-base text-text-primary border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Files />
|
||||
<span>
|
||||
{selectedDocumentIds?.length ?? 0}/{useDocuments?.length ?? 0}
|
||||
</span>
|
||||
Files
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<XIcon
|
||||
className="h-4 mx-2 cursor-pointer text-muted-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
/>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex min-h-6 h-full"
|
||||
/>
|
||||
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<XIcon
|
||||
className="h-4 mx-2 cursor-pointer text-muted-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
/>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex min-h-6 h-full"
|
||||
/>
|
||||
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { EmptyType } from '@/components/empty/constant';
|
||||
import Empty from '@/components/empty/empty';
|
||||
import { FileIcon } from '@/components/icon-font';
|
||||
import { ImageWithPopover } from '@/components/image';
|
||||
import { Input } from '@/components/originui/input';
|
||||
@ -65,6 +67,7 @@ export default function SearchingView({
|
||||
// changeLanguage();
|
||||
// }, [i18n]);
|
||||
const [searchtext, setSearchtext] = useState<string>('');
|
||||
const [retrievalLoading, setRetrievalLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchtext(searchStr);
|
||||
@ -182,6 +185,9 @@ export default function SearchingView({
|
||||
selectedDocumentIds={selectedDocumentIds}
|
||||
setSelectedDocumentIds={setSelectedDocumentIds}
|
||||
onTesting={handleTestChunk}
|
||||
setLoading={(loading: boolean) => {
|
||||
setRetrievalLoading(loading);
|
||||
}}
|
||||
></RetrievalDocuments>
|
||||
</div>
|
||||
{/* <div className="w-full border-b border-border-default/80 my-6"></div> */}
|
||||
@ -264,6 +270,17 @@ export default function SearchingView({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isSearchStrEmpty &&
|
||||
!retrievalLoading &&
|
||||
!answer.answer &&
|
||||
!sendingLoading &&
|
||||
total <= 0 &&
|
||||
chunks?.length <= 0 &&
|
||||
relatedQuestions?.length <= 0 && (
|
||||
<div className="h-2/5 flex items-center justify-center">
|
||||
<Empty type={EmptyType.SearchData} iconWidth={80} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{total > 0 && (
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { CardContainer } from '@/components/card-container';
|
||||
import { EmptyCardType } from '@/components/empty/constant';
|
||||
import { EmptyAppCard } from '@/components/empty/empty';
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import ListFilterBar from '@/components/list-filter-bar';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
@ -6,6 +8,8 @@ import { Button } from '@/components/ui/button';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'umi';
|
||||
import { useFetchSearchList, useRenameSearch } from './hooks';
|
||||
import { SearchCard } from './search-card';
|
||||
|
||||
@ -27,6 +31,7 @@ export default function SearchList() {
|
||||
onSearchRenameOk,
|
||||
initialSearchName,
|
||||
} = useRenameSearch();
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
console.log(value);
|
||||
};
|
||||
@ -35,61 +40,86 @@ export default function SearchList() {
|
||||
refetchList();
|
||||
});
|
||||
};
|
||||
const openCreateModalFun = () => {
|
||||
const openCreateModalFun = useCallback(() => {
|
||||
// setIsEdit(false);
|
||||
showSearchRenameModal();
|
||||
};
|
||||
}, [showSearchRenameModal]);
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
// setIsEdit(false);
|
||||
setSearchListParams({ ...searchParams, page, page_size: pageSize });
|
||||
};
|
||||
|
||||
const [searchUrl, setSearchUrl] = useSearchParams();
|
||||
const isCreate = searchUrl.get('isCreate') === 'true';
|
||||
useEffect(() => {
|
||||
if (isCreate) {
|
||||
openCreateModalFun();
|
||||
searchUrl.delete('isCreate');
|
||||
setSearchUrl(searchUrl);
|
||||
}
|
||||
}, [isCreate, openCreateModalFun, searchUrl, setSearchUrl]);
|
||||
|
||||
return (
|
||||
<section className="w-full h-full flex flex-col">
|
||||
<div className="px-8 pt-8">
|
||||
<ListFilterBar
|
||||
icon="searches"
|
||||
title={t('searchApps')}
|
||||
showFilter={false}
|
||||
onSearchChange={(e) => handleSearchChange(e.target.value)}
|
||||
>
|
||||
<Button
|
||||
variant={'default'}
|
||||
onClick={() => {
|
||||
openCreateModalFun();
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('createSearch')}
|
||||
</Button>
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{list?.data.search_apps.map((x) => {
|
||||
return (
|
||||
<SearchCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showSearchRenameModal={() => {
|
||||
showSearchRenameModal(x);
|
||||
}}
|
||||
></SearchCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
{list?.data.total && list?.data.total > 0 && (
|
||||
<div className="px-8 mb-4">
|
||||
<RAGFlowPagination
|
||||
current={searchParams.page}
|
||||
pageSize={searchParams.page_size}
|
||||
total={list?.data.total}
|
||||
onChange={handlePageChange}
|
||||
{(!list?.data?.search_apps?.length ||
|
||||
list?.data?.search_apps?.length <= 0) && (
|
||||
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
|
||||
<EmptyAppCard
|
||||
showIcon
|
||||
size="large"
|
||||
className="w-[480px] p-14"
|
||||
type={EmptyCardType.Search}
|
||||
onClick={() => openCreateModalFun()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!list?.data?.search_apps?.length && (
|
||||
<>
|
||||
<div className="px-8 pt-8">
|
||||
<ListFilterBar
|
||||
icon="searches"
|
||||
title={t('searchApps')}
|
||||
showFilter={false}
|
||||
onSearchChange={(e) => handleSearchChange(e.target.value)}
|
||||
>
|
||||
<Button
|
||||
variant={'default'}
|
||||
onClick={() => {
|
||||
openCreateModalFun();
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('createSearch')}
|
||||
</Button>
|
||||
</ListFilterBar>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
|
||||
{list?.data.search_apps.map((x) => {
|
||||
return (
|
||||
<SearchCard
|
||||
key={x.id}
|
||||
data={x}
|
||||
showSearchRenameModal={() => {
|
||||
showSearchRenameModal(x);
|
||||
}}
|
||||
></SearchCard>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
</div>
|
||||
{list?.data.total && list?.data.total > 0 && (
|
||||
<div className="px-8 mb-4">
|
||||
<RAGFlowPagination
|
||||
current={searchParams.page}
|
||||
pageSize={searchParams.page_size}
|
||||
total={list?.data.total}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{openCreateModal && (
|
||||
<RenameDialog
|
||||
hideModal={hideSearchRenameModal}
|
||||
|
||||
@ -47,6 +47,7 @@ const AddDataSourceModal = ({
|
||||
}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
maskClosable={false}
|
||||
// onOk={() => handleOk()}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
|
||||
@ -0,0 +1,391 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { FileUploader } from '@/components/file-uploader';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import message from '@/components/ui/message';
|
||||
import { FileMimeType } from '@/constants/common';
|
||||
import {
|
||||
pollGmailWebAuthResult,
|
||||
startGmailWebAuth,
|
||||
} from '@/services/data-source-service';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export type GmailTokenFieldProps = {
|
||||
value?: string;
|
||||
onChange: (value: any) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const credentialHasRefreshToken = (content: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return Boolean(parsed?.refresh_token);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const describeCredentials = (content?: string) => {
|
||||
if (!content) return '';
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
if (parsed?.refresh_token) {
|
||||
return 'Uploaded OAuth tokens with a refresh token.';
|
||||
}
|
||||
if (parsed?.installed || parsed?.web) {
|
||||
return 'Client credentials detected. Complete verification to mint long-lived tokens.';
|
||||
}
|
||||
return 'Stored Google credential JSON.';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const GmailTokenField = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: GmailTokenFieldProps) => {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [pendingCredentials, setPendingCredentials] = useState<string>('');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [webAuthLoading, setWebAuthLoading] = useState(false);
|
||||
const [webFlowId, setWebFlowId] = useState<string | null>(null);
|
||||
const [webStatus, setWebStatus] = useState<
|
||||
'idle' | 'waiting' | 'success' | 'error'
|
||||
>('idle');
|
||||
const [webStatusMessage, setWebStatusMessage] = useState('');
|
||||
const webFlowIdRef = useRef<string | null>(null);
|
||||
const webPollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearWebState = useCallback(() => {
|
||||
if (webPollTimerRef.current) {
|
||||
clearTimeout(webPollTimerRef.current);
|
||||
webPollTimerRef.current = null;
|
||||
}
|
||||
webFlowIdRef.current = null;
|
||||
setWebFlowId(null);
|
||||
setWebStatus('idle');
|
||||
setWebStatusMessage('');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (webPollTimerRef.current) {
|
||||
clearTimeout(webPollTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
webFlowIdRef.current = webFlowId;
|
||||
}, [webFlowId]);
|
||||
|
||||
const credentialSummary = useMemo(() => describeCredentials(value), [value]);
|
||||
const hasVerifiedTokens = useMemo(
|
||||
() => Boolean(value && credentialHasRefreshToken(value)),
|
||||
[value],
|
||||
);
|
||||
const hasUploadedButUnverified = useMemo(
|
||||
() => Boolean(value && !hasVerifiedTokens),
|
||||
[hasVerifiedTokens, value],
|
||||
);
|
||||
|
||||
const resetDialog = useCallback(
|
||||
(shouldResetState: boolean) => {
|
||||
setDialogOpen(false);
|
||||
clearWebState();
|
||||
if (shouldResetState) {
|
||||
setPendingCredentials('');
|
||||
setFiles([]);
|
||||
}
|
||||
},
|
||||
[clearWebState],
|
||||
);
|
||||
|
||||
const fetchWebResult = useCallback(
|
||||
async (flowId: string) => {
|
||||
try {
|
||||
const { data } = await pollGmailWebAuthResult({
|
||||
flow_id: flowId,
|
||||
});
|
||||
if (data.code === 0 && data.data?.credentials) {
|
||||
onChange(data.data.credentials);
|
||||
setPendingCredentials('');
|
||||
message.success('Gmail credentials verified.');
|
||||
resetDialog(false);
|
||||
return;
|
||||
}
|
||||
if (data.code === 106) {
|
||||
setWebStatus('waiting');
|
||||
setWebStatusMessage('Authorization confirmed. Finalizing tokens...');
|
||||
if (webPollTimerRef.current) {
|
||||
clearTimeout(webPollTimerRef.current);
|
||||
}
|
||||
webPollTimerRef.current = setTimeout(
|
||||
() => fetchWebResult(flowId),
|
||||
1500,
|
||||
);
|
||||
return;
|
||||
}
|
||||
message.error(data.message || 'Authorization failed.');
|
||||
clearWebState();
|
||||
} catch (err) {
|
||||
message.error('Unable to retrieve authorization result.');
|
||||
clearWebState();
|
||||
}
|
||||
},
|
||||
[clearWebState, onChange, resetDialog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
const payload = event.data;
|
||||
if (!payload || payload.type !== 'ragflow-gmail-oauth') {
|
||||
return;
|
||||
}
|
||||
if (!payload.flowId) {
|
||||
return;
|
||||
}
|
||||
if (webFlowIdRef.current && webFlowIdRef.current !== payload.flowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status === 'success') {
|
||||
setWebStatus('waiting');
|
||||
setWebStatusMessage('Authorization confirmed. Finalizing tokens...');
|
||||
fetchWebResult(payload.flowId);
|
||||
} else {
|
||||
message.error(
|
||||
payload.message || 'Authorization window reported an error.',
|
||||
);
|
||||
clearWebState();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
}, [clearWebState, fetchWebResult]);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(nextFiles: File[]) => {
|
||||
if (!nextFiles.length) {
|
||||
setFiles([]);
|
||||
onChange('');
|
||||
setPendingCredentials('');
|
||||
clearWebState();
|
||||
return;
|
||||
}
|
||||
const file = nextFiles[nextFiles.length - 1];
|
||||
file
|
||||
.text()
|
||||
.then((text) => {
|
||||
try {
|
||||
JSON.parse(text);
|
||||
} catch {
|
||||
message.error('Invalid JSON file.');
|
||||
setFiles([]);
|
||||
clearWebState();
|
||||
return;
|
||||
}
|
||||
setFiles([file]);
|
||||
clearWebState();
|
||||
if (credentialHasRefreshToken(text)) {
|
||||
onChange(text);
|
||||
setPendingCredentials('');
|
||||
message.success('Gmail OAuth credentials uploaded.');
|
||||
return;
|
||||
}
|
||||
setPendingCredentials(text);
|
||||
setDialogOpen(true);
|
||||
message.info(
|
||||
'Client configuration uploaded. Verification is required to finish setup.',
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('Unable to read the uploaded file.');
|
||||
setFiles([]);
|
||||
});
|
||||
},
|
||||
[clearWebState, onChange],
|
||||
);
|
||||
|
||||
const handleStartWebAuthorization = useCallback(async () => {
|
||||
if (!pendingCredentials) {
|
||||
message.error('No Google credential file detected.');
|
||||
return;
|
||||
}
|
||||
setWebAuthLoading(true);
|
||||
clearWebState();
|
||||
try {
|
||||
const { data } = await startGmailWebAuth({
|
||||
credentials: pendingCredentials,
|
||||
});
|
||||
if (data.code === 0 && data.data?.authorization_url) {
|
||||
const flowId = data.data.flow_id;
|
||||
const popup = window.open(
|
||||
data.data.authorization_url,
|
||||
'ragflow-gmail-oauth',
|
||||
'width=600,height=720',
|
||||
);
|
||||
if (!popup) {
|
||||
message.error(
|
||||
'Popup was blocked. Please allow popups for this site.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
popup.focus();
|
||||
webFlowIdRef.current = flowId;
|
||||
setWebFlowId(flowId);
|
||||
setWebStatus('waiting');
|
||||
setWebStatusMessage('Complete the Google consent in the popup window.');
|
||||
} else {
|
||||
message.error(data.message || 'Failed to start browser authorization.');
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('Failed to start browser authorization.');
|
||||
} finally {
|
||||
setWebAuthLoading(false);
|
||||
}
|
||||
}, [clearWebState, pendingCredentials]);
|
||||
|
||||
const handleManualWebCheck = useCallback(() => {
|
||||
if (!webFlowId) {
|
||||
message.info('Start browser authorization first.');
|
||||
return;
|
||||
}
|
||||
setWebStatus('waiting');
|
||||
setWebStatusMessage('Checking authorization status...');
|
||||
fetchWebResult(webFlowId);
|
||||
}, [fetchWebResult, webFlowId]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
message.warning(
|
||||
'Verification canceled. Upload the credential again to restart.',
|
||||
);
|
||||
resetDialog(true);
|
||||
}, [resetDialog]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{(credentialSummary ||
|
||||
hasVerifiedTokens ||
|
||||
hasUploadedButUnverified ||
|
||||
pendingCredentials) && (
|
||||
<div className="flex flex-wrap items-center gap-3 rounded-md border border-dashed border-muted-foreground/40 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{hasVerifiedTokens ? (
|
||||
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-700">
|
||||
Verified
|
||||
</span>
|
||||
) : null}
|
||||
{hasUploadedButUnverified ? (
|
||||
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-amber-700">
|
||||
Needs authorization
|
||||
</span>
|
||||
) : null}
|
||||
{pendingCredentials && !hasVerifiedTokens ? (
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-blue-700">
|
||||
Uploaded (pending)
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{credentialSummary ? (
|
||||
<p className="m-0">{credentialSummary}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<FileUploader
|
||||
className="py-4 border-[0.5px] bg-bg-card text-text-secondary"
|
||||
value={files}
|
||||
onValueChange={handleValueChange}
|
||||
accept={{ '*.json': [FileMimeType.Json] }}
|
||||
maxFileCount={1}
|
||||
description={'Upload your Gmail OAuth JSON file.'}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && dialogOpen) {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Complete Gmail verification</DialogTitle>
|
||||
<DialogDescription>
|
||||
The uploaded client credentials do not contain a refresh token.
|
||||
Run the verification flow once to mint reusable tokens.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border border-dashed border-muted-foreground/40 bg-muted/10 px-4 py-4 text-sm text-muted-foreground">
|
||||
<div className="text-sm font-semibold text-foreground">
|
||||
Authorize in browser
|
||||
</div>
|
||||
<p className="mt-2">
|
||||
We will open Google's consent page in a new window. Sign in
|
||||
with the admin account, grant access, and return here. Your
|
||||
credentials will update automatically.
|
||||
</p>
|
||||
{webStatus !== 'idle' && (
|
||||
<p
|
||||
className={`mt-2 text-xs ${
|
||||
webStatus === 'error'
|
||||
? 'text-destructive'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{webStatusMessage}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={handleStartWebAuthorization}
|
||||
disabled={webAuthLoading}
|
||||
>
|
||||
{webAuthLoading && (
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
Authorize with Google
|
||||
</Button>
|
||||
{webFlowId ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleManualWebCheck}
|
||||
disabled={webStatus === 'success'}
|
||||
>
|
||||
Refresh status
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-2">
|
||||
<Button variant="ghost" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GmailTokenField;
|
||||
@ -1,5 +1,3 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { FileUploader } from '@/components/file-uploader';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -17,6 +15,7 @@ import {
|
||||
startGoogleDriveWebAuth,
|
||||
} from '@/services/data-source-service';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
type GoogleDriveTokenFieldProps = {
|
||||
value?: string;
|
||||
@ -313,12 +312,16 @@ const GoogleDriveTokenField = ({
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
if (!open && dialogOpen) {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogContent
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Complete Google verification</DialogTitle>
|
||||
<DialogDescription>
|
||||
@ -326,7 +329,6 @@ const GoogleDriveTokenField = ({
|
||||
Run the verification flow once to mint reusable tokens.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border border-dashed border-muted-foreground/40 bg-muted/10 px-4 py-4 text-sm text-muted-foreground">
|
||||
<div className="text-sm font-semibold text-foreground">
|
||||
@ -370,7 +372,6 @@ const GoogleDriveTokenField = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-2">
|
||||
<Button variant="ghost" onClick={handleCancel}>
|
||||
Cancel
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { FormFieldType } from '@/components/dynamic-form';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { t } from 'i18next';
|
||||
import GmailTokenField from './component/gmail-token-field';
|
||||
import GoogleDriveTokenField from './component/google-drive-token-field';
|
||||
|
||||
export enum DataSourceKey {
|
||||
@ -10,7 +11,7 @@ export enum DataSourceKey {
|
||||
DISCORD = 'discord',
|
||||
GOOGLE_DRIVE = 'google_drive',
|
||||
MOODLE = 'moodle',
|
||||
// GMAIL = 'gmail',
|
||||
GMAIL = 'gmail',
|
||||
JIRA = 'jira',
|
||||
WEBDAV = 'webdav',
|
||||
DROPBOX = 'dropbox',
|
||||
@ -45,6 +46,11 @@ export const DataSourceInfo = {
|
||||
description: t(`setting.${DataSourceKey.GOOGLE_DRIVE}Description`),
|
||||
icon: <SvgIcon name={'data-source/google-drive'} width={38} />,
|
||||
},
|
||||
[DataSourceKey.GMAIL]: {
|
||||
name: 'Gmail',
|
||||
description: t(`setting.${DataSourceKey.GMAIL}Description`),
|
||||
icon: <SvgIcon name={'data-source/gmail'} width={38} />,
|
||||
},
|
||||
[DataSourceKey.MOODLE]: {
|
||||
name: 'Moodle',
|
||||
description: t(`setting.${DataSourceKey.MOODLE}Description`),
|
||||
@ -320,6 +326,38 @@ export const DataSourceFormFields = {
|
||||
defaultValue: 'uploaded',
|
||||
},
|
||||
],
|
||||
[DataSourceKey.GMAIL]: [
|
||||
{
|
||||
label: 'Primary Admin Email',
|
||||
name: 'config.credentials.google_primary_admin',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'admin@example.com',
|
||||
tooltip: t('setting.gmailPrimaryAdminTip'),
|
||||
},
|
||||
{
|
||||
label: 'OAuth Token JSON',
|
||||
name: 'config.credentials.google_tokens',
|
||||
type: FormFieldType.Textarea,
|
||||
required: true,
|
||||
render: (fieldProps: any) => (
|
||||
<GmailTokenField
|
||||
value={fieldProps.value}
|
||||
onChange={fieldProps.onChange}
|
||||
placeholder='{ "token": "...", "refresh_token": "...", ... }'
|
||||
/>
|
||||
),
|
||||
tooltip: t('setting.gmailTokenTip'),
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
name: 'config.credentials.authentication_method',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
hidden: true,
|
||||
defaultValue: 'uploaded',
|
||||
},
|
||||
],
|
||||
[DataSourceKey.MOODLE]: [
|
||||
{
|
||||
label: 'Moodle URL',
|
||||
@ -550,6 +588,17 @@ export const DataSourceFormDefaultValues = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[DataSourceKey.GMAIL]: {
|
||||
name: '',
|
||||
source: DataSourceKey.GMAIL,
|
||||
config: {
|
||||
credentials: {
|
||||
google_primary_admin: '',
|
||||
google_tokens: '',
|
||||
authentication_method: 'uploaded',
|
||||
},
|
||||
},
|
||||
},
|
||||
[DataSourceKey.MOODLE]: {
|
||||
name: '',
|
||||
source: DataSourceKey.MOODLE,
|
||||
|
||||
@ -96,62 +96,77 @@ export default function McpServer() {
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isSelectionMode && (
|
||||
<section className="pb-5 flex items-center">
|
||||
<Checkbox id="all" onCheckedChange={handleSelectAll} />
|
||||
<Label
|
||||
className="pl-2 text-text-primary cursor-pointer"
|
||||
htmlFor="all"
|
||||
>
|
||||
{t('common.selectAll')}
|
||||
</Label>
|
||||
<span className="text-text-secondary pr-10 pl-5">
|
||||
{t('mcp.selected')} {selectedList.length}
|
||||
</span>
|
||||
<div className="flex gap-10 items-center">
|
||||
<Button variant={'secondary'} onClick={handleExportMcp}>
|
||||
<Upload className="size-3.5"></Upload>
|
||||
{t('mcp.export')}
|
||||
</Button>
|
||||
<ConfirmDeleteDialog
|
||||
onOk={handleDelete}
|
||||
title={t('common.delete') + ' ' + t('mcp.mcpServers')}
|
||||
content={{
|
||||
title: t('common.deleteThem'),
|
||||
node: (
|
||||
<ConfirmDeleteDialogNode
|
||||
name={`${t('mcp.selected')} ${selectedList.length} ${t('mcp.mcpServers')}`}
|
||||
></ConfirmDeleteDialogNode>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Button variant={'danger'}>
|
||||
<Trash2 className="size-3.5 cursor-pointer" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</div>
|
||||
</section>
|
||||
{!data.mcp_servers?.length && (
|
||||
<div
|
||||
className="flex items-center justify-between border border-dashed border-border-button rounded-md p-10 cursor-pointer w-[590px]"
|
||||
onClick={showEditModal('')}
|
||||
>
|
||||
<div className="text-text-secondary text-sm">{t('empty.noMCP')}</div>
|
||||
<Button variant={'ghost'} className="border border-border-button">
|
||||
<Plus className="size-3.5 font-medium" /> {t('empty.addNow')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!!data.mcp_servers?.length && (
|
||||
<>
|
||||
{isSelectionMode && (
|
||||
<section className="pb-5 flex items-center">
|
||||
<Checkbox id="all" onCheckedChange={handleSelectAll} />
|
||||
<Label
|
||||
className="pl-2 text-text-primary cursor-pointer"
|
||||
htmlFor="all"
|
||||
>
|
||||
{t('common.selectAll')}
|
||||
</Label>
|
||||
<span className="text-text-secondary pr-10 pl-5">
|
||||
{t('mcp.selected')} {selectedList.length}
|
||||
</span>
|
||||
<div className="flex gap-10 items-center">
|
||||
<Button variant={'secondary'} onClick={handleExportMcp}>
|
||||
<Upload className="size-3.5"></Upload>
|
||||
{t('mcp.export')}
|
||||
</Button>
|
||||
<ConfirmDeleteDialog
|
||||
onOk={handleDelete}
|
||||
title={t('common.delete') + ' ' + t('mcp.mcpServers')}
|
||||
content={{
|
||||
title: t('common.deleteThem'),
|
||||
node: (
|
||||
<ConfirmDeleteDialogNode
|
||||
name={`${t('mcp.selected')} ${selectedList.length} ${t('mcp.mcpServers')}`}
|
||||
></ConfirmDeleteDialogNode>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Button variant={'danger'}>
|
||||
<Trash2 className="size-3.5 cursor-pointer" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<CardContainer>
|
||||
{data.mcp_servers.map((item) => (
|
||||
<McpCard
|
||||
key={item.id}
|
||||
data={item}
|
||||
selectedList={selectedList}
|
||||
handleSelectChange={handleSelectChange}
|
||||
showEditModal={showEditModal}
|
||||
isSelectionMode={isSelectionMode}
|
||||
></McpCard>
|
||||
))}
|
||||
</CardContainer>
|
||||
<div className="mt-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total || 0}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<CardContainer>
|
||||
{data.mcp_servers.map((item) => (
|
||||
<McpCard
|
||||
key={item.id}
|
||||
data={item}
|
||||
selectedList={selectedList}
|
||||
handleSelectChange={handleSelectChange}
|
||||
showEditModal={showEditModal}
|
||||
isSelectionMode={isSelectionMode}
|
||||
></McpCard>
|
||||
))}
|
||||
</CardContainer>
|
||||
<div className="mt-8">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total || 0}
|
||||
onChange={handlePageChange}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
{editVisible && (
|
||||
<EditMcpDialog
|
||||
hideModal={hideEditModal}
|
||||
|
||||
@ -75,7 +75,9 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
|
||||
<div className="flex items-center space-x-3">
|
||||
<LlmIcon name={item.name} />
|
||||
<div>
|
||||
<div className="font-medium text-xl">{item.name}</div>
|
||||
<div className="font-medium text-xl text-text-primary">
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -34,9 +34,17 @@ export const featchDataSourceDetail = (id: string) =>
|
||||
request.get(api.dataSourceDetail(id));
|
||||
|
||||
export const startGoogleDriveWebAuth = (payload: { credentials: string }) =>
|
||||
request.post(api.googleDriveWebAuthStart, { data: payload });
|
||||
request.post(api.googleWebAuthStart('google-drive'), { data: payload });
|
||||
|
||||
export const pollGoogleDriveWebAuthResult = (payload: { flow_id: string }) =>
|
||||
request.post(api.googleDriveWebAuthResult, { data: payload });
|
||||
request.post(api.googleWebAuthResult('google-drive'), { data: payload });
|
||||
|
||||
// Gmail web auth follows the same pattern as Google Drive, but uses
|
||||
// Gmail-specific endpoints and is consumed by the GmailTokenField UI.
|
||||
export const startGmailWebAuth = (payload: { credentials: string }) =>
|
||||
request.post(api.googleWebAuthStart('gmail'), { data: payload });
|
||||
|
||||
export const pollGmailWebAuthResult = (payload: { flow_id: string }) =>
|
||||
request.post(api.googleWebAuthResult('gmail'), { data: payload });
|
||||
|
||||
export default dataSourceService;
|
||||
|
||||
@ -42,8 +42,10 @@ export default {
|
||||
dataSourceRebuild: (id: string) => `${api_host}/connector/${id}/rebuild`,
|
||||
dataSourceLogs: (id: string) => `${api_host}/connector/${id}/logs`,
|
||||
dataSourceDetail: (id: string) => `${api_host}/connector/${id}`,
|
||||
googleDriveWebAuthStart: `${api_host}/connector/google-drive/oauth/web/start`,
|
||||
googleDriveWebAuthResult: `${api_host}/connector/google-drive/oauth/web/result`,
|
||||
googleWebAuthStart: (type: 'google-drive' | 'gmail') =>
|
||||
`${api_host}/connector/google/oauth/web/start?type=${type}`,
|
||||
googleWebAuthResult: (type: 'google-drive' | 'gmail') =>
|
||||
`${api_host}/connector/google/oauth/web/result?type=${type}`,
|
||||
|
||||
// plugin
|
||||
llm_tools: `${api_host}/plugin/llm_tools`,
|
||||
|
||||