Compare commits
11 Commits
3bd0b99495
...
850e119a81
| Author | SHA1 | Date | |
|---|---|---|---|
| 850e119a81 | |||
| 0a78920bff | |||
| 0089e2b30c | |||
| b7cb4d3e35 | |||
| fd1ad18489 | |||
| 5acc407240 | |||
| 16ec6ad346 | |||
| 5312b75362 | |||
| 33a189f620 | |||
| 56def59c2b | |||
| 7fbab750af |
@ -84,6 +84,7 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
|
||||
## 🔥 Latest Updates
|
||||
|
||||
- 2025-10-23 Supports MinerU & Docling as document parsing methods.
|
||||
- 2025-10-15 Supports orchestrable ingestion pipeline.
|
||||
- 2025-08-08 Supports OpenAI's latest GPT-5 series models.
|
||||
- 2025-08-01 Supports agentic workflow and MCP.
|
||||
|
||||
@ -80,6 +80,7 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
|
||||
## 🔥 Pembaruan Terbaru
|
||||
|
||||
- 2025-10-23 Mendukung MinerU & Docling sebagai metode penguraian dokumen.
|
||||
- 2025-10-15 Dukungan untuk jalur data yang terorkestrasi.
|
||||
- 2025-08-08 Mendukung model seri GPT-5 terbaru dari OpenAI.
|
||||
- 2025-08-01 Mendukung alur kerja agen dan MCP.
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
|
||||
## 🔥 最新情報
|
||||
|
||||
- 2025-10-23 ドキュメント解析方法として MinerU と Docling をサポートします。
|
||||
- 2025-10-15 オーケストレーションされたデータパイプラインのサポート。
|
||||
- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。
|
||||
- 2025-08-01 エージェントワークフローとMCPをサポート。
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
|
||||
## 🔥 업데이트
|
||||
|
||||
- 2025-10-23 문서 파싱 방법으로 MinerU 및 Docling을 지원합니다.
|
||||
- 2025-10-15 조정된 데이터 파이프라인 지원.
|
||||
- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.
|
||||
- 2025-08-01 에이전트 워크플로우와 MCP를 지원합니다.
|
||||
|
||||
@ -80,7 +80,8 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
|
||||
|
||||
## 🔥 Últimas Atualizações
|
||||
|
||||
- 10-15-2025 Suporte para pipelines de dados orquestrados.
|
||||
- 23-10-2025 Suporta MinerU e Docling como métodos de análise de documentos.
|
||||
- 15-10-2025 Suporte para pipelines de dados orquestrados.
|
||||
- 08-08-2025 Suporta a mais recente série GPT-5 da OpenAI.
|
||||
- 01-08-2025 Suporta fluxo de trabalho agente e MCP.
|
||||
- 23-05-2025 Adicione o componente executor de código Python/JS ao Agente.
|
||||
|
||||
@ -83,6 +83,7 @@
|
||||
|
||||
## 🔥 近期更新
|
||||
|
||||
- 2025-10-23 支援 MinerU 和 Docling 作為文件解析方法。
|
||||
- 2025-10-15 支援可編排的資料管道。
|
||||
- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。
|
||||
- 2025-08-01 支援 agentic workflow 和 MCP
|
||||
|
||||
@ -83,6 +83,7 @@
|
||||
|
||||
## 🔥 近期更新
|
||||
|
||||
- 2025-10-23 支持 MinerU 和 Docling 作为文档解析方法。
|
||||
- 2025-10-15 支持可编排的数据管道。
|
||||
- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型。
|
||||
- 2025-08-01 支持 agentic workflow 和 MCP。
|
||||
|
||||
@ -133,3 +133,9 @@ user_default_llm:
|
||||
# - "RAGFlow" # display name
|
||||
# - "" # sender email address
|
||||
# mail_frontend_url: "https://your-frontend.example.com"
|
||||
# tcadp_config:
|
||||
# secret_id: 'tencent_secret_id'
|
||||
# secret_key: 'tencent_secret_key'
|
||||
# region: 'tencent_region'
|
||||
# table_result_type: '1'
|
||||
# markdown_image_response_type: '1'
|
||||
|
||||
@ -45,6 +45,9 @@ class MinerUContentType(StrEnum):
|
||||
TABLE = "table"
|
||||
TEXT = "text"
|
||||
EQUATION = "equation"
|
||||
CODE = "code"
|
||||
LIST = "list"
|
||||
DISCARDED = "discarded"
|
||||
|
||||
|
||||
class MinerUParser(RAGFlowPdfParser):
|
||||
@ -80,8 +83,10 @@ class MinerUParser(RAGFlowPdfParser):
|
||||
logging.error(f"[MinerU] Unexpected error during installation check: {e}")
|
||||
return False
|
||||
|
||||
def _run_mineru(self, input_path: Path, output_dir: Path, method: str = "auto", lang: Optional[str] = None):
|
||||
def _run_mineru(self, input_path: Path, output_dir: Path, method: str = "auto", backend: str = "pipeline", lang: Optional[str] = None):
|
||||
cmd = [str(self.mineru_path), "-p", str(input_path), "-o", str(output_dir), "-m", method]
|
||||
if backend:
|
||||
cmd.extend(["-b", backend])
|
||||
if lang:
|
||||
cmd.extend(["-l", lang])
|
||||
|
||||
@ -231,8 +236,10 @@ class MinerUParser(RAGFlowPdfParser):
|
||||
poss.append(([int(p) - 1 for p in pn.split("-")], left, right, top, bottom))
|
||||
return poss
|
||||
|
||||
def _read_output(self, output_dir: Path, file_stem: str, method: str = "auto") -> list[dict[str, Any]]:
|
||||
def _read_output(self, output_dir: Path, file_stem: str, method: str = "auto", backend: str = "pipeline") -> list[dict[str, Any]]:
|
||||
subdir = output_dir / file_stem / method
|
||||
if backend.startswith("vlm-"):
|
||||
subdir = output_dir / file_stem / "vlm"
|
||||
json_file = subdir / f"{file_stem}_content_list.json"
|
||||
|
||||
if not json_file.exists():
|
||||
@ -259,6 +266,12 @@ class MinerUParser(RAGFlowPdfParser):
|
||||
section = "".join(output["image_caption"]) + "\n" + "".join(output["image_footnote"])
|
||||
case MinerUContentType.EQUATION:
|
||||
section = output["text"]
|
||||
case MinerUContentType.CODE:
|
||||
section = output["code_body"] + "\n".join(output.get("code_caption", []))
|
||||
case MinerUContentType.LIST:
|
||||
section = "\n".join(output.get("list_items", []))
|
||||
case MinerUContentType.DISCARDED:
|
||||
pass
|
||||
|
||||
if section:
|
||||
sections.append((section, self._line_tag(output)))
|
||||
@ -274,6 +287,7 @@ class MinerUParser(RAGFlowPdfParser):
|
||||
callback: Optional[Callable] = None,
|
||||
*,
|
||||
output_dir: Optional[str] = None,
|
||||
backend: str = "pipeline",
|
||||
lang: Optional[str] = None,
|
||||
method: str = "auto",
|
||||
delete_output: bool = True,
|
||||
@ -313,8 +327,8 @@ class MinerUParser(RAGFlowPdfParser):
|
||||
self.__images__(pdf, zoomin=1)
|
||||
|
||||
try:
|
||||
self._run_mineru(pdf, out_dir, method=method, lang=lang)
|
||||
outputs = self._read_output(out_dir, pdf.stem, method=method)
|
||||
self._run_mineru(pdf, out_dir, method=method, backend=backend, lang=lang)
|
||||
outputs = self._read_output(out_dir, pdf.stem, method=method, backend=backend)
|
||||
self.logger.info(f"[MinerU] Parsed {len(outputs)} blocks from PDF.")
|
||||
if callback:
|
||||
callback(0.75, f"[MinerU] Parsed {len(outputs)} blocks from PDF.")
|
||||
|
||||
504
deepdoc/parser/tcadp_parser.py
Normal file
@ -0,0 +1,504 @@
|
||||
#
|
||||
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import requests
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
from tencentcloud.lkeap.v20240522 import lkeap_client, models
|
||||
|
||||
from api.utils.configs import get_base_config
|
||||
from deepdoc.parser.pdf_parser import RAGFlowPdfParser
|
||||
|
||||
|
||||
class TencentCloudAPIClient:
|
||||
"""Tencent Cloud API client using official SDK"""
|
||||
|
||||
def __init__(self, secret_id, secret_key, region):
|
||||
self.secret_id = secret_id
|
||||
self.secret_key = secret_key
|
||||
self.region = region
|
||||
|
||||
# Create credentials
|
||||
self.cred = credential.Credential(secret_id, secret_key)
|
||||
|
||||
# Instantiate an http option, optional, can be skipped if no special requirements
|
||||
self.httpProfile = HttpProfile()
|
||||
self.httpProfile.endpoint = "lkeap.tencentcloudapi.com"
|
||||
|
||||
# Instantiate a client option, optional, can be skipped if no special requirements
|
||||
self.clientProfile = ClientProfile()
|
||||
self.clientProfile.httpProfile = self.httpProfile
|
||||
|
||||
# Instantiate the client object for the product to be requested, clientProfile is optional
|
||||
self.client = lkeap_client.LkeapClient(self.cred, region, self.clientProfile)
|
||||
|
||||
def reconstruct_document_sse(self, file_type, file_url=None, file_base64=None, file_start_page=1, file_end_page=1000, config=None):
|
||||
"""Call document parsing API using official SDK"""
|
||||
try:
|
||||
# Instantiate a request object, each interface corresponds to a request object
|
||||
req = models.ReconstructDocumentSSERequest()
|
||||
|
||||
# Build request parameters
|
||||
params = {
|
||||
"FileType": file_type,
|
||||
"FileStartPageNumber": file_start_page,
|
||||
"FileEndPageNumber": file_end_page,
|
||||
}
|
||||
|
||||
# According to Tencent Cloud API documentation, either FileUrl or FileBase64 parameter must be provided, if both are provided only FileUrl will be used
|
||||
if file_url:
|
||||
params["FileUrl"] = file_url
|
||||
logging.info(f"[TCADP] Using file URL: {file_url}")
|
||||
elif file_base64:
|
||||
params["FileBase64"] = file_base64
|
||||
logging.info(f"[TCADP] Using Base64 data, length: {len(file_base64)} characters")
|
||||
else:
|
||||
raise ValueError("Must provide either FileUrl or FileBase64 parameter")
|
||||
|
||||
if config:
|
||||
params["Config"] = config
|
||||
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
# The returned resp is an instance of ReconstructDocumentSSEResponse, corresponding to the request object
|
||||
resp = self.client.ReconstructDocumentSSE(req)
|
||||
parser_result = {}
|
||||
|
||||
# Output json format string response
|
||||
if isinstance(resp, types.GeneratorType): # Streaming response
|
||||
logging.info("[TCADP] Detected streaming response")
|
||||
for event in resp:
|
||||
logging.info(f"[TCADP] Received event: {event}")
|
||||
if event.get('data'):
|
||||
try:
|
||||
data_dict = json.loads(event['data'])
|
||||
logging.info(f"[TCADP] Parsed data: {data_dict}")
|
||||
|
||||
if data_dict.get('Progress') == "100":
|
||||
parser_result = data_dict
|
||||
logging.info("[TCADP] Document parsing completed!")
|
||||
logging.info(f"[TCADP] Task ID: {data_dict.get('TaskId')}")
|
||||
logging.info(f"[TCADP] Success pages: {data_dict.get('SuccessPageNum')}")
|
||||
logging.info(f"[TCADP] Failed pages: {data_dict.get('FailPageNum')}")
|
||||
|
||||
# Print failed page information
|
||||
failed_pages = data_dict.get("FailedPages", [])
|
||||
if failed_pages:
|
||||
logging.warning("[TCADP] Failed parsing pages:")
|
||||
for page in failed_pages:
|
||||
logging.warning(f"[TCADP] Page number: {page.get('PageNumber')}, Error: {page.get('ErrorMsg')}")
|
||||
|
||||
# Check if there is a download link
|
||||
download_url = data_dict.get("DocumentRecognizeResultUrl")
|
||||
if download_url:
|
||||
logging.info(f"[TCADP] Got download link: {download_url}")
|
||||
else:
|
||||
logging.warning("[TCADP] No download link obtained")
|
||||
|
||||
break # Found final result, exit loop
|
||||
else:
|
||||
# Print progress information
|
||||
progress = data_dict.get("Progress", "0")
|
||||
logging.info(f"[TCADP] Progress: {progress}%")
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"[TCADP] Failed to parse JSON data: {e}")
|
||||
logging.error(f"[TCADP] Raw data: {event.get('data')}")
|
||||
continue
|
||||
else:
|
||||
logging.info(f"[TCADP] Event without data: {event}")
|
||||
else: # Non-streaming response
|
||||
logging.info("[TCADP] Detected non-streaming response")
|
||||
if hasattr(resp, 'data') and resp.data:
|
||||
try:
|
||||
data_dict = json.loads(resp.data)
|
||||
parser_result = data_dict
|
||||
logging.info(f"[TCADP] JSON parsing successful: {parser_result}")
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"[TCADP] JSON parsing failed: {e}")
|
||||
return None
|
||||
else:
|
||||
logging.error("[TCADP] No data in response")
|
||||
return None
|
||||
|
||||
return parser_result
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
logging.error(f"[TCADP] Tencent Cloud SDK error: {err}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"[TCADP] Unknown error: {e}")
|
||||
logging.error(f"[TCADP] Error stack trace: {traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
def download_result_file(self, download_url, output_dir):
|
||||
"""Download parsing result file"""
|
||||
if not download_url:
|
||||
logging.warning("[TCADP] No downloadable result file")
|
||||
return None
|
||||
|
||||
try:
|
||||
response = requests.get(download_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Generate filename
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"tcadp_result_{timestamp}.zip"
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
logging.info(f"[TCADP] Document parsing result downloaded to: {os.path.basename(file_path)}")
|
||||
return file_path
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"[TCADP] Failed to download file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class TCADPParser(RAGFlowPdfParser):
|
||||
def __init__(self, secret_id: str = None, secret_key: str = None, region: str = "ap-guangzhou"):
|
||||
super().__init__()
|
||||
|
||||
# First initialize logger
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
# Priority: read configuration from RAGFlow configuration system (service_conf.yaml)
|
||||
try:
|
||||
tcadp_parser = get_base_config("tcadp_config", {})
|
||||
if isinstance(tcadp_parser, dict) and tcadp_parser:
|
||||
self.secret_id = secret_id or tcadp_parser.get("secret_id")
|
||||
self.secret_key = secret_key or tcadp_parser.get("secret_key")
|
||||
self.region = region or tcadp_parser.get("region", "ap-guangzhou")
|
||||
self.table_result_type = tcadp_parser.get("table_result_type", "1")
|
||||
self.markdown_image_response_type = tcadp_parser.get("markdown_image_response_type", "1")
|
||||
self.logger.info("[TCADP] Configuration read from service_conf.yaml")
|
||||
else:
|
||||
self.logger.error("[TCADP] Please configure tcadp_config in service_conf.yaml first")
|
||||
|
||||
except ImportError:
|
||||
self.logger.info("[TCADP] Configuration module import failed")
|
||||
|
||||
if not self.secret_id or not self.secret_key:
|
||||
raise ValueError("[TCADP] Please set Tencent Cloud API keys, configure tcadp_config in service_conf.yaml")
|
||||
|
||||
def check_installation(self) -> bool:
|
||||
"""Check if Tencent Cloud API configuration is correct"""
|
||||
try:
|
||||
# Check necessary configuration parameters
|
||||
if not self.secret_id or not self.secret_key:
|
||||
self.logger.error("[TCADP] Tencent Cloud API configuration incomplete")
|
||||
return False
|
||||
|
||||
# Try to create client to verify configuration
|
||||
TencentCloudAPIClient(self.secret_id, self.secret_key, self.region)
|
||||
self.logger.info("[TCADP] Tencent Cloud API configuration check passed")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"[TCADP] Tencent Cloud API configuration check failed: {e}")
|
||||
return False
|
||||
|
||||
def _file_to_base64(self, file_path: str, binary: bytes = None) -> str:
|
||||
"""Convert file to Base64 format"""
|
||||
|
||||
if binary:
|
||||
# If binary data is directly available, convert directly
|
||||
return base64.b64encode(binary).decode('utf-8')
|
||||
else:
|
||||
# Read from file path and convert
|
||||
with open(file_path, 'rb') as f:
|
||||
file_data = f.read()
|
||||
return base64.b64encode(file_data).decode('utf-8')
|
||||
|
||||
def _extract_content_from_zip(self, zip_path: str) -> list[dict[str, Any]]:
|
||||
"""Extract parsing results from downloaded ZIP file"""
|
||||
results = []
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_file:
|
||||
# Find JSON result files
|
||||
json_files = [f for f in zip_file.namelist() if f.endswith(".json")]
|
||||
|
||||
for json_file in json_files:
|
||||
with zip_file.open(json_file) as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
results.extend(data)
|
||||
else:
|
||||
results.append(data)
|
||||
|
||||
# Find Markdown files
|
||||
md_files = [f for f in zip_file.namelist() if f.endswith(".md")]
|
||||
for md_file in md_files:
|
||||
with zip_file.open(md_file) as f:
|
||||
content = f.read().decode("utf-8")
|
||||
results.append({"type": "text", "content": content, "file": md_file})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[TCADP] Failed to extract ZIP file content: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def _parse_content_to_sections(self, content_data: list[dict[str, Any]]) -> list[tuple[str, str]]:
|
||||
"""Convert parsing results to sections format"""
|
||||
sections = []
|
||||
|
||||
for item in content_data:
|
||||
content_type = item.get("type", "text")
|
||||
content = item.get("content", "")
|
||||
|
||||
if not content:
|
||||
continue
|
||||
|
||||
# Process based on content type
|
||||
if content_type == "text" or content_type == "paragraph":
|
||||
section_text = content
|
||||
elif content_type == "table":
|
||||
# Handle table content
|
||||
table_data = item.get("table_data", {})
|
||||
if isinstance(table_data, dict):
|
||||
# Convert table data to text
|
||||
rows = table_data.get("rows", [])
|
||||
section_text = "\n".join([" | ".join(row) for row in rows])
|
||||
else:
|
||||
section_text = str(table_data)
|
||||
elif content_type == "image":
|
||||
# Handle image content
|
||||
caption = item.get("caption", "")
|
||||
section_text = f"[Image] {caption}" if caption else "[Image]"
|
||||
elif content_type == "equation":
|
||||
# Handle equation content
|
||||
section_text = f"$${content}$$"
|
||||
else:
|
||||
section_text = content
|
||||
|
||||
if section_text.strip():
|
||||
# Generate position tag (simplified version)
|
||||
position_tag = "@@1\t0.0\t1000.0\t0.0\t100.0##"
|
||||
sections.append((section_text, position_tag))
|
||||
|
||||
return sections
|
||||
|
||||
def _parse_content_to_tables(self, content_data: list[dict[str, Any]]) -> list:
|
||||
"""Convert parsing results to tables format"""
|
||||
tables = []
|
||||
|
||||
for item in content_data:
|
||||
if item.get("type") == "table":
|
||||
table_data = item.get("table_data", {})
|
||||
if isinstance(table_data, dict):
|
||||
rows = table_data.get("rows", [])
|
||||
if rows:
|
||||
# Convert to table format
|
||||
table_html = "<table>\n"
|
||||
for i, row in enumerate(rows):
|
||||
table_html += " <tr>\n"
|
||||
for cell in row:
|
||||
tag = "th" if i == 0 else "td"
|
||||
table_html += f" <{tag}>{cell}</{tag}>\n"
|
||||
table_html += " </tr>\n"
|
||||
table_html += "</table>"
|
||||
tables.append(table_html)
|
||||
|
||||
return tables
|
||||
|
||||
def parse_pdf(
|
||||
self,
|
||||
filepath: str | PathLike[str],
|
||||
binary: BytesIO | bytes,
|
||||
callback: Optional[Callable] = None,
|
||||
*,
|
||||
output_dir: Optional[str] = None,
|
||||
file_type: str = "PDF",
|
||||
file_start_page: Optional[int] = 1,
|
||||
file_end_page: Optional[int] = 1000,
|
||||
delete_output: Optional[bool] = True,
|
||||
max_retries: Optional[int] = 1,
|
||||
) -> tuple:
|
||||
"""Parse PDF document"""
|
||||
|
||||
temp_file = None
|
||||
created_tmp_dir = False
|
||||
|
||||
try:
|
||||
# Handle input file
|
||||
if binary:
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
||||
temp_file.write(binary)
|
||||
temp_file.close()
|
||||
file_path = temp_file.name
|
||||
self.logger.info(f"[TCADP] Received binary PDF -> {os.path.basename(file_path)}")
|
||||
if callback:
|
||||
callback(0.1, f"[TCADP] Received binary PDF -> {os.path.basename(file_path)}")
|
||||
else:
|
||||
file_path = str(filepath)
|
||||
if not os.path.exists(file_path):
|
||||
if callback:
|
||||
callback(-1, f"[TCADP] PDF file does not exist: {file_path}")
|
||||
raise FileNotFoundError(f"[TCADP] PDF file does not exist: {file_path}")
|
||||
|
||||
# Convert file to Base64 format
|
||||
if callback:
|
||||
callback(0.2, "[TCADP] Converting file to Base64 format")
|
||||
|
||||
file_base64 = self._file_to_base64(file_path, binary)
|
||||
if callback:
|
||||
callback(0.25, f"[TCADP] File converted to Base64, size: {len(file_base64)} characters")
|
||||
|
||||
# Create Tencent Cloud API client
|
||||
client = TencentCloudAPIClient(self.secret_id, self.secret_key, self.region)
|
||||
|
||||
# Call document parsing API (with retry mechanism)
|
||||
if callback:
|
||||
callback(0.3, "[TCADP] Starting to call Tencent Cloud document parsing API")
|
||||
|
||||
result = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
if attempt > 0:
|
||||
self.logger.info(f"[TCADP] Retry attempt {attempt + 1}")
|
||||
if callback:
|
||||
callback(0.3 + attempt * 0.1, f"[TCADP] Retry attempt {attempt + 1}")
|
||||
time.sleep(2 ** attempt) # Exponential backoff
|
||||
|
||||
config = {
|
||||
"TableResultType": self.table_result_type,
|
||||
"MarkdownImageResponseType": self.markdown_image_response_type
|
||||
}
|
||||
|
||||
result = client.reconstruct_document_sse(
|
||||
file_type=file_type,
|
||||
file_base64=file_base64,
|
||||
file_start_page=file_start_page,
|
||||
file_end_page=file_end_page,
|
||||
config=config
|
||||
)
|
||||
|
||||
if result:
|
||||
self.logger.info(f"[TCADP] Attempt {attempt + 1} successful")
|
||||
break
|
||||
else:
|
||||
self.logger.warning(f"[TCADP] Attempt {attempt + 1} failed, result is None")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[TCADP] Attempt {attempt + 1} exception: {e}")
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
|
||||
if not result:
|
||||
error_msg = f"[TCADP] Document parsing failed, retried {max_retries} times"
|
||||
self.logger.error(error_msg)
|
||||
if callback:
|
||||
callback(-1, error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# Get download link
|
||||
download_url = result.get("DocumentRecognizeResultUrl")
|
||||
if not download_url:
|
||||
if callback:
|
||||
callback(-1, "[TCADP] No parsing result download link obtained")
|
||||
raise RuntimeError("[TCADP] No parsing result download link obtained")
|
||||
|
||||
if callback:
|
||||
callback(0.6, f"[TCADP] Parsing result download link: {download_url}")
|
||||
|
||||
# Set output directory
|
||||
if output_dir:
|
||||
out_dir = Path(output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
out_dir = Path(tempfile.mkdtemp(prefix="adp_pdf_"))
|
||||
created_tmp_dir = True
|
||||
|
||||
# Download result file
|
||||
zip_path = client.download_result_file(download_url, str(out_dir))
|
||||
if not zip_path:
|
||||
if callback:
|
||||
callback(-1, "[TCADP] Failed to download parsing result")
|
||||
raise RuntimeError("[TCADP] Failed to download parsing result")
|
||||
|
||||
if callback:
|
||||
# Shorten file path display, only show filename
|
||||
zip_filename = os.path.basename(zip_path)
|
||||
callback(0.8, f"[TCADP] Parsing result downloaded: {zip_filename}")
|
||||
|
||||
# Extract ZIP file content
|
||||
content_data = self._extract_content_from_zip(zip_path)
|
||||
self.logger.info(f"[TCADP] Extracted {len(content_data)} content blocks")
|
||||
|
||||
if callback:
|
||||
callback(0.9, f"[TCADP] Extracted {len(content_data)} content blocks")
|
||||
|
||||
# Convert to sections and tables format
|
||||
sections = self._parse_content_to_sections(content_data)
|
||||
tables = self._parse_content_to_tables(content_data)
|
||||
|
||||
self.logger.info(f"[TCADP] Parsing completed: {len(sections)} sections, {len(tables)} tables")
|
||||
|
||||
if callback:
|
||||
callback(1.0, f"[TCADP] Parsing completed: {len(sections)} sections, {len(tables)} tables")
|
||||
|
||||
return sections, tables
|
||||
|
||||
finally:
|
||||
# Clean up temporary files
|
||||
if temp_file and os.path.exists(temp_file.name):
|
||||
try:
|
||||
os.unlink(temp_file.name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if delete_output and created_tmp_dir and out_dir.exists():
|
||||
try:
|
||||
shutil.rmtree(out_dir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test ADP parser
|
||||
parser = TCADPParser()
|
||||
print("ADP available:", parser.check_installation())
|
||||
|
||||
# Test parsing
|
||||
filepath = ""
|
||||
if filepath and os.path.exists(filepath):
|
||||
with open(filepath, "rb") as file:
|
||||
sections, tables = parser.parse_pdf(filepath=filepath, binary=file.read())
|
||||
print(f"Parsing result: {len(sections)} sections, {len(tables)} tables")
|
||||
for i, (section, tag) in enumerate(sections[:3]): # Only print first 3
|
||||
print(f"Section {i + 1}: {section[:100]}...")
|
||||
@ -221,3 +221,4 @@ REGISTER_ENABLED=1
|
||||
# - For OpenSearch:
|
||||
# COMPOSE_PROFILES=opensearch,sandbox
|
||||
USE_DOCLING=false
|
||||
USE_MINERU=false
|
||||
@ -179,18 +179,52 @@ function start_mcp_server() {
|
||||
}
|
||||
|
||||
function ensure_docling() {
|
||||
if [[ "${USE_DOCLING}" == "true" ]]; then
|
||||
if ! python3 -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('docling') else 1)"; then
|
||||
echo "[docling] not found, installing..."
|
||||
python3 -m pip install --no-cache-dir "docling${DOCLING_VERSION:-}"
|
||||
else
|
||||
echo "[docling] already installed, skip."
|
||||
[[ "${USE_DOCLING}" == "true" ]] || return 0
|
||||
python3 -c 'import pip' >/dev/null 2>&1 || python3 -m ensurepip --upgrade || true
|
||||
DOCLING_PIN="${DOCLING_VERSION:-==2.58.0}"
|
||||
python3 -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('docling') else 1)" \
|
||||
|| python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --extra-index-url https://pypi.org/simple --no-cache-dir "docling${DOCLING_PIN}"
|
||||
}
|
||||
|
||||
function ensure_mineru() {
|
||||
[[ "${USE_MINERU}" == "true" ]] || { echo "[mineru] disabled by USE_MINERU"; return 0; }
|
||||
|
||||
export HUGGINGFACE_HUB_ENDPOINT="${HF_ENDPOINT:-https://hf-mirror.com}"
|
||||
|
||||
local default_prefix="/ragflow/uv_tools"
|
||||
local venv_dir="${default_prefix}/.venv"
|
||||
local exe="${MINERU_EXECUTABLE:-${venv_dir}/bin/mineru}"
|
||||
|
||||
if [[ -x "${exe}" ]]; then
|
||||
echo "[mineru] found: ${exe}"
|
||||
export MINERU_EXECUTABLE="${exe}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[mineru] not found, bootstrapping with uv ..."
|
||||
|
||||
(
|
||||
set -e
|
||||
mkdir -p "${default_prefix}"
|
||||
cd "${default_prefix}"
|
||||
[[ -d "${venv_dir}" ]] || uv venv "${venv_dir}"
|
||||
|
||||
source "${venv_dir}/bin/activate"
|
||||
uv pip install -U "mineru[core]" -i https://mirrors.aliyun.com/pypi/simple --extra-index-url https://pypi.org/simple
|
||||
deactivate
|
||||
)
|
||||
export MINERU_EXECUTABLE="${exe}"
|
||||
if ! "${MINERU_EXECUTABLE}" --help >/dev/null 2>&1; then
|
||||
echo "[mineru] installation failed: ${MINERU_EXECUTABLE} not working" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "[mineru] installed: ${MINERU_EXECUTABLE}"
|
||||
}
|
||||
# -----------------------------------------------------------------------------
|
||||
# Start components based on flags
|
||||
# -----------------------------------------------------------------------------
|
||||
ensure_docling
|
||||
ensure_mineru
|
||||
|
||||
if [[ "${ENABLE_WEBSERVER}" -eq 1 ]]; then
|
||||
echo "Starting nginx..."
|
||||
@ -213,7 +247,6 @@ if [[ "${ENABLE_MCP_SERVER}" -eq 1 ]]; then
|
||||
start_mcp_server
|
||||
fi
|
||||
|
||||
ensure_docling
|
||||
|
||||
if [[ "${ENABLE_TASKEXECUTOR}" -eq 1 ]]; then
|
||||
if [[ "${CONSUMER_NO_END}" -gt "${CONSUMER_NO_BEG}" ]]; then
|
||||
|
||||
@ -138,3 +138,9 @@ user_default_llm:
|
||||
# - "RAGFlow" # display name
|
||||
# - "" # sender email address
|
||||
# mail_frontend_url: "https://your-frontend.example.com"
|
||||
# tcadp_config:
|
||||
# secret_id: '${TENCENT_SECRET_ID}'
|
||||
# secret_key: '${TENCENT_SECRET_KEY}'
|
||||
# region: '${TENCENT_REGION}'
|
||||
# table_result_type: '1'
|
||||
# markdown_image_response_type: '1'
|
||||
|
||||
10
docs/faq.mdx
@ -536,5 +536,15 @@ uv pip install -U "mineru[core]" -i https://mirrors.aliyun.com/pypi/simple
|
||||
4. In the web UI, navigate to the **Configuration** page of your dataset. Click **Built-in** in the **Ingestion pipeline** section, select a chunking method from the **Built-in** dropdown, which supports PDF parsing, and slect **MinerU** in **PDF parser**.
|
||||
5. If you use a custom ingestion pipeline instead, you must also complete the first three steps before selecting **MinerU** in the **Parsing method** section of the **Parser** component.
|
||||
|
||||
---
|
||||
|
||||
### How to configure MinerU-specific settings?
|
||||
|
||||
1. Set `MINERU_EXECUTABLE` (default: `mineru`) to the path of the MinerU executable.
|
||||
2. Set `MINERU_DELETE_OUTPUT` to `0` to keep MinerU's output. (Default: `1`, which deletes temporary output)
|
||||
3. Set `MINERU_OUTPUT_DIR` to specify the output directory for MinerU.
|
||||
4. Set `MINERU_BACKEND` to `"pipeline"`. (Options: `"pipeline"` (default) | `"vlm-transformers"`)
|
||||
|
||||
:::tip NOTE
|
||||
For information about other environment variables natively supported by MinerU, see [here](https://opendatalab.github.io/MinerU/usage/cli_tools/#environment-variables-description).
|
||||
:::
|
||||
|
||||
@ -98,7 +98,7 @@ dependencies = [
|
||||
"strenum==0.4.15",
|
||||
"tabulate==0.9.0",
|
||||
"tavily-python==0.5.1",
|
||||
"tencentcloud-sdk-python==3.0.1215",
|
||||
"tencentcloud-sdk-python==3.0.1478",
|
||||
"tika==2.6.0",
|
||||
"tiktoken==0.7.0",
|
||||
"umap_learn==0.5.6",
|
||||
|
||||
@ -80,12 +80,21 @@ class Docx(DocxParser):
|
||||
img = paragraph._element.xpath('.//pic:pic')
|
||||
if not img:
|
||||
return None
|
||||
img = img[0]
|
||||
embed = img.xpath('.//a:blip/@r:embed')[0]
|
||||
related_part = document.part.related_parts[embed]
|
||||
image = related_part.image
|
||||
image = Image.open(BytesIO(image.blob))
|
||||
return image
|
||||
try:
|
||||
img = img[0]
|
||||
embed = img.xpath('.//a:blip/@r:embed')[0]
|
||||
related_part = document.part.related_parts[embed]
|
||||
image = related_part.image
|
||||
if image is not None:
|
||||
image = Image.open(BytesIO(image.blob))
|
||||
return image
|
||||
elif related_part.blob is not None:
|
||||
image = Image.open(BytesIO(related_part.blob))
|
||||
return image
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def concat_img(self, img1, img2):
|
||||
if img1 and not img2:
|
||||
|
||||
@ -36,6 +36,7 @@ from deepdoc.parser.figure_parser import VisionFigureParser,vision_figure_parser
|
||||
from deepdoc.parser.pdf_parser import PlainParser, VisionParser
|
||||
from deepdoc.parser.mineru_parser import MinerUParser
|
||||
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
|
||||
|
||||
|
||||
@ -438,7 +439,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
Successive text will be sliced into pieces using 'delimiter'.
|
||||
Next, these successive pieces are merge into chunks whose token number is no more than 'Max token number'.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
is_english = lang.lower() == "english" # is_english(cks)
|
||||
parser_config = kwargs.get(
|
||||
@ -462,7 +463,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
embeds = extract_embed_file(binary)
|
||||
else:
|
||||
raise Exception("Embedding extraction from file path is not supported.")
|
||||
|
||||
|
||||
# Recursively chunk each embedded file and collect results
|
||||
for embed_filename, embed_bytes in embeds:
|
||||
try:
|
||||
@ -476,7 +477,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
if re.search(r"\.docx$", filename, re.IGNORECASE):
|
||||
callback(0.1, "Start to parse.")
|
||||
|
||||
|
||||
|
||||
|
||||
# fix "There is no item named 'word/NULL' in the archive", referring to https://github.com/python-openxml/python-docx/issues/1105#issuecomment-1298075246
|
||||
_SerializedRelationships.load_from_xml = load_from_xml_v2
|
||||
@ -529,6 +530,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
binary=binary,
|
||||
callback=callback,
|
||||
output_dir=os.environ.get("MINERU_OUTPUT_DIR", ""),
|
||||
backend=os.environ.get("MINERU_BACKEND", "pipeline"),
|
||||
delete_output=bool(int(os.environ.get("MINERU_DELETE_OUTPUT", 1))),
|
||||
)
|
||||
parser_config["chunk_token_num"] = 0
|
||||
@ -550,7 +552,22 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
|
||||
parser_config["chunk_token_num"] = 0
|
||||
res = tokenize_table(tables, doc, is_english)
|
||||
callback(0.8, "Finish parsing.")
|
||||
|
||||
|
||||
elif layout_recognizer == "TCADP Parser":
|
||||
tcadp_parser = TCADPParser()
|
||||
if not tcadp_parser.check_installation():
|
||||
callback(-1, "TCADP parser not available. Please check Tencent Cloud API configuration.")
|
||||
return res
|
||||
|
||||
sections, tables = tcadp_parser.parse_pdf(
|
||||
filepath=filename,
|
||||
binary=binary,
|
||||
callback=callback,
|
||||
output_dir=os.environ.get("TCADP_OUTPUT_DIR", ""),
|
||||
file_type="PDF"
|
||||
)
|
||||
parser_config["chunk_token_num"] = 0
|
||||
callback(0.8, "Finish parsing.")
|
||||
else:
|
||||
if layout_recognizer == "Plain Text":
|
||||
pdf_parser = PlainParser()
|
||||
|
||||
@ -31,6 +31,7 @@ from api.utils.base64_image import image2id
|
||||
from deepdoc.parser import ExcelParser
|
||||
from deepdoc.parser.mineru_parser import MinerUParser
|
||||
from deepdoc.parser.pdf_parser import PlainParser, RAGFlowPdfParser, VisionParser
|
||||
from deepdoc.parser.tcadp_parser import TCADPParser
|
||||
from rag.app.naive import Docx
|
||||
from rag.flow.base import ProcessBase, ProcessParamBase
|
||||
from rag.flow.parser.schema import ParserFromUpstream
|
||||
@ -74,7 +75,7 @@ class ParserParam(ProcessParamBase):
|
||||
|
||||
self.setups = {
|
||||
"pdf": {
|
||||
"parse_method": "deepdoc", # deepdoc/plain_text/vlm
|
||||
"parse_method": "deepdoc", # deepdoc/plain_text/tcadp_parser/vlm
|
||||
"lang": "Chinese",
|
||||
"suffix": [
|
||||
"pdf",
|
||||
@ -157,7 +158,7 @@ class ParserParam(ProcessParamBase):
|
||||
pdf_parse_method = pdf_config.get("parse_method", "")
|
||||
self.check_empty(pdf_parse_method, "Parse method abnormal.")
|
||||
|
||||
if pdf_parse_method.lower() not in ["deepdoc", "plain_text", "mineru"]:
|
||||
if pdf_parse_method.lower() not in ["deepdoc", "plain_text", "mineru", "tcadp parser"]:
|
||||
self.check_empty(pdf_config.get("lang", ""), "PDF VLM language")
|
||||
|
||||
pdf_output_format = pdf_config.get("output_format", "")
|
||||
@ -240,6 +241,39 @@ class Parser(ProcessBase):
|
||||
"text": t,
|
||||
}
|
||||
bboxes.append(box)
|
||||
elif conf.get("parse_method").lower() == "tcadp parser":
|
||||
# ADP is a document parsing tool using Tencent Cloud API
|
||||
tcadp_parser = TCADPParser()
|
||||
sections, _ = tcadp_parser.parse_pdf(
|
||||
filepath=name,
|
||||
binary=blob,
|
||||
callback=self.callback,
|
||||
file_type="PDF",
|
||||
file_start_page=1,
|
||||
file_end_page=1000
|
||||
)
|
||||
bboxes = []
|
||||
for section, position_tag in sections:
|
||||
if position_tag:
|
||||
# Extract position information from TCADP's position tag
|
||||
# Format: @@{page_number}\t{x0}\t{x1}\t{top}\t{bottom}##
|
||||
import re
|
||||
match = re.match(r"@@([0-9-]+)\t([0-9.]+)\t([0-9.]+)\t([0-9.]+)\t([0-9.]+)##", position_tag)
|
||||
if match:
|
||||
pn, x0, x1, top, bott = match.groups()
|
||||
bboxes.append({
|
||||
"page_number": int(pn.split('-')[0]), # Take the first page number
|
||||
"x0": float(x0),
|
||||
"x1": float(x1),
|
||||
"top": float(top),
|
||||
"bottom": float(bott),
|
||||
"text": section
|
||||
})
|
||||
else:
|
||||
# If no position info, add as text without position
|
||||
bboxes.append({"text": section})
|
||||
else:
|
||||
bboxes.append({"text": section})
|
||||
else:
|
||||
vision_model = LLMBundle(self._canvas._tenant_id, LLMType.IMAGE2TEXT, llm_name=conf.get("parse_method"), lang=self._param.setups["pdf"].get("lang"))
|
||||
lines, _ = VisionParser(vision_model=vision_model)(blob, callback=self.callback)
|
||||
|
||||
@ -40,7 +40,7 @@ class Session(Base):
|
||||
If stream=False, returns a single Message object for the final answer.
|
||||
"""
|
||||
if self.__session_type == "agent":
|
||||
res = self._ask_agent(question, stream)
|
||||
res = self._ask_agent(question, stream, **kwargs)
|
||||
elif self.__session_type == "chat":
|
||||
res = self._ask_chat(question, stream, **kwargs)
|
||||
else:
|
||||
@ -97,9 +97,11 @@ class Session(Base):
|
||||
json_data, stream=stream)
|
||||
return res
|
||||
|
||||
def _ask_agent(self, question: str, stream: bool):
|
||||
def _ask_agent(self, question: str, stream: bool, **kwargs):
|
||||
json_data = {"question": question, "stream": stream, "session_id": self.id}
|
||||
json_data.update(kwargs)
|
||||
res = self.post(f"/agents/{self.agent_id}/completions",
|
||||
{"question": question, "stream": stream, "session_id": self.id}, stream=stream)
|
||||
json_data, stream=stream)
|
||||
return res
|
||||
|
||||
def update(self, update_message):
|
||||
|
||||
8
uv.lock
generated
@ -5450,7 +5450,7 @@ requires-dist = [
|
||||
{ name = "strenum", specifier = "==0.4.15" },
|
||||
{ name = "tabulate", specifier = "==0.9.0" },
|
||||
{ name = "tavily-python", specifier = "==0.5.1" },
|
||||
{ name = "tencentcloud-sdk-python", specifier = "==3.0.1215" },
|
||||
{ name = "tencentcloud-sdk-python", specifier = "==3.0.1478" },
|
||||
{ name = "tika", specifier = "==2.6.0" },
|
||||
{ name = "tiktoken", specifier = "==0.7.0" },
|
||||
{ name = "trio", specifier = ">=0.29.0" },
|
||||
@ -6508,14 +6508,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tencentcloud-sdk-python"
|
||||
version = "3.0.1215"
|
||||
version = "3.0.1478"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/4c/7a320c65d605e817bedd1205c77a612be7d4dde621182cc7c00e334207ce/tencentcloud-sdk-python-3.0.1215.tar.gz", hash = "sha256:24441e69d418301d50be0279cb148a747fc272b836e41d18e213750093f490c6", size = 9566281, upload-time = "2024-08-19T20:24:26.541Z" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/47/05163b257f6c0e60aed4272d48bdb816567ab3c805d3e8770430f0cc1be2/tencentcloud-sdk-python-3.0.1478.tar.gz", hash = "sha256:89996462d53a672946aa32d01673a4818ebcd8bc72b024f35ebe96cebe2df179", size = 12297889, upload_time = "2025-10-20T20:54:40.603Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/08/98090d1a139e8995053ed22e099b43aa4dea8cffe056f8f0bc5178aeecbd/tencentcloud_sdk_python-3.0.1215-py2.py3-none-any.whl", hash = "sha256:899ced749baf74846f1eabf452f74aa0e48d1965f0ca7828a8b73b446f76f5f2", size = 10265517, upload-time = "2024-08-19T20:24:19.52Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/db/daa85799b9af2aa50539b27eeb0d6a2a0ac35465f62683107847830dbe4d/tencentcloud_sdk_python-3.0.1478-py2.py3-none-any.whl", hash = "sha256:10ddee1c1348f49e2b54af606f978d4cb17fca656639e8d99b6527e6e4793833", size = 12984723, upload_time = "2025-10-20T20:54:27.767Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
10
web/src/assets/svg/home-icon/agents-bri.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5V18H18C18.5304 18 19.0391 18.2107 19.4142 18.5858C19.7893 18.9609 20 19.4696 20 20V21M12 13H16M12 8H20M16 8V5C16 4.46957 16.2107 3.96086 16.5858 3.58579C16.9609 3.21071 17.4696 3 18 3M16.5 13C16.5 13.2761 16.2761 13.5 16 13.5C15.7239 13.5 15.5 13.2761 15.5 13C15.5 12.7239 15.7239 12.5 16 12.5C16.2761 12.5 16.5 12.7239 16.5 13ZM18.5 3C18.5 3.27614 18.2761 3.5 18 3.5C17.7239 3.5 17.5 3.27614 17.5 3C17.5 2.72386 17.7239 2.5 18 2.5C18.2761 2.5 18.5 2.72386 18.5 3ZM20.5 21C20.5 21.2761 20.2761 21.5 20 21.5C19.7239 21.5 19.5 21.2761 19.5 21C19.5 20.7239 19.7239 20.5 20 20.5C20.2761 20.5 20.5 20.7239 20.5 21ZM20.5 8C20.5 8.27614 20.2761 8.5 20 8.5C19.7239 8.5 19.5 8.27614 19.5 8C19.5 7.72386 19.7239 7.5 20 7.5C20.2761 7.5 20.5 7.72386 20.5 8Z" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.00307 5.12505C5.98523 4.72548 6.04747 4.32637 6.18613 3.9512C6.32478 3.57604 6.53706 3.23238 6.81048 2.94045C7.08389 2.64853 7.41292 2.41422 7.77821 2.25132C8.14351 2.08841 8.53769 2.0002 8.93757 1.99186C9.33745 1.98353 9.73497 2.05524 10.1067 2.20278C10.4785 2.35032 10.817 2.57072 11.1023 2.851C11.3877 3.13128 11.6141 3.46579 11.7683 3.83485C11.9224 4.20391 12.0013 4.60008 12.0001 5.00005V18.0001C11.9995 18.5468 11.8868 19.0876 11.669 19.5891C11.4512 20.0906 11.133 20.5421 10.7338 20.9158C10.3347 21.2895 9.86321 21.5774 9.34846 21.7617C8.83372 21.946 8.28665 22.0228 7.74105 21.9874C7.19545 21.952 6.6629 21.8051 6.17629 21.5558C5.68969 21.3065 5.25935 20.9601 4.91186 20.538C4.56437 20.1159 4.30711 19.627 4.15596 19.1016C4.00481 18.5761 3.96299 18.0253 4.03307 17.4831M6.00307 5.12505C5.41528 5.27619 4.86958 5.5591 4.40731 5.95236C3.94503 6.34562 3.57831 6.83892 3.33492 7.3949C3.09152 7.95087 2.97783 8.55495 3.00246 9.16136C3.02709 9.76778 3.18939 10.3606 3.47707 10.8951M6.00307 5.12505C6.02285 5.60878 6.1594 6.08055 6.40107 6.50005M3.47707 10.8951C2.97125 11.306 2.5735 11.8343 2.31841 12.434C2.06333 13.0337 1.95863 13.6866 2.01344 14.336C2.06824 14.9854 2.28089 15.6116 2.63288 16.1601C2.98487 16.7085 3.46554 17.1627 4.03307 17.4831M3.47707 10.8951C3.66001 10.7461 3.85578 10.6145 4.06207 10.5001M4.03307 17.4831C4.63334 17.8216 5.3109 18.0004 6.00007 18.0001M9.00007 13.0001C9.83963 12.7047 10.5728 12.167 11.1067 11.4551C11.6407 10.7431 11.9516 9.88872 12.0001 9.00005" stroke="url(#paint0_linear_508_48860)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_508_48860" x1="7.27748" y1="1.99121" x2="7.27748" y2="21.9958" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#888888"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
10
web/src/assets/svg/home-icon/agents.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5V18H18C18.5304 18 19.0391 18.2107 19.4142 18.5858C19.7893 18.9609 20 19.4696 20 20V21M12 13H16M12 8H20M16 8V5C16 4.46957 16.2107 3.96086 16.5858 3.58579C16.9609 3.21071 17.4696 3 18 3M16.5 13C16.5 13.2761 16.2761 13.5 16 13.5C15.7239 13.5 15.5 13.2761 15.5 13C15.5 12.7239 15.7239 12.5 16 12.5C16.2761 12.5 16.5 12.7239 16.5 13ZM18.5 3C18.5 3.27614 18.2761 3.5 18 3.5C17.7239 3.5 17.5 3.27614 17.5 3C17.5 2.72386 17.7239 2.5 18 2.5C18.2761 2.5 18.5 2.72386 18.5 3ZM20.5 21C20.5 21.2761 20.2761 21.5 20 21.5C19.7239 21.5 19.5 21.2761 19.5 21C19.5 20.7239 19.7239 20.5 20 20.5C20.2761 20.5 20.5 20.7239 20.5 21ZM20.5 8C20.5 8.27614 20.2761 8.5 20 8.5C19.7239 8.5 19.5 8.27614 19.5 8C19.5 7.72386 19.7239 7.5 20 7.5C20.2761 7.5 20.5 7.72386 20.5 8Z" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.00307 5.12505C5.98523 4.72548 6.04747 4.32637 6.18613 3.9512C6.32478 3.57604 6.53706 3.23238 6.81048 2.94045C7.08389 2.64853 7.41292 2.41422 7.77821 2.25132C8.14351 2.08841 8.53769 2.0002 8.93757 1.99186C9.33745 1.98353 9.73497 2.05524 10.1067 2.20278C10.4785 2.35032 10.817 2.57072 11.1023 2.851C11.3877 3.13128 11.6141 3.46579 11.7683 3.83485C11.9224 4.20391 12.0013 4.60008 12.0001 5.00005V18.0001C11.9995 18.5468 11.8868 19.0876 11.669 19.5891C11.4512 20.0906 11.133 20.5421 10.7338 20.9158C10.3347 21.2895 9.86321 21.5774 9.34846 21.7617C8.83372 21.946 8.28665 22.0228 7.74105 21.9874C7.19545 21.952 6.6629 21.8051 6.17629 21.5558C5.68969 21.3065 5.25935 20.9601 4.91186 20.538C4.56437 20.1159 4.30711 19.627 4.15596 19.1016C4.00481 18.5761 3.96299 18.0253 4.03307 17.4831M6.00307 5.12505C5.41528 5.27619 4.86958 5.5591 4.40731 5.95236C3.94503 6.34562 3.57831 6.83892 3.33492 7.3949C3.09152 7.95087 2.97783 8.55495 3.00246 9.16136C3.02709 9.76778 3.18939 10.3606 3.47707 10.8951M6.00307 5.12505C6.02285 5.60878 6.1594 6.08055 6.40107 6.50005M3.47707 10.8951C2.97125 11.306 2.5735 11.8343 2.31841 12.434C2.06333 13.0337 1.95863 13.6866 2.01344 14.336C2.06824 14.9854 2.28089 15.6116 2.63288 16.1601C2.98487 16.7085 3.46554 17.1627 4.03307 17.4831M3.47707 10.8951C3.66001 10.7461 3.85578 10.6145 4.06207 10.5001M4.03307 17.4831C4.63334 17.8216 5.3109 18.0004 6.00007 18.0001M9.00007 13.0001C9.83963 12.7047 10.5728 12.167 11.1067 11.4551C11.6407 10.7431 11.9516 9.88872 12.0001 9.00005" stroke="url(#paint0_linear_503_48571)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_503_48571" x1="11.2497" y1="1.99121" x2="11.2497" y2="21.9958" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
10
web/src/assets/svg/home-icon/chats-bri.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.3096 9.16504C19.8021 9.16504 20.2745 9.35961 20.6228 9.70594C20.9711 10.0523 21.1667 10.522 21.1667 11.0118V20.5095C21.1667 20.6392 21.1281 20.7659 21.0556 20.8737C20.9832 20.9815 20.8802 21.0655 20.7597 21.1151C20.6393 21.1648 20.5067 21.1777 20.3789 21.1524C20.251 21.1272 20.1335 21.0647 20.0413 20.9731L17.9966 18.9398C17.6484 18.5935 17.1761 18.3988 16.6836 18.3987H10.0239C9.53135 18.3987 9.05897 18.2041 8.71069 17.8578C8.36241 17.5115 8.16675 17.0418 8.16675 16.552V15.6286" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.1667 11.7831C18.1667 12.3546 17.941 12.9026 17.5391 13.3066C17.1373 13.7107 16.5922 13.9377 16.0239 13.9377H8.3396C7.77133 13.9378 7.22637 14.1649 6.8246 14.5689L4.46532 16.9411C4.35893 17.048 4.22339 17.1208 4.07584 17.1503C3.92829 17.1799 3.77535 17.1647 3.63636 17.1068C3.49737 17.0489 3.37857 16.9509 3.29498 16.8252C3.21139 16.6994 3.16677 16.5515 3.16675 16.4003V5.31956C3.16675 4.74815 3.39251 4.20014 3.79438 3.79608C4.19624 3.39203 4.74128 3.16504 5.30961 3.16504H16.0239C16.5922 3.16504 17.1373 3.39203 17.5391 3.79608C17.941 4.20014 18.1667 4.74815 18.1667 5.31956V11.7831Z" stroke="url(#paint0_linear_508_48870)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_508_48870" x1="11.0834" y1="3.16504" x2="11.0834" y2="17.165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#888888"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
10
web/src/assets/svg/home-icon/chats.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.3096 9.16504C19.8021 9.16504 20.2745 9.35961 20.6228 9.70594C20.9711 10.0523 21.1667 10.522 21.1667 11.0118V20.5095C21.1667 20.6392 21.1281 20.7659 21.0556 20.8737C20.9832 20.9815 20.8802 21.0655 20.7597 21.1151C20.6393 21.1648 20.5067 21.1777 20.3789 21.1524C20.251 21.1272 20.1335 21.0647 20.0413 20.9731L17.9966 18.9398C17.6484 18.5935 17.1761 18.3988 16.6836 18.3987H10.0239C9.53135 18.3987 9.05897 18.2041 8.71069 17.8578C8.36241 17.5115 8.16675 17.0418 8.16675 16.552V15.6286" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.1667 11.7831C18.1667 12.3546 17.941 12.9026 17.5391 13.3066C17.1373 13.7107 16.5922 13.9377 16.0239 13.9377H8.3396C7.77133 13.9378 7.22637 14.1649 6.8246 14.5689L4.46532 16.9411C4.35893 17.048 4.22339 17.1208 4.07584 17.1503C3.92829 17.1799 3.77535 17.1647 3.63636 17.1068C3.49737 17.0489 3.37857 16.9509 3.29498 16.8252C3.21139 16.6994 3.16677 16.5515 3.16675 16.4003V5.31956C3.16675 4.74815 3.39251 4.20014 3.79438 3.79608C4.19624 3.39203 4.74128 3.16504 5.30961 3.16504H16.0239C16.5922 3.16504 17.1373 3.39203 17.5391 3.79608C17.941 4.20014 18.1667 4.74815 18.1667 5.31956V11.7831Z" stroke="url(#paint0_linear_503_48585)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_503_48585" x1="13.881" y1="3.16504" x2="13.881" y2="24.7059" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
10
web/src/assets/svg/home-icon/datasets-bri.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0001 5C21.0001 6.65685 16.9706 8 12.0001 8C7.0295 8 3.00007 6.65685 3.00007 5M21.0001 5C21.0001 3.34315 16.9706 2 12.0001 2C7.0295 2 3.00007 3.34315 3.00007 5M21.0001 5V8M3.00007 5L3.00007 19C2.9945 19.4809 3.33587 19.9552 3.9954 20.383C4.65493 20.8107 5.61329 21.1793 6.7897 21.4577C7.96611 21.7361 9.32608 21.9162 10.755 21.9827C12.1839 22.0493 13.6398 22.0003 15.0001 21.84M3.00007 12C3.00151 12.4675 3.33075 12.9285 3.96152 13.3461C4.59229 13.7636 5.50712 14.1263 6.63303 14.4051C7.75894 14.6839 9.06475 14.8711 10.4463 14.9519C11.8278 15.0326 13.2468 15.0045 14.5901 14.87" stroke="url(#paint0_linear_508_48865)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 12L18 17H22L19 22" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_508_48865" x1="12.5" y1="2" x2="12.5" y2="22.0116" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#888888"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
10
web/src/assets/svg/home-icon/datasets.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0001 5C21.0001 6.65685 16.9706 8 12.0001 8C7.0295 8 3.00007 6.65685 3.00007 5M21.0001 5C21.0001 3.34315 16.9706 2 12.0001 2C7.0295 2 3.00007 3.34315 3.00007 5M21.0001 5V8M3.00007 5L3.00007 19C2.9945 19.4809 3.33587 19.9552 3.9954 20.383C4.65493 20.8107 5.61329 21.1793 6.7897 21.4577C7.96611 21.7361 9.32608 21.9162 10.755 21.9827C12.1839 22.0493 13.6398 22.0003 15.0001 21.84M3.00007 12C3.00151 12.4675 3.33075 12.9285 3.96152 13.3461C4.59229 13.7636 5.50712 14.1263 6.63303 14.4051C7.75894 14.6839 9.06475 14.8711 10.4463 14.9519C11.8278 15.0326 13.2468 15.0045 14.5901 14.87" stroke="url(#paint0_linear_510_2111)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 12L18 17H22L19 22" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_510_2111" x1="12.5" y1="2" x2="12.5" y2="22.0116" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
10
web/src/assets/svg/home-icon/file-bri.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 14L7.45 11.1C7.61696 10.7687 7.87281 10.4903 8.18893 10.296C8.50504 10.1018 8.86897 9.99927 9.24 10H20M20 10C20.3055 9.99946 20.6071 10.0689 20.8816 10.2031C21.1561 10.3372 21.3963 10.5325 21.5836 10.7739C21.7709 11.0152 21.9004 11.2963 21.9622 11.5956C22.024 11.8948 22.0164 12.2042 21.94 12.5L20.39 18.5C20.279 18.9299 20.0281 19.3106 19.6769 19.5822C19.3256 19.8538 18.894 20.0008 18.45 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V5C2 3.9 2.9 3 4 3H7.93C8.25941 3.0017 8.58331 3.08475 8.8729 3.24176C9.1625 3.39877 9.40882 3.62488 9.59 3.9L10.41 5.1C10.5912 5.37512 10.8375 5.60123 11.1271 5.75824C11.4167 5.91525 11.7406 5.9983 12.07 6H18C18.5304 6 19.0391 6.21071 19.4142 6.58579C19.7893 6.96086 20 7.46957 20 8V10Z" stroke="url(#paint0_linear_508_49090)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 16C12.5523 16 13 15.5523 13 15C13 14.4477 12.5523 14 12 14C11.4477 14 11 14.4477 11 15C11 15.5523 11.4477 16 12 16Z" stroke="#00BEB4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_508_49090" x1="12.5574" y1="3" x2="12.5574" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#888888"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
web/src/assets/svg/home-icon/file.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 14L7.45 11.1C7.61696 10.7687 7.87281 10.4903 8.18893 10.296C8.50504 10.1018 8.86897 9.99927 9.24 10H20M20 10C20.3055 9.99946 20.6071 10.0689 20.8816 10.2031C21.1561 10.3372 21.3963 10.5325 21.5836 10.7739C21.7709 11.0152 21.9004 11.2963 21.9622 11.5956C22.024 11.8948 22.0164 12.2042 21.94 12.5L20.39 18.5C20.279 18.9299 20.0281 19.3106 19.6769 19.5822C19.3256 19.8538 18.894 20.0008 18.45 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V5C2 3.9 2.9 3 4 3H7.93C8.25941 3.0017 8.58331 3.08475 8.8729 3.24176C9.1625 3.39877 9.40882 3.62488 9.59 3.9L10.41 5.1C10.5912 5.37512 10.8375 5.60123 11.1271 5.75824C11.4167 5.91525 11.7406 5.9983 12.07 6H18C18.5304 6 19.0391 6.21071 19.4142 6.58579C19.7893 6.96086 20 7.46957 20 8V10Z" stroke="url(#paint0_linear_508_49095)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 16C12.5523 16 13 15.5523 13 15C13 14.4477 12.5523 14 12 14C11.4477 14 11 14.4477 11 15C11 15.5523 11.4477 16 12 16Z" stroke="#00BEB4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_508_49095" x1="12.5574" y1="3" x2="12.5574" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
web/src/assets/svg/home-icon/searches-bri.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.9999 21.0002L16.6599 16.6602" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19Z" stroke="url(#paint0_linear_508_48855)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_508_48855" x1="11.4444" y1="3" x2="11.4444" y2="19" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#161618"/>
|
||||
<stop offset="1" stop-color="#888888"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
10
web/src/assets/svg/home-icon/searches.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.9999 21.0002L16.6599 16.6602" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19Z" stroke="url(#paint0_linear_503_48578)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_503_48578" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 670 B |
46
web/src/components/card-singleline-container/index.less
Normal file
@ -0,0 +1,46 @@
|
||||
.single :nth-child(n + 2):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 1):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.single :nth-child(n + 2):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 1):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.single :nth-child(n + 3):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 2):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.single :nth-child(n + 4):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 3):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.single :nth-child(n + 5):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 4):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
.single :nth-child(n + 6):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 5):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
41
web/src/components/card-singleline-container/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isValidElement, PropsWithChildren, ReactNode } from 'react';
|
||||
import './index.less';
|
||||
|
||||
type CardContainerProps = { className?: string } & PropsWithChildren;
|
||||
|
||||
export function CardSineLineContainer({
|
||||
children,
|
||||
className,
|
||||
}: CardContainerProps) {
|
||||
const flattenChildren = (children: ReactNode): ReactNode[] => {
|
||||
const result: ReactNode[] = [];
|
||||
|
||||
const traverse = (child: ReactNode) => {
|
||||
if (Array.isArray(child)) {
|
||||
child.forEach(traverse);
|
||||
} else if (isValidElement(child) && child.props.children) {
|
||||
result.push(child);
|
||||
} else {
|
||||
result.push(child);
|
||||
}
|
||||
};
|
||||
|
||||
traverse(children);
|
||||
return result;
|
||||
};
|
||||
const childArray = flattenChildren(children);
|
||||
const childCount = childArray.length;
|
||||
console.log(childArray, childCount);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'grid gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 single',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -74,7 +74,7 @@ export function Collapse({
|
||||
<div>{rightContent}</div>
|
||||
</section>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">{children}</CollapsibleContent>
|
||||
<CollapsibleContent className="pt-5">{children}</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useControllableState } from '@/hooks/use-controllable-state';
|
||||
import { cn, formatBytes } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -53,11 +52,13 @@ function FilePreview({ file }: FilePreviewProps) {
|
||||
function FileCard({ file, progress, onRemove }: FileCardProps) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-2.5">
|
||||
<div className="flex flex-1 gap-2.5">
|
||||
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex flex-1 gap-2.5 overflow-hidden">
|
||||
<div className="w-8">
|
||||
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 gap-2 overflow-hidden">
|
||||
<div className="flex flex-col gap-px">
|
||||
<p className="line-clamp-1 text-sm font-medium text-foreground/80">
|
||||
<p className="line-clamp-1 text-sm font-medium text-foreground/80 text-ellipsis">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@ -319,8 +320,8 @@ export function FileUploader(props: FileUploaderProps) {
|
||||
)}
|
||||
</Dropzone>
|
||||
{files?.length ? (
|
||||
<ScrollArea className="h-fit w-full px-3">
|
||||
<div className="flex max-h-48 flex-col gap-4">
|
||||
<div className="h-fit w-full px-3">
|
||||
<div className="flex max-h-48 flex-col gap-4 overflow-auto scrollbar-auto">
|
||||
{files?.map((file, index) => (
|
||||
<FileCard
|
||||
key={index}
|
||||
@ -330,7 +331,7 @@ export function FileUploader(props: FileUploaderProps) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -29,7 +29,7 @@ export function HomeCard({
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4 flex gap-2 items-start group h-full">
|
||||
<CardContent className="p-4 flex gap-2 items-start group h-full w-full">
|
||||
<div className="flex justify-between mb-4">
|
||||
<RAGFlowAvatar
|
||||
className="w-[32px] h-[32px]"
|
||||
|
||||
@ -70,7 +70,7 @@ export function LargeModelFormField({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant={'ghost'}>
|
||||
<Funnel />
|
||||
<Funnel className="text-text-disabled" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
|
||||
@ -19,6 +19,7 @@ export const enum ParseDocumentType {
|
||||
PlainText = 'Plain Text',
|
||||
MinerU = 'MinerU',
|
||||
Docling = 'Docling',
|
||||
TCADPParser = 'TCADP Parser',
|
||||
}
|
||||
|
||||
export function LayoutRecognizeFormField({
|
||||
@ -45,6 +46,7 @@ export function LayoutRecognizeFormField({
|
||||
ParseDocumentType.PlainText,
|
||||
ParseDocumentType.MinerU,
|
||||
ParseDocumentType.Docling,
|
||||
ParseDocumentType.TCADPParser,
|
||||
].map((x) => ({
|
||||
label: x === ParseDocumentType.PlainText ? t(camelCase(x)) : x,
|
||||
value: x,
|
||||
|
||||
@ -6,7 +6,7 @@ import React, {
|
||||
ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { IconFont } from '../icon-font';
|
||||
import { HomeIcon } from '../svg-icon';
|
||||
import { Button, ButtonProps } from '../ui/button';
|
||||
import { SearchInput } from '../ui/input';
|
||||
import { CheckboxFormMultipleProps, FilterPopover } from './filter-popover';
|
||||
@ -72,7 +72,8 @@ export default function ListFilterBar({
|
||||
<div className={cn('flex justify-between mb-5 items-center', className)}>
|
||||
<div className="text-2xl font-semibold flex items-center gap-2.5">
|
||||
{typeof icon === 'string' ? (
|
||||
<IconFont name={icon} className="size-6"></IconFont>
|
||||
// <IconFont name={icon} className="size-6"></IconFont>
|
||||
<HomeIcon name={`${icon}`} width={'32'} />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
|
||||
@ -140,7 +140,7 @@ export const SelectWithSearch = forwardRef<
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto',
|
||||
'!bg-bg-input hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto',
|
||||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -7,10 +7,10 @@ type SkeletonCardProps = {
|
||||
export function SkeletonCard(props: SkeletonCardProps) {
|
||||
const { className } = props;
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<Skeleton className="h-4 w-full bg-bg-card" />
|
||||
<Skeleton className="h-4 w-full bg-bg-card" />
|
||||
<Skeleton className="h-4 w-2/3 bg-bg-card" />
|
||||
<div className={cn('space-y-4', className)}>
|
||||
<Skeleton className="h-8 w-full bg-bg-card rounded-lg" />
|
||||
<Skeleton className="h-8 w-4/5 bg-bg-card rounded-lg" />
|
||||
<Skeleton className="h-8 w-3/5 bg-bg-card rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import Icon, { UserOutlined } from '@ant-design/icons';
|
||||
import { IconComponentProps } from '@ant-design/icons/lib/components/Icon';
|
||||
import { Avatar } from 'antd';
|
||||
import { AvatarSize } from 'antd/es/avatar/AvatarContext';
|
||||
import { useIsDarkTheme } from './theme-provider';
|
||||
|
||||
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => {
|
||||
const list = requireContext.keys().map((key) => {
|
||||
@ -74,4 +75,32 @@ export const LlmIcon = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const HomeIcon = ({
|
||||
name,
|
||||
height = '32',
|
||||
width = '32',
|
||||
size = 'large',
|
||||
imgClass,
|
||||
}: {
|
||||
name: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
size?: AvatarSize;
|
||||
imgClass?: string;
|
||||
}) => {
|
||||
const isDark = useIsDarkTheme();
|
||||
const icon = isDark ? name : `${name}-bri`;
|
||||
|
||||
return icon ? (
|
||||
<SvgIcon
|
||||
name={`home-icon/${icon}`}
|
||||
width={width}
|
||||
height={height}
|
||||
imgClass={imgClass}
|
||||
></SvgIcon>
|
||||
) : (
|
||||
<Avatar shape="square" size={size} icon={<UserOutlined />} />
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgIcon;
|
||||
|
||||
@ -31,7 +31,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-8 w-full rounded-md border border-input bg-bg-base px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-8 w-full rounded-md border border-input bg-bg-input px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-text-primary',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@ -13,6 +13,33 @@ export interface SegmentedLabeledOption {
|
||||
title?: string;
|
||||
}
|
||||
declare type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[];
|
||||
const segmentedVariants = {
|
||||
round: {
|
||||
default: 'rounded-md',
|
||||
none: 'rounded-none',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl',
|
||||
xxl: 'rounded-2xl',
|
||||
xxxl: 'rounded-3xl',
|
||||
full: 'rounded-full',
|
||||
},
|
||||
size: {
|
||||
default: 'px-1 py-1',
|
||||
sm: 'px-1 py-1',
|
||||
md: 'px-2 py-1.5',
|
||||
lg: 'px-4 px-2',
|
||||
xl: 'px-5 py-2.5',
|
||||
xxl: 'px-6 py-3',
|
||||
},
|
||||
buttonSize: {
|
||||
default: 'px-2 py-1',
|
||||
md: 'px-2 py-1',
|
||||
lg: 'px-4 px-1.5',
|
||||
xl: 'px-6 py-2',
|
||||
},
|
||||
};
|
||||
export interface SegmentedProps
|
||||
extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange'> {
|
||||
options: SegmentedOptions;
|
||||
@ -24,6 +51,9 @@ export interface SegmentedProps
|
||||
direction?: 'ltr' | 'rtl';
|
||||
motionName?: string;
|
||||
activeClassName?: string;
|
||||
rounded?: keyof typeof segmentedVariants.round;
|
||||
sizeType?: keyof typeof segmentedVariants.size;
|
||||
buttonSize?: keyof typeof segmentedVariants.buttonSize;
|
||||
}
|
||||
|
||||
export function Segmented({
|
||||
@ -32,6 +62,9 @@ export function Segmented({
|
||||
onChange,
|
||||
className,
|
||||
activeClassName,
|
||||
rounded = 'default',
|
||||
sizeType = 'default',
|
||||
buttonSize = 'default',
|
||||
}: SegmentedProps) {
|
||||
const [selectedValue, setSelectedValue] = React.useState<
|
||||
SegmentedValue | undefined
|
||||
@ -45,7 +78,9 @@ export function Segmented({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center rounded-3xl p-1 gap-2 bg-bg-card px-5 py-2.5',
|
||||
'flex items-center p-1 gap-2 bg-bg-card',
|
||||
segmentedVariants.round[rounded],
|
||||
segmentedVariants.size[sizeType],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@ -57,10 +92,11 @@ export function Segmented({
|
||||
<div
|
||||
key={actualValue}
|
||||
className={cn(
|
||||
'inline-flex items-center px-6 py-2 text-base font-normal rounded-3xl cursor-pointer',
|
||||
'inline-flex items-center text-base font-normal cursor-pointer',
|
||||
segmentedVariants.round[rounded],
|
||||
segmentedVariants.buttonSize[buttonSize],
|
||||
{
|
||||
'text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2':
|
||||
selectedValue === actualValue,
|
||||
'text-text-primary bg-bg-base': selectedValue === actualValue,
|
||||
},
|
||||
activeClassName && selectedValue === actualValue
|
||||
? activeClassName
|
||||
|
||||
@ -26,7 +26,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-8 w-full items-center bg-bg-card justify-between rounded-md border border-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
'flex h-8 w-full items-center bg-bg-input justify-between rounded-md border border-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -291,7 +291,7 @@ export const RAGFlowSelect = forwardRef<
|
||||
onReset={handleReset}
|
||||
allowClear={allowClear}
|
||||
ref={ref}
|
||||
className={cn('bg-bg-base', triggerClassName)}
|
||||
className={triggerClassName}
|
||||
>
|
||||
<SelectValue placeholder={placeholder}>{label}</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
@ -18,7 +18,7 @@ const Separator = React.forwardRef<
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
'shrink-0 bg-border-button',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-text-title-invert rounded-lg p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
'fixed z-50 gap-4 bg-bg-base rounded-lg p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
@ -73,7 +73,7 @@ const SheetContent = React.forwardRef<
|
||||
{children}
|
||||
{closeIcon ? (
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary ">
|
||||
<X className="h-4 w-4 " />
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
) : null}
|
||||
|
||||
@ -116,9 +116,13 @@ export function Header() {
|
||||
/>
|
||||
</div>
|
||||
<Segmented
|
||||
rounded="xxxl"
|
||||
sizeType="xl"
|
||||
buttonSize="xl"
|
||||
options={options}
|
||||
value={pathname}
|
||||
onChange={handleChange}
|
||||
activeClassName="text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2"
|
||||
></Segmented>
|
||||
<div className="flex items-center gap-5 text-text-badge">
|
||||
<a
|
||||
|
||||
@ -352,7 +352,7 @@ export default {
|
||||
manual: `<p>Only <b>PDF</b> is supported.</p><p>
|
||||
We assume that the manual has a hierarchical section structure, using the lowest section titles as basic unit for chunking documents. Therefore, figures and tables in the same section will not be separated, which may result in larger chunk sizes.
|
||||
</p>`,
|
||||
naive: `<p>Supported file formats are <b>MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPT, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML</b>.</p>
|
||||
naive: `<p>Supported file formats are <b>MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPTX, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML</b>.</p>
|
||||
<p>This method chunks files using a 'naive' method: </p>
|
||||
<p>
|
||||
<li>Use vision detection model to split the texts into smaller segments.</li>
|
||||
@ -710,7 +710,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
timezone: 'Time zone',
|
||||
timezoneMessage: 'Please input your timezone!',
|
||||
timezonePlaceholder: 'select your timezone',
|
||||
email: 'Email address',
|
||||
email: 'Email',
|
||||
emailDescription: 'Once registered, E-mail cannot be changed.',
|
||||
currentPassword: 'Current password',
|
||||
currentPasswordMessage: 'Please input your password!',
|
||||
@ -865,9 +865,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
apiVersion: 'API-Version',
|
||||
apiVersionMessage: 'Please input API version',
|
||||
add: 'Add',
|
||||
updateDate: 'Update Date',
|
||||
role: 'Role',
|
||||
invite: 'Invite',
|
||||
updateDate: 'Date',
|
||||
role: 'State',
|
||||
invite: 'Invite Member',
|
||||
agree: 'Accept',
|
||||
refuse: 'Decline',
|
||||
teamMembers: 'Team Members',
|
||||
@ -1631,7 +1631,7 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
email: 'Email',
|
||||
'text&markdown': 'Text & Markup',
|
||||
word: 'Word',
|
||||
slides: 'PPT',
|
||||
slides: 'PPTX',
|
||||
audio: 'Audio',
|
||||
video: 'Video',
|
||||
},
|
||||
@ -1803,13 +1803,13 @@ Important structured information may include: names, dates, locations, events, k
|
||||
confirmRerun: 'Confirm Rerun Process',
|
||||
confirmRerunModalContent: `
|
||||
<p class="text-sm text-text-disabled font-medium mb-2">
|
||||
You are about to rerun the process starting from the <strong class="text-text-primary">{{step}}</strong> step.
|
||||
You are about to rerun the process starting from the <span class="text-text-secondary">{{step}}</span> step.
|
||||
</p>
|
||||
<p class="text-sm mb-3 text-text-secondary">This will:</p>
|
||||
<p class="text-sm mb-3 text-text-disabled">This will:</p><br />
|
||||
<ul class="list-disc list-inside space-y-1 text-sm text-text-secondary">
|
||||
<li>Overwrite existing results from the current step onwards</li>
|
||||
<li>Create a new log entry for tracking</li>
|
||||
<li>Previous steps will remain unchanged</li>
|
||||
<li>• Overwrite existing results from the current step onwards</li>
|
||||
<li>• Create a new log entry for tracking</li>
|
||||
<li>• Previous steps will remain unchanged</li>
|
||||
</ul>`,
|
||||
changeStepModalTitle: 'Step Switch Warning',
|
||||
changeStepModalContent: `
|
||||
|
||||
@ -336,7 +336,7 @@ export default {
|
||||
我们假设手册具有分层部分结构。 我们使用最低的部分标题作为对文档进行切片的枢轴。
|
||||
因此,同一部分中的图和表不会被分割,并且块大小可能会很大。
|
||||
</p>`,
|
||||
naive: `<p>支持的文件格式为<b>MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPT、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML</b>。</p>
|
||||
naive: `<p>支持的文件格式为<b>MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPTX、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML</b>。</p>
|
||||
<p>此方法将简单的方法应用于块文件:</p>
|
||||
<p>
|
||||
<li>系统将使用视觉检测模型将连续文本分割成多个片段。</li>
|
||||
@ -701,7 +701,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
timezone: '时区',
|
||||
timezoneMessage: '请选择时区',
|
||||
timezonePlaceholder: '请选择时区',
|
||||
email: '邮箱地址',
|
||||
email: '邮箱',
|
||||
emailDescription: '一旦注册,电子邮件将无法更改。',
|
||||
currentPassword: '当前密码',
|
||||
currentPasswordMessage: '请输入当前密码',
|
||||
@ -821,9 +821,9 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
apiVersion: 'API版本',
|
||||
apiVersionMessage: '请输入API版本!',
|
||||
add: '添加',
|
||||
updateDate: '更新日期',
|
||||
role: '角色',
|
||||
invite: '邀请',
|
||||
updateDate: '日期',
|
||||
role: '状态',
|
||||
invite: '邀请成员',
|
||||
agree: '同意',
|
||||
refuse: '拒绝',
|
||||
teamMembers: '团队成员',
|
||||
@ -1690,13 +1690,13 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
confirmRerun: '确认重新运行流程',
|
||||
confirmRerunModalContent: `
|
||||
<p class="text-sm text-text-disabled font-medium mb-2">
|
||||
您即将从 <strong class="text-text-primary">{{step}}</strong> 步骤开始重新运行该过程
|
||||
您即将从 <span class="text-text-secondary">{{step}}</span> 步骤开始重新运行该过程
|
||||
</p>
|
||||
<p class="text-sm mb-3 text-text-secondary">这将:</p>
|
||||
<p class="text-sm mb-3 text-text-disabled">这将:</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm text-text-secondary">
|
||||
<li>从当前步骤开始覆盖现有结果</li>
|
||||
<li>创建新的日志条目进行跟踪</li>
|
||||
<li>之前的步骤将保持不变</li>
|
||||
<li>• 从当前步骤开始覆盖现有结果</li>
|
||||
<li>• 创建新的日志条目进行跟踪</li>
|
||||
<li>• 之前的步骤将保持不变</li>
|
||||
</ul>`,
|
||||
changeStepModalTitle: '切换步骤警告',
|
||||
changeStepModalContent: `
|
||||
|
||||
@ -93,7 +93,9 @@ function InnerButtonEdge({
|
||||
}, [data?.isHovered, isTargetPlaceholder, sourceHandleId, target]);
|
||||
|
||||
const activeMarkerEnd =
|
||||
selected || !isEmpty(showHighlight) ? 'url(#selected-marker)' : markerEnd;
|
||||
selected || !isEmpty(showHighlight) || isTargetPlaceholder
|
||||
? 'url(#selected-marker)'
|
||||
: markerEnd;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -311,7 +311,11 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
>
|
||||
<AgentBackground></AgentBackground>
|
||||
<Spotlight className="z-0" opcity={0.7} coverage={70} />
|
||||
<Controls position={'bottom-center'} orientation="horizontal">
|
||||
<Controls
|
||||
position={'bottom-center'}
|
||||
orientation="horizontal"
|
||||
className="bg-bg-base px-4 py-2 h-auto w-auto [&>button]:bg-transparent [&>button]:border-0 [&>button]:text-text-primary [&>button]:hover:bg-bg-base-hover [&>button]:hover:text-text-primary [&>button]:active:bg-bg-base-active [&>button]:p-0 [&>button]:size-4 gap-2.5 rounded-md"
|
||||
>
|
||||
<ControlButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@ -6,10 +6,18 @@ import {
|
||||
} from '@/components/ui/accordion';
|
||||
import { Operator } from '@/constants/agent';
|
||||
import useGraphStore from '@/pages/agent/store';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { PropsWithChildren, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { OperatorItemList } from './operator-item-list';
|
||||
|
||||
function OperatorAccordionTrigger({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<AccordionTrigger className="text-xs text-text-secondary hover:no-underline items-center">
|
||||
<span className="h-4 translate-y-1"> {children}</span>
|
||||
</AccordionTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccordionOperators({
|
||||
isCustomDropdown = false,
|
||||
mousePosition,
|
||||
@ -26,10 +34,10 @@ export function AccordionOperators({
|
||||
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
|
||||
>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-xl">
|
||||
<OperatorAccordionTrigger>
|
||||
{t('flow.foundation')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
</OperatorAccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-text-primary">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Agent, Operator.Retrieval]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
@ -38,10 +46,8 @@ export function AccordionOperators({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.dialog')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorAccordionTrigger>{t('flow.dialog')}</OperatorAccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-text-primary">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Message, Operator.UserFillUp]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
@ -50,10 +56,8 @@ export function AccordionOperators({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.flow')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorAccordionTrigger>{t('flow.flow')}</OperatorAccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-text-primary">
|
||||
<OperatorItemList
|
||||
operators={[
|
||||
Operator.Switch,
|
||||
@ -66,10 +70,10 @@ export function AccordionOperators({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger className="text-xl">
|
||||
<OperatorAccordionTrigger>
|
||||
{t('flow.dataManipulation')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
</OperatorAccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-text-primary">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Code, Operator.StringTransform]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
@ -78,10 +82,8 @@ export function AccordionOperators({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.tools')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorAccordionTrigger>{t('flow.tools')}</OperatorAccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-text-primary">
|
||||
<OperatorItemList
|
||||
operators={[
|
||||
Operator.TavilySearch,
|
||||
@ -180,8 +182,10 @@ export function PipelineAccordionOperators({
|
||||
defaultValue="item-1"
|
||||
>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Chunker</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<AccordionTrigger className="translate-y-2 hover:no-underline text-text-primary font-normal">
|
||||
Chunker
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4">
|
||||
<OperatorItemList
|
||||
operators={chunkerOperators}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
|
||||
@ -96,5 +96,9 @@ export function OperatorItemList({
|
||||
);
|
||||
};
|
||||
|
||||
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
|
||||
return (
|
||||
<ul className="space-y-2 text-text-primary font-normal">
|
||||
{operators.map(renderOperatorItem)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@ -10,17 +9,17 @@ import { IModalProps } from '@/interfaces/common';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { lowerFirst } from 'lodash';
|
||||
import { Play, X } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { BeginId, Operator } from '../constant';
|
||||
import { CirclePlay, X } from 'lucide-react';
|
||||
import { Operator } from '../constant';
|
||||
import { AgentFormContext } from '../context';
|
||||
import { RunTooltip } from '../flow-tooltip';
|
||||
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
|
||||
import { useIsMcp } from '../hooks/use-is-mcp';
|
||||
import OperatorIcon from '../operator-icon';
|
||||
import useGraphStore from '../store';
|
||||
import { needsSingleStepDebugging } from '../utils';
|
||||
import { FormConfigMap } from './form-config-map';
|
||||
import SingleDebugSheet from './single-debug-sheet';
|
||||
import { TitleInput } from './title-input';
|
||||
|
||||
interface IProps {
|
||||
node?: RAGFlowNodeType;
|
||||
@ -48,17 +47,7 @@ const FormSheet = ({
|
||||
|
||||
const OperatorForm = currentFormMap?.component ?? EmptyContent;
|
||||
|
||||
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
|
||||
id: node?.id,
|
||||
data: node?.data,
|
||||
});
|
||||
|
||||
const isMcp = useMemo(() => {
|
||||
return (
|
||||
operatorName === Operator.Tool &&
|
||||
Object.values(Operator).every((x) => x !== clickedToolId)
|
||||
);
|
||||
}, [clickedToolId, operatorName]);
|
||||
const isMcp = useIsMcp(operatorName);
|
||||
|
||||
const { t } = useTranslate('flow');
|
||||
|
||||
@ -75,36 +64,19 @@ const FormSheet = ({
|
||||
<section className="flex-col border-b py-2 px-5">
|
||||
<div className="flex items-center gap-2 pb-3">
|
||||
<OperatorIcon name={operatorName}></OperatorIcon>
|
||||
|
||||
{isMcp ? (
|
||||
<div className="flex-1">MCP Config</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
<label htmlFor="">{t('title')}</label>
|
||||
{node?.id === BeginId ? (
|
||||
<span>{t(BeginId)}</span>
|
||||
) : (
|
||||
<Input
|
||||
value={name}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TitleInput node={node}></TitleInput>
|
||||
{needsSingleStepDebugging(operatorName) && (
|
||||
<RunTooltip>
|
||||
<Play
|
||||
className="size-5 cursor-pointer"
|
||||
<CirclePlay
|
||||
className="size-3.5 cursor-pointer"
|
||||
onClick={showSingleDebugDrawer}
|
||||
/>
|
||||
</RunTooltip>
|
||||
)}
|
||||
<X onClick={hideModal} />
|
||||
<X onClick={hideModal} className="size-3.5 cursor-pointer" />
|
||||
</div>
|
||||
{isMcp || (
|
||||
<span>
|
||||
<span className="text-text-secondary">
|
||||
{t(
|
||||
`${lowerFirst(operatorName === Operator.Tool ? clickedToolId : operatorName)}Description`,
|
||||
)}
|
||||
|
||||
61
web/src/pages/agent/form-sheet/title-input.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import { PenLine } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BeginId, Operator } from '../constant';
|
||||
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
|
||||
import { useIsMcp } from '../hooks/use-is-mcp';
|
||||
|
||||
type TitleInputProps = {
|
||||
node?: RAGFlowNodeType;
|
||||
};
|
||||
|
||||
export function TitleInput({ node }: TitleInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
|
||||
id: node?.id,
|
||||
data: node?.data,
|
||||
});
|
||||
|
||||
const operatorName: Operator = node?.data.label as Operator;
|
||||
|
||||
const isMcp = useIsMcp(operatorName);
|
||||
|
||||
const [isEditingMode, setIsEditingMode] = useState(false);
|
||||
|
||||
const switchIsEditingMode = useCallback(() => {
|
||||
setIsEditingMode((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
handleNameBlur();
|
||||
setIsEditingMode(false);
|
||||
}, [handleNameBlur]);
|
||||
|
||||
if (isMcp) {
|
||||
return <div className="flex-1 text-base">MCP Config</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
{node?.id === BeginId ? (
|
||||
<span>{t(BeginId)}</span>
|
||||
) : isEditingMode ? (
|
||||
<Input
|
||||
value={name}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
) : (
|
||||
<div className="flex items-center gap-2.5 text-base">
|
||||
{name}
|
||||
<PenLine
|
||||
onClick={switchIsEditingMode}
|
||||
className="size-3.5 text-text-secondary cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -28,18 +28,31 @@ import { useDeleteAgentNodeMCP } from './tool-popover/use-update-mcp';
|
||||
import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools';
|
||||
import { useGetAgentMCPIds, useGetAgentToolNames } from './use-get-tools';
|
||||
|
||||
type ToolCardProps = React.HTMLAttributes<HTMLLIElement> &
|
||||
PropsWithChildren & {
|
||||
isNodeTool?: boolean;
|
||||
};
|
||||
|
||||
export function ToolCard({
|
||||
children,
|
||||
className,
|
||||
isNodeTool = true,
|
||||
...props
|
||||
}: PropsWithChildren & React.HTMLAttributes<HTMLLIElement>) {
|
||||
}: ToolCardProps) {
|
||||
const element = useMemo(() => {
|
||||
return (
|
||||
<LabelCard {...props} className={cn('flex justify-between', className)}>
|
||||
<LabelCard
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex justify-between ',
|
||||
{ 'p-2.5 text-text-primary text-sm': !isNodeTool },
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</LabelCard>
|
||||
);
|
||||
}, [children, className, props]);
|
||||
}, [children, className, isNodeTool, props]);
|
||||
|
||||
if (children === Operator.Code) {
|
||||
return (
|
||||
@ -67,13 +80,13 @@ function ActionButton<T>({ deleteRecord, record, edit }: ActionButtonProps<T>) {
|
||||
}, [deleteRecord, record]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-text-secondary">
|
||||
<div className="flex items-center gap-4 text-text-secondary">
|
||||
<PencilLine
|
||||
className="size-4 cursor-pointer"
|
||||
className="size-3.5 cursor-pointer"
|
||||
data-tool={record}
|
||||
onClick={edit}
|
||||
/>
|
||||
<X className="size-4 cursor-pointer" onClick={handleDelete} />
|
||||
<X className="size-3.5 cursor-pointer" onClick={handleDelete} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -102,10 +115,10 @@ export function AgentTools() {
|
||||
|
||||
return (
|
||||
<section className="space-y-2.5">
|
||||
<span className="text-text-secondary">{t('flow.tools')}</span>
|
||||
<ul className="space-y-2">
|
||||
<span className="text-text-secondary text-sm">{t('flow.tools')}</span>
|
||||
<ul className="space-y-2.5">
|
||||
{toolNames.map((x) => (
|
||||
<ToolCard key={x}>
|
||||
<ToolCard key={x} isNodeTool={false}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<OperatorIcon name={x as Operator}></OperatorIcon>
|
||||
{x}
|
||||
@ -118,7 +131,7 @@ export function AgentTools() {
|
||||
</ToolCard>
|
||||
))}
|
||||
{mcpIds.map((id) => (
|
||||
<ToolCard key={id}>
|
||||
<ToolCard key={id} isNodeTool={false}>
|
||||
{findMcpById(id)?.name}
|
||||
<ActionButton
|
||||
record={id}
|
||||
@ -156,13 +169,13 @@ export function Agents({ node }: INextOperatorForm) {
|
||||
|
||||
return (
|
||||
<section className="space-y-2.5">
|
||||
<span className="text-text-secondary">{t('flow.agent')}</span>
|
||||
<ul className="space-y-2">
|
||||
<span className="text-text-secondary text-sm">{t('flow.agent')}</span>
|
||||
<ul className="space-y-2.5">
|
||||
{subBottomAgentNodeIds.map((id) => {
|
||||
const currentNode = getNode(id);
|
||||
|
||||
return (
|
||||
<ToolCard key={id}>
|
||||
<ToolCard key={id} isNodeTool={false}>
|
||||
{currentNode?.data.name}
|
||||
<ActionButton
|
||||
record={id}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import {
|
||||
LargeModelFilterFormSchema,
|
||||
LargeModelFormField,
|
||||
@ -15,6 +14,7 @@ import {
|
||||
FormLabel,
|
||||
} from '@/components/ui/form';
|
||||
import { Input, NumberInput } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
|
||||
@ -124,66 +124,53 @@ function AgentForm({ node }: INextOperatorForm) {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<FormContainer>
|
||||
{isSubAgent && <DescriptionField></DescriptionField>}
|
||||
<LargeModelFormField showSpeech2TextModel></LargeModelFormField>
|
||||
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
|
||||
<QueryVariable
|
||||
name="visual_files_var"
|
||||
label="Visual Input File"
|
||||
type={VariableType.File}
|
||||
></QueryVariable>
|
||||
{isSubAgent && <DescriptionField></DescriptionField>}
|
||||
<LargeModelFormField showSpeech2TextModel></LargeModelFormField>
|
||||
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
|
||||
<QueryVariable
|
||||
name="visual_files_var"
|
||||
label="Visual Input File"
|
||||
type={VariableType.File}
|
||||
></QueryVariable>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`sys_prompt`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>{t('flow.systemPrompt')}</FormLabel>
|
||||
<FormControl>
|
||||
<PromptEditor
|
||||
{...field}
|
||||
placeholder={t('flow.messagePlaceholder')}
|
||||
showToolbar={true}
|
||||
extraOptions={extraOptions}
|
||||
></PromptEditor>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
</FormContainer>
|
||||
|
||||
<FormContainer>
|
||||
/>
|
||||
{isSubAgent || (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`sys_prompt`}
|
||||
name={`prompts`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>{t('flow.systemPrompt')}</FormLabel>
|
||||
<FormLabel>{t('flow.userPrompt')}</FormLabel>
|
||||
<FormControl>
|
||||
<PromptEditor
|
||||
{...field}
|
||||
placeholder={t('flow.messagePlaceholder')}
|
||||
showToolbar={true}
|
||||
extraOptions={extraOptions}
|
||||
></PromptEditor>
|
||||
<section>
|
||||
<PromptEditor {...field} showToolbar={true}></PromptEditor>
|
||||
</section>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</FormContainer>
|
||||
{isSubAgent || (
|
||||
<FormContainer>
|
||||
{/* <DynamicPrompt></DynamicPrompt> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`prompts`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>{t('flow.userPrompt')}</FormLabel>
|
||||
<FormControl>
|
||||
<section>
|
||||
<PromptEditor
|
||||
{...field}
|
||||
showToolbar={true}
|
||||
></PromptEditor>
|
||||
</section>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</FormContainer>
|
||||
)}
|
||||
|
||||
<FormContainer>
|
||||
<AgentTools></AgentTools>
|
||||
<Agents node={node}></Agents>
|
||||
</FormContainer>
|
||||
<Separator></Separator>
|
||||
<AgentTools></AgentTools>
|
||||
<Agents node={node}></Agents>
|
||||
<Collapse title={<div>{t('flow.advancedSettings')}</div>}>
|
||||
<FormContainer>
|
||||
<section className="space-y-5">
|
||||
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@ -270,7 +257,7 @@ function AgentForm({ node }: INextOperatorForm) {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormContainer>
|
||||
</section>
|
||||
</Collapse>
|
||||
<Output list={outputList}></Output>
|
||||
</FormWrapper>
|
||||
|
||||
@ -147,53 +147,48 @@ export function QueryTable({ data = [], deleteRecord, showModal }: IProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="rounded-md border">
|
||||
<Table rootClassName="rounded-md">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
<div className="rounded-md border w-full bg-bg-card">
|
||||
<Table rootClassName="rounded-md">
|
||||
<TableHeader className="bg-bg-card">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cn(cell.column.columnDef.meta?.cellClassName)}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cn(cell.column.columnDef.meta?.cellClassName)}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableEmpty columnsLength={columns.length}></TableEmpty>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<TableEmpty columnsLength={columns.length}></TableEmpty>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
.typeahead-popover {
|
||||
background: #fff;
|
||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
position: fixed;
|
||||
@ -10,16 +9,6 @@
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.typeahead-popover ul::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.typeahead-popover ul {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.typeahead-popover ul li {
|
||||
@ -28,7 +17,6 @@
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.typeahead-popover ul li.selected {
|
||||
@ -37,7 +25,6 @@
|
||||
|
||||
.typeahead-popover li {
|
||||
margin: 0 8px 0 8px;
|
||||
color: #050505;
|
||||
cursor: pointer;
|
||||
line-height: 16px;
|
||||
font-size: 15px;
|
||||
@ -45,7 +32,6 @@
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -89,7 +89,7 @@ function PromptContent({
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })}
|
||||
className={cn('border rounded-sm ', { 'border-accent-primary': !isBlur })}
|
||||
>
|
||||
{showToolbar && (
|
||||
<div className="border-b px-2 py-2 justify-end flex">
|
||||
@ -107,7 +107,7 @@ function PromptContent({
|
||||
)}
|
||||
<ContentEditable
|
||||
className={cn(
|
||||
'relative px-2 py-1 focus-visible:outline-none max-h-[50vh] overflow-auto',
|
||||
'relative px-2 py-1 focus-visible:outline-none max-h-[50vh] overflow-auto text-sm',
|
||||
{
|
||||
'min-h-40': multiLine,
|
||||
},
|
||||
@ -163,7 +163,7 @@ export function PromptEditor({
|
||||
placeholder={
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-1 left-2 text-text-secondary pointer-events-none',
|
||||
'absolute top-1 left-2 text-text-disabled pointer-events-none',
|
||||
{
|
||||
'truncate w-[90%]': !multiLine,
|
||||
'translate-y-10': multiLine,
|
||||
|
||||
@ -49,7 +49,7 @@ export class VariableNode extends DecoratorNode<ReactNode> {
|
||||
|
||||
decorate(): ReactNode {
|
||||
let content: ReactNode = (
|
||||
<div className="text-blue-600">{this.__label}</div>
|
||||
<div className="text-accent-primary">{this.__label}</div>
|
||||
);
|
||||
if (this.__parentLabel) {
|
||||
content = (
|
||||
@ -62,7 +62,7 @@ export class VariableNode extends DecoratorNode<ReactNode> {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="bg-gray-200 dark:bg-gray-400 text-sm inline-flex items-center rounded-md px-2 py-1">
|
||||
<div className="bg-accent-primary-5 text-sm inline-flex items-center rounded-md px-2 py-1">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -91,13 +91,13 @@ function VariablePickerMenuItem({
|
||||
id={'typeahead-item-' + index}
|
||||
>
|
||||
<div>
|
||||
<span className="text text-slate-500">{option.title}</span>
|
||||
<span className="text text-text-secondary">{option.title}</span>
|
||||
<ul className="pl-2 py-1">
|
||||
{option.options.map((x) => (
|
||||
<li
|
||||
key={x.value}
|
||||
onClick={() => selectOptionAndCleanUp(x)}
|
||||
className="hover:bg-slate-300 p-1"
|
||||
className="hover:bg-bg-card p-1 text-text-primary rounded-sm"
|
||||
>
|
||||
{x.label}
|
||||
</li>
|
||||
@ -335,8 +335,8 @@ export default function VariablePickerMenuPlugin({
|
||||
const nextOptions = buildNextOptions();
|
||||
return anchorElementRef.current && nextOptions.length
|
||||
? ReactDOM.createPortal(
|
||||
<div className="typeahead-popover w-[200px] p-2">
|
||||
<ul className="overflow-y-auto !scrollbar-thin overflow-x-hidden">
|
||||
<div className="typeahead-popover w-[200px] p-2 bg-bg-base">
|
||||
<ul className="scroll-auto overflow-x-hidden">
|
||||
{nextOptions.map((option, i: number) => (
|
||||
<VariablePickerMenuItem
|
||||
index={i}
|
||||
|
||||
@ -20,7 +20,8 @@ export function PdfFormFields({ prefix }: CommonProps) {
|
||||
return (
|
||||
!isEmpty(parseMethod) &&
|
||||
parseMethod !== ParseDocumentType.DeepDOC &&
|
||||
parseMethod !== ParseDocumentType.PlainText
|
||||
parseMethod !== ParseDocumentType.PlainText &&
|
||||
parseMethod !== ParseDocumentType.TCADPParser
|
||||
);
|
||||
}, [parseMethod]);
|
||||
|
||||
|
||||
11
web/src/pages/agent/hooks/use-is-mcp.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Operator } from '../constant';
|
||||
import useGraphStore from '../store';
|
||||
|
||||
export function useIsMcp(operatorName: Operator) {
|
||||
const clickedToolId = useGraphStore((state) => state.clickedToolId);
|
||||
|
||||
return (
|
||||
operatorName === Operator.Tool &&
|
||||
Object.values(Operator).every((x) => x !== clickedToolId)
|
||||
);
|
||||
}
|
||||
@ -75,7 +75,7 @@ export default function Agents() {
|
||||
title={t('flow.agents')}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
icon="agent"
|
||||
icon="agents"
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
|
||||
@ -41,7 +41,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
className="space-y-6 w-full text-ellipsis overflow-hidden"
|
||||
id={TagRenameId}
|
||||
>
|
||||
<NameFormField></NameFormField>
|
||||
@ -53,6 +53,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
|
||||
<FormLabel required>DSL</FormLabel>
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
className="w-[calc(100%-40px)] text-ellipsis overflow-hidden"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
maxFileCount={1}
|
||||
|
||||
@ -43,7 +43,10 @@ export function SeeAllCard() {
|
||||
const { navigateToDatasetList } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<Card className="w-40 flex-none h-full" onClick={navigateToDatasetList}>
|
||||
<Card
|
||||
className="w-full flex-none h-full cursor-pointer"
|
||||
onClick={navigateToDatasetList}
|
||||
>
|
||||
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
|
||||
See All <ChevronRight className="size-4" />
|
||||
</CardContent>
|
||||
|
||||
@ -63,7 +63,7 @@ export default function Datasets() {
|
||||
filters={owners}
|
||||
onChange={handleFilterSubmit}
|
||||
className="px-8"
|
||||
icon={'data'}
|
||||
icon={'datasets'}
|
||||
>
|
||||
<Button onClick={showModal}>
|
||||
<Plus className=" size-2.5" />
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
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 { AgentDropdown } from '../agents/agent-dropdown';
|
||||
import { useRenameAgent } from '../agents/use-rename-agent';
|
||||
import { ApplicationCard } from './application-card';
|
||||
|
||||
export function Agents() {
|
||||
const { data } = useFetchAgentListByPage();
|
||||
@ -21,9 +21,9 @@ export function Agents() {
|
||||
return (
|
||||
<>
|
||||
{data.slice(0, 10).map((x) => (
|
||||
<ApplicationCard
|
||||
<HomeCard
|
||||
key={x.id}
|
||||
app={x}
|
||||
data={{ name: x.title, ...x } as any}
|
||||
onClick={navigateToAgent(x.id)}
|
||||
moreDropdown={
|
||||
<AgentDropdown
|
||||
@ -33,7 +33,7 @@ export function Agents() {
|
||||
<MoreButton></MoreButton>
|
||||
</AgentDropdown>
|
||||
}
|
||||
></ApplicationCard>
|
||||
></HomeCard>
|
||||
))}
|
||||
{agentRenameVisible && (
|
||||
<RenameDialog
|
||||
|
||||
@ -48,7 +48,7 @@ export type SeeAllAppCardProps = {
|
||||
|
||||
export function SeeAllAppCard({ click }: SeeAllAppCardProps) {
|
||||
return (
|
||||
<Card className="w-64 min-h-[76px]" onClick={click}>
|
||||
<Card className="w-full min-h-[76px] cursor-pointer" onClick={click}>
|
||||
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
|
||||
See All <ChevronRight className="size-4" />
|
||||
</CardContent>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import { CardSineLineContainer } from '@/components/card-singleline-container';
|
||||
import { HomeIcon } from '@/components/svg-icon';
|
||||
import { Segmented, SegmentedValue } from '@/components/ui/segmented';
|
||||
import { Routes } from '@/routes';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@ -10,9 +11,9 @@ import { ChatList } from './chat-list';
|
||||
import { SearchList } from './search-list';
|
||||
|
||||
const IconMap = {
|
||||
[Routes.Chats]: 'chat',
|
||||
[Routes.Searches]: 'search',
|
||||
[Routes.Agents]: 'agent',
|
||||
[Routes.Chats]: 'chats',
|
||||
[Routes.Searches]: 'searches',
|
||||
[Routes.Agents]: 'agents',
|
||||
};
|
||||
|
||||
export function Applications() {
|
||||
@ -34,33 +35,40 @@ export function Applications() {
|
||||
);
|
||||
|
||||
const handleChange = (path: SegmentedValue) => {
|
||||
setVal(path as string);
|
||||
setVal(path as Routes);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="mt-12">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h2 className="text-2xl font-bold flex gap-2.5">
|
||||
<IconFont
|
||||
{/* <IconFont
|
||||
name={IconMap[val as keyof typeof IconMap]}
|
||||
className="size-8"
|
||||
></IconFont>
|
||||
></IconFont> */}
|
||||
<HomeIcon
|
||||
name={`${IconMap[val as keyof typeof IconMap]}`}
|
||||
width={'32'}
|
||||
/>
|
||||
{options.find((x) => x.value === val)?.label}
|
||||
</h2>
|
||||
<Segmented
|
||||
options={options}
|
||||
value={val}
|
||||
onChange={handleChange}
|
||||
className="bg-bg-card border border-border-button rounded-full"
|
||||
activeClassName="bg-text-primary border-none"
|
||||
buttonSize="xl"
|
||||
// className="bg-bg-card border border-border-button rounded-lg"
|
||||
// activeClassName="bg-text-primary border-none rounded-lg"
|
||||
></Segmented>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{/* <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>}
|
||||
</div>
|
||||
</CardSineLineContainer>
|
||||
{/* </div> */}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
@ -5,7 +6,6 @@ import { useFetchDialogList } from '@/hooks/use-chat-request';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChatDropdown } from '../next-chats/chat-dropdown';
|
||||
import { useRenameChat } from '../next-chats/hooks/use-rename-chat';
|
||||
import { ApplicationCard } from './application-card';
|
||||
|
||||
export function ChatList() {
|
||||
const { t } = useTranslation();
|
||||
@ -24,12 +24,11 @@ export function ChatList() {
|
||||
return (
|
||||
<>
|
||||
{data.dialogs.slice(0, 10).map((x) => (
|
||||
<ApplicationCard
|
||||
<HomeCard
|
||||
key={x.id}
|
||||
app={{
|
||||
data={{
|
||||
avatar: x.icon,
|
||||
title: x.name,
|
||||
update_time: x.update_time,
|
||||
...x,
|
||||
}}
|
||||
onClick={navigateToChat(x.id)}
|
||||
moreDropdown={
|
||||
@ -37,7 +36,7 @@ export function ChatList() {
|
||||
<MoreButton></MoreButton>
|
||||
</ChatDropdown>
|
||||
}
|
||||
></ApplicationCard>
|
||||
></HomeCard>
|
||||
))}
|
||||
{chatRenameVisible && (
|
||||
<RenameDialog
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import { CardSineLineContainer } from '@/components/card-singleline-container';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { HomeIcon } from '@/components/svg-icon';
|
||||
import { CardSkeleton } from '@/components/ui/skeleton';
|
||||
import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -21,7 +22,8 @@ export function Datasets() {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-6 flex gap-2.5 items-center">
|
||||
<IconFont name="data" className="size-8"></IconFont>
|
||||
{/* <IconFont name="data" className="size-8"></IconFont> */}
|
||||
<HomeIcon name="datasets" width={'32'} />
|
||||
{t('header.dataset')}
|
||||
</h2>
|
||||
<div className="flex gap-6">
|
||||
@ -30,7 +32,8 @@ export function Datasets() {
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 3xl:grid-cols-7 max-h-[78vh] overflow-auto">
|
||||
// <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 3xl:grid-cols-7 max-h-[78vh] overflow-auto">
|
||||
<CardSineLineContainer>
|
||||
{kbs
|
||||
?.slice(0, 6)
|
||||
.map((dataset) => (
|
||||
@ -43,7 +46,8 @@ export function Datasets() {
|
||||
<div className="min-h-24">
|
||||
<SeeAllCard></SeeAllCard>
|
||||
</div>
|
||||
</div>
|
||||
</CardSineLineContainer>
|
||||
// </div>
|
||||
)}
|
||||
</div>
|
||||
{datasetRenameVisible && (
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
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 { useFetchSearchList, useRenameSearch } from '../next-searches/hooks';
|
||||
import { SearchDropdown } from '../next-searches/search-dropdown';
|
||||
import { ApplicationCard } from './application-card';
|
||||
|
||||
export function SearchList() {
|
||||
const { data, refetch: refetchList } = useFetchSearchList();
|
||||
@ -25,13 +25,9 @@ export function SearchList() {
|
||||
return (
|
||||
<>
|
||||
{data?.data.search_apps.slice(0, 10).map((x) => (
|
||||
<ApplicationCard
|
||||
<HomeCard
|
||||
key={x.id}
|
||||
app={{
|
||||
avatar: x.avatar,
|
||||
title: x.name,
|
||||
update_time: x.update_time,
|
||||
}}
|
||||
data={x}
|
||||
onClick={navigateToSearch(x.id)}
|
||||
moreDropdown={
|
||||
<SearchDropdown
|
||||
@ -41,7 +37,7 @@ export function SearchList() {
|
||||
<MoreButton></MoreButton>
|
||||
</SearchDropdown>
|
||||
}
|
||||
></ApplicationCard>
|
||||
></HomeCard>
|
||||
))}
|
||||
{openCreateModal && (
|
||||
<RenameDialog
|
||||
|
||||
@ -40,7 +40,7 @@ export default function ChatList() {
|
||||
<div className="px-8 pt-8">
|
||||
<ListFilterBar
|
||||
title={t('chat.chatApps')}
|
||||
icon="chat"
|
||||
icon="chats"
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
>
|
||||
|
||||
@ -26,7 +26,7 @@ export default function SearchPage({
|
||||
// const { data: userInfo } = useFetchUserInfo();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section className="relative w-full flex transition-all justify-center items-center mt-32">
|
||||
<section className="relative w-full flex transition-all justify-center items-center mt-[15vh]">
|
||||
<div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]">
|
||||
<h1
|
||||
className={cn(
|
||||
@ -36,7 +36,7 @@ export default function SearchPage({
|
||||
RAGFlow
|
||||
</h1>
|
||||
|
||||
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[230px] border">
|
||||
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[240px] border">
|
||||
{!isSearching && <Spotlight className="z-0" />}
|
||||
<div className="flex flex-col justify-center items-center w-2/3">
|
||||
{!isSearching && (
|
||||
@ -55,7 +55,7 @@ export default function SearchPage({
|
||||
<div className="relative w-full ">
|
||||
<Input
|
||||
placeholder={t('search.searchGreeting')}
|
||||
className="w-full rounded-full py-6 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
|
||||
className="w-full rounded-full py-7 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
|
||||
value={searchText}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
@ -91,7 +91,6 @@ export default function SearchingView({
|
||||
>
|
||||
RAGFlow
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
|
||||
@ -117,14 +116,14 @@ export default function SearchingView({
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
|
||||
<X
|
||||
className="text-text-secondary cursor-pointer"
|
||||
className="text-text-secondary cursor-pointer opacity-80"
|
||||
size={14}
|
||||
onClick={() => {
|
||||
setSearchtext('');
|
||||
handleClickRelatedQuestion('');
|
||||
}}
|
||||
/>
|
||||
<span className="text-text-secondary ml-4">|</span>
|
||||
<span className="text-text-secondary opacity-20 ml-4">|</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full bg-text-primary p-1 text-bg-base shadow w-12 h-8 ml-4"
|
||||
@ -170,11 +169,13 @@ export default function SearchingView({
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="w-full border-b border-border-default/80 my-6"></div>
|
||||
{answer.answer && !sendingLoading && (
|
||||
<div className="w-full border-b border-border-default/80 my-6"></div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* retrieval documents */}
|
||||
{!isSearchStrEmpty && (
|
||||
{!isSearchStrEmpty && !sendingLoading && (
|
||||
<>
|
||||
<div className=" mt-3 w-44 ">
|
||||
<RetrievalDocuments
|
||||
@ -183,7 +184,7 @@ export default function SearchingView({
|
||||
onTesting={handleTestChunk}
|
||||
></RetrievalDocuments>
|
||||
</div>
|
||||
<div className="w-full border-b border-border-default/80 my-6"></div>
|
||||
{/* <div className="w-full border-b border-border-default/80 my-6"></div> */}
|
||||
</>
|
||||
)}
|
||||
<div className="mt-3 ">
|
||||
@ -218,7 +219,7 @@ export default function SearchingView({
|
||||
</Popover>
|
||||
</div>
|
||||
<div
|
||||
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
|
||||
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit mt-3"
|
||||
onClick={() =>
|
||||
clickDocumentButton(chunk.doc_id, chunk as any)
|
||||
}
|
||||
@ -237,26 +238,30 @@ export default function SearchingView({
|
||||
)}
|
||||
{relatedQuestions?.length > 0 &&
|
||||
searchData.search_config.related_search && (
|
||||
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
|
||||
<p className="text-text-primary mb-2 text-xl">
|
||||
{t('search.relatedSearch')}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap justify-start gap-2">
|
||||
{relatedQuestions?.map((x, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="transparent"
|
||||
className="bg-bg-card text-text-secondary"
|
||||
onClick={handleClickRelatedQuestion(
|
||||
x,
|
||||
searchData.search_config.summary,
|
||||
)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
))}
|
||||
<>
|
||||
<div className="w-full border-b border-border-default/80 mt-6"></div>
|
||||
|
||||
<div className="mt-6 w-full overflow-hidden opacity-100 max-h-96">
|
||||
<p className="text-text-primary mb-2 text-xl">
|
||||
{t('search.relatedSearch')}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap justify-start gap-2">
|
||||
{relatedQuestions?.map((x, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="transparent"
|
||||
className="bg-bg-card text-text-secondary"
|
||||
onClick={handleClickRelatedQuestion(
|
||||
x,
|
||||
searchData.search_config.summary,
|
||||
)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -272,7 +277,6 @@ export default function SearchingView({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mindMapVisible && (
|
||||
<div className="flex-1 h-[88dvh] z-30 ml-32 mt-5">
|
||||
<MindMapDrawer
|
||||
|
||||
@ -48,7 +48,7 @@ export default function SearchList() {
|
||||
<section className="w-full h-full flex flex-col">
|
||||
<div className="px-8 pt-8">
|
||||
<ListFilterBar
|
||||
icon="search"
|
||||
icon="searches"
|
||||
title={t('searchApps')}
|
||||
showFilter={false}
|
||||
onSearchChange={(e) => handleSearchChange(e.target.value)}
|
||||
|
||||
@ -14,7 +14,7 @@ export function ProfileSettingWrapperCard({
|
||||
children,
|
||||
}: ProfileSettingWrapperCardProps) {
|
||||
return (
|
||||
<Card className="w-full mb-5 border-border-button bg-transparent">
|
||||
<Card className="w-full border-border-button bg-transparent relative">
|
||||
<CardHeader className="border-b border-border-button p-5">
|
||||
{header}
|
||||
</CardHeader>
|
||||
|
||||
@ -151,7 +151,7 @@ export default function McpServer() {
|
||||
onOk={onImportOk}
|
||||
></ImportMcpDialog>
|
||||
)}
|
||||
<Spotlight className="z-0" opcity={0.7} coverage={70} />
|
||||
<Spotlight />
|
||||
</ProfileSettingWrapperCard>
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,10 +38,18 @@ const UserSetting = () => {
|
||||
</Breadcrumb>
|
||||
</PageHeader>
|
||||
<div
|
||||
className={cn(styles.settingWrapper, 'overflow-auto flex flex-1 pt-4')}
|
||||
className={cn(
|
||||
styles.settingWrapper,
|
||||
'overflow-auto flex flex-1 pt-4 pr-4 pb-4',
|
||||
)}
|
||||
>
|
||||
<SideBar></SideBar>
|
||||
<div className={cn(styles.outletWrapper, 'flex flex-1')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.outletWrapper,
|
||||
'flex flex-1 border border-border-button rounded-lg',
|
||||
)}
|
||||
>
|
||||
<Outlet></Outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// src/components/ProfilePage.tsx
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import PasswordInput from '@/components/originui/password-input';
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
@ -121,7 +122,8 @@ const ProfilePage: FC = () => {
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-bg-base text-text-secondary p-5">
|
||||
<div className="h-full w-full text-text-secondary p-5 relative">
|
||||
<Spotlight />
|
||||
{/* Header */}
|
||||
<header className="flex flex-col gap-1 justify-between items-start mb-6">
|
||||
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as z from 'zod';
|
||||
|
||||
const AddingUserModal = ({
|
||||
visible,
|
||||
@ -8,42 +20,54 @@ const AddingUserModal = ({
|
||||
loading,
|
||||
onOk,
|
||||
}: IModalProps<string>) => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
type FieldType = {
|
||||
email?: string;
|
||||
};
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.min(1, { message: t('common.required') }),
|
||||
});
|
||||
|
||||
const handleOk = async () => {
|
||||
const ret = await form.validateFields();
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
return onOk?.(ret.email);
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleOk = async (data: FormData) => {
|
||||
return onOk?.(data.email);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('setting.add')}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
okButtonProps={{ loading }}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
onOk={form.handleSubmit(handleOk)}
|
||||
confirmLoading={loading}
|
||||
okText={t('common.ok')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label={t('setting.email')}
|
||||
name="email"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleOk)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('setting.email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('setting.email')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
.teamWrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
.teamCard {
|
||||
// width: 100%;
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,26 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
useFetchUserInfo,
|
||||
useListTenantUser,
|
||||
} from '@/hooks/user-setting-hooks';
|
||||
import { Button, Card, Flex, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TeamOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import { SearchInput } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import AddingUserModal from './add-user-modal';
|
||||
import { useAddUser } from './hooks';
|
||||
import styles from './index.less';
|
||||
import TenantTable from './tenant-table';
|
||||
import UserTable from './user-table';
|
||||
|
||||
const iconStyle = { fontSize: 20, color: '#1677ff' };
|
||||
|
||||
const UserSettingTeam = () => {
|
||||
const { data: userInfo } = useFetchUserInfo();
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchUser, setSearchUser] = useState('');
|
||||
useListTenantUser();
|
||||
const {
|
||||
addingTenantModalVisible,
|
||||
@ -26,38 +30,58 @@ const UserSettingTeam = () => {
|
||||
} = useAddUser();
|
||||
|
||||
return (
|
||||
<div className={styles.teamWrapper}>
|
||||
<Card className={styles.teamCard}>
|
||||
<Flex align="center" justify={'space-between'}>
|
||||
<span>
|
||||
{userInfo.nickname} {t('setting.workspace')}
|
||||
</span>
|
||||
<Button type="primary" onClick={showAddingTenantModal}>
|
||||
<UserAddOutlined />
|
||||
{t('setting.invite')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<div className="w-full flex flex-col gap-4 p-4 relative">
|
||||
<Spotlight />
|
||||
<Card className="bg-transparent border-none px-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-0 pt-1">
|
||||
<CardTitle className="text-2xl font-medium">
|
||||
{userInfo?.nickname} {t('setting.workspace')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<UserOutlined style={iconStyle} /> {t('setting.teamMembers')}
|
||||
</Space>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<UserTable></UserTable>
|
||||
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
|
||||
<Card className="bg-transparent border-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
|
||||
{/* <User className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-base">
|
||||
{t('setting.teamMembers')}
|
||||
</CardTitle>
|
||||
<section className="flex gap-4 items-center">
|
||||
<SearchInput
|
||||
className="bg-bg-input border-border-default w-32"
|
||||
placeholder={t('common.search')}
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
<Button onClick={showAddingTenantModal}>
|
||||
<UserPlus className=" h-4 w-4" />
|
||||
{t('setting.invite')}
|
||||
</Button>
|
||||
</section>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<UserTable searchUser={searchUser}></UserTable>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<TeamOutlined style={iconStyle} /> {t('setting.joinedTeams')}
|
||||
</Space>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<TenantTable></TenantTable>
|
||||
|
||||
<Card className="bg-transparent border-none mt-8">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
|
||||
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-base">
|
||||
{t('setting.joinedTeams')}
|
||||
</CardTitle>
|
||||
<SearchInput
|
||||
className="bg-bg-input border-border-default w-32"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={t('common.search')}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<TenantTable searchTerm={searchTerm}></TenantTable>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{addingTenantModalVisible && (
|
||||
<AddingUserModal
|
||||
visible
|
||||
|
||||
@ -1,75 +1,160 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useFetchUserInfo, useListTenant } from '@/hooks/user-setting-hooks';
|
||||
import { ITenant } from '@/interfaces/database/user-setting';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Space, Table } from 'antd';
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, LogOut } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TenantRole } from '../constants';
|
||||
import { useHandleAgreeTenant, useHandleQuitUser } from './hooks';
|
||||
|
||||
const TenantTable = () => {
|
||||
const TenantTable = ({ searchTerm }: { searchTerm: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { data, loading } = useListTenant();
|
||||
const { handleAgree } = useHandleAgreeTenant();
|
||||
const { data: user } = useFetchUserInfo();
|
||||
const { handleQuitTenantUser } = useHandleQuitUser();
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortedData = useMemo(() => {
|
||||
if (!data || data.length === 0) return data;
|
||||
let filtered = data;
|
||||
if (searchTerm) {
|
||||
filtered = data.filter(
|
||||
(tenant) =>
|
||||
tenant.nickname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
tenant.email.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (sortOrder) {
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const dateA = new Date(a.update_date).getTime();
|
||||
const dateB = new Date(b.update_date).getTime();
|
||||
|
||||
const columns: TableProps<ITenant>['columns'] = [
|
||||
{
|
||||
title: t('common.name'),
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
},
|
||||
{
|
||||
title: t('setting.email'),
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: t('setting.updateDate'),
|
||||
dataIndex: 'update_date',
|
||||
key: 'update_date',
|
||||
render(value) {
|
||||
return formatDate(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('common.action'),
|
||||
key: 'action',
|
||||
render: (_, { role, tenant_id }) => {
|
||||
if (role === TenantRole.Invite) {
|
||||
return (
|
||||
<Space>
|
||||
<Button type="link" onClick={handleAgree(tenant_id, true)}>
|
||||
{t(`setting.agree`)}
|
||||
</Button>
|
||||
<Button type="link" onClick={handleAgree(tenant_id, false)}>
|
||||
{t(`setting.refuse`)}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
} else if (role === TenantRole.Normal && user.id !== tenant_id) {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={handleQuitTenantUser(user.id, tenant_id)}
|
||||
>
|
||||
{t('setting.quit')}
|
||||
</Button>
|
||||
);
|
||||
if (sortOrder === 'asc') {
|
||||
return dateA - dateB;
|
||||
} else {
|
||||
return dateB - dateA;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [data, sortOrder, searchTerm]);
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
setSortOrder('desc');
|
||||
} else if (sortOrder === 'desc') {
|
||||
setSortOrder(null);
|
||||
} else {
|
||||
setSortOrder('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const renderSortIcon = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
return <ArrowUp className="ml-1 h-4 w-4" />;
|
||||
} else if (sortOrder === 'desc') {
|
||||
return <ArrowDown className="ml-1 h-4 w-4" />;
|
||||
} else {
|
||||
return <ArrowUpDown className="ml-1 h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table<ITenant>
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey={'tenant_id'}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
|
||||
<Table rootClassName="rounded-lg">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
|
||||
<TableHead
|
||||
className="h-12 px-4 cursor-pointer"
|
||||
onClick={toggleSortOrder}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{t('setting.updateDate')}
|
||||
{renderSortIcon()}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('common.action')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : sortedData && sortedData.length > 0 ? (
|
||||
sortedData.map((tenant) => (
|
||||
<TableRow key={tenant.tenant_id} className="hover:bg-bg-card">
|
||||
<TableCell className="p-4 flex gap-1 items-center">
|
||||
<RAGFlowAvatar
|
||||
isPerson
|
||||
className="size-4"
|
||||
avatar={tenant.avatar}
|
||||
name={tenant.nickname}
|
||||
/>
|
||||
{tenant.nickname}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{formatDate(tenant.update_date)}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">{tenant.email}</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{tenant.role === TenantRole.Invite ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto"
|
||||
onClick={handleAgree(tenant.tenant_id, true)}
|
||||
>
|
||||
{t(`setting.agree`)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto"
|
||||
onClick={handleAgree(tenant.tenant_id, false)}
|
||||
>
|
||||
{t(`setting.refuse`)}
|
||||
</Button>
|
||||
</div>
|
||||
) : tenant.role === TenantRole.Normal &&
|
||||
user.id !== tenant.tenant_id ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleQuitTenantUser(user.id, tenant.tenant_id)}
|
||||
>
|
||||
{/* {t('setting.quit')} */}
|
||||
<LogOut />
|
||||
</Button>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="h-24 text-center">
|
||||
{t('common.noData')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,75 +1,160 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useListTenantUser } from '@/hooks/user-setting-hooks';
|
||||
import { ITenantUser } from '@/interfaces/database/user-setting';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Table, Tag } from 'antd';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Trash2 } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TenantRole } from '../constants';
|
||||
import { useHandleDeleteUser } from './hooks';
|
||||
|
||||
const ColorMap = {
|
||||
[TenantRole.Normal]: 'green',
|
||||
[TenantRole.Invite]: 'orange',
|
||||
[TenantRole.Owner]: 'red',
|
||||
const ColorMap: Record<string, string> = {
|
||||
[TenantRole.Normal]: 'bg-transparent text-text-primary',
|
||||
[TenantRole.Invite]: 'bg-accent-primary-5 bg-accent-primary rounded-sm',
|
||||
[TenantRole.Owner]: 'bg-red-100 text-red-800',
|
||||
};
|
||||
|
||||
const UserTable = () => {
|
||||
const UserTable = ({ searchUser }: { searchUser: string }) => {
|
||||
const { data, loading } = useListTenantUser();
|
||||
const { handleDeleteTenantUser } = useHandleDeleteUser();
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const sortedData = useMemo(() => {
|
||||
console.log('sortedData', data, searchUser);
|
||||
if (!data || data.length === 0) return data;
|
||||
let filtered = data;
|
||||
if (searchUser) {
|
||||
filtered = filtered.filter(
|
||||
(tenant) =>
|
||||
tenant.nickname.toLowerCase().includes(searchUser.toLowerCase()) ||
|
||||
tenant.email.toLowerCase().includes(searchUser.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (sortOrder) {
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const dateA = new Date(a.update_date).getTime();
|
||||
const dateB = new Date(b.update_date).getTime();
|
||||
|
||||
const columns: TableProps<ITenantUser>['columns'] = [
|
||||
{
|
||||
title: t('common.name'),
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
},
|
||||
{
|
||||
title: t('setting.email'),
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: t('setting.role'),
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render(value, { role }) {
|
||||
return (
|
||||
<Tag color={ColorMap[role as keyof typeof ColorMap]}>
|
||||
{upperFirst(role)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('setting.updateDate'),
|
||||
dataIndex: 'update_date',
|
||||
key: 'update_date',
|
||||
render(value) {
|
||||
return formatDate(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('common.action'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Button type="text" onClick={handleDeleteTenantUser(record.user_id)}>
|
||||
<DeleteOutlined size={20} />
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (sortOrder === 'asc') {
|
||||
return dateA - dateB;
|
||||
} else {
|
||||
return dateB - dateA;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [data, sortOrder, searchUser]);
|
||||
const toggleSortOrder = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
setSortOrder('desc');
|
||||
} else if (sortOrder === 'desc') {
|
||||
setSortOrder(null);
|
||||
} else {
|
||||
setSortOrder('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const renderSortIcon = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
return <ArrowUp className="ml-1 h-4 w-4 " />;
|
||||
} else if (sortOrder === 'desc') {
|
||||
return <ArrowDown className="ml-1 h-4 w-4" />;
|
||||
} else {
|
||||
return <ArrowUpDown className="ml-1 h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Table<ITenantUser>
|
||||
rowKey={'user_id'}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
|
||||
<Table rootClassName="rounded-lg">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
|
||||
<TableHead
|
||||
className="h-12 px-4 cursor-pointer"
|
||||
onClick={toggleSortOrder}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{t('setting.updateDate')}
|
||||
{renderSortIcon()}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('setting.role')}</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('common.action')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : sortedData && sortedData.length > 0 ? (
|
||||
sortedData.map((record) => (
|
||||
<TableRow key={record.user_id} className="hover:bg-bg-card">
|
||||
<TableCell className="p-4 ">
|
||||
<div className="flex gap-1 items-center">
|
||||
<RAGFlowAvatar
|
||||
isPerson
|
||||
className="size-4"
|
||||
avatar={record.avatar}
|
||||
name={record.nickname}
|
||||
/>
|
||||
{record.nickname}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{formatDate(record.update_date)}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">{record.email}</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{record.role === TenantRole.Normal && (
|
||||
<Badge className={ColorMap[record.role]}>
|
||||
{upperFirst('Member')}
|
||||
</Badge>
|
||||
)}
|
||||
{record.role !== TenantRole.Normal && (
|
||||
<Badge className={ColorMap[record.role]}>
|
||||
{upperFirst(record.role)}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleDeleteTenantUser(record.user_id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
{t('common.noData')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||