Compare commits

...

11 Commits

Author SHA1 Message Date
850e119a81 Doc: Updated How to update MinerU's settings (#10818)
### What problem does this PR solve?

### Type of change

- [x] Documentation Update
2025-10-27 19:38:42 +08:00
0a78920bff Feat: Modify the style of the agent operator form #10703 (#10821)
### What problem does this PR solve?

Feat: Modify the style of the agent operator form #10703

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-27 19:37:52 +08:00
0089e2b30c Fix: bug fixes and icon replacement #10703 (#10814)
### What problem does this PR solve?

Fix: bug fixes and icon replacement #10703

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-27 19:02:18 +08:00
b7cb4d3e35 Feat:All-in-one MinerU and Docling (#10813)
### What problem does this PR solve?

issue:
[#10789](https://github.com/infiniflow/ragflow/issues/10789)
change:
All-in-one MinerU and Docling

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-27 19:01:15 +08:00
fd1ad18489 Feat: Adjust the style of the toolbar at the bottom of the agent canvas #10703 (#10807)
### What problem does this PR solve?

Feat: Adjust the style of the toolbar at the bottom of the agent canvas
#10703
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-27 17:04:32 +08:00
5acc407240 Feat: MinerU supports VLM-Transfomers backend (#10809)
### What problem does this PR solve?

MinerU supports VLM-Transfomers backend.

Set `MINERU_BACKEND="pipeline"` to choose the backend. (Options:
pipeline | vlm-transformers, default is pipeline)

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-10-27 17:04:13 +08:00
16ec6ad346 Fix: Pass kwargs in python api #10699 (#10808)
### What problem does this PR solve?

Fix: Pass kwargs in python api #10699

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-27 15:18:56 +08:00
5312b75362 Fix: Home and team page style adjustment, and some bug fixes #10703 (#10805)
### What problem does this PR solve?

Fix: Home and team page style adjustment, and some bug fixes #10703

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-27 15:15:12 +08:00
33a189f620 Feat: add TCADP Parser (#10775)
### What problem does this PR solve?

This PR adds a new TCADP (Tencent Cloud Advanced Document Processing)
parser to RAGFlow, enabling users to leverage Tencent Cloud's document
parsing capabilities for more accurate and structured document
processing. The implementation includes:
New TCADP Parser: A complete implementation of Tencent Cloud's document
parsing API without SDK dependency
Configuration Support: Added configuration options in service_conf.yaml
for Tencent Cloud API credentials
Frontend Integration: Updated UI components to support the new TCADP
parser option
Error Handling: Comprehensive error handling and retry mechanisms for
API calls
Result Processing: Support for both SSE streaming and JSON response
formats from Tencent Cloud API

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-10-27 15:14:58 +08:00
56def59c2b Fix:Error retrieving DOCX image (docx.image.exceptions.UnrecognizedImageError) (#10794)
### What problem does this PR solve?

https://github.com/infiniflow/ragflow/issues/10776

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-10-27 13:23:16 +08:00
7fbab750af Doc: readme updates. (#10801)
### Type of change

- [x] Documentation Update
2025-10-27 12:20:23 +08:00
88 changed files with 1713 additions and 536 deletions

View File

@ -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.

View File

@ -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.

View File

@ -60,6 +60,7 @@
## 🔥 最新情報
- 2025-10-23 ドキュメント解析方法として MinerU と Docling をサポートします。
- 2025-10-15 オーケストレーションされたデータパイプラインのサポート。
- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。
- 2025-08-01 エージェントワークフローとMCPをサポート。

View File

@ -60,6 +60,7 @@
## 🔥 업데이트
- 2025-10-23 문서 파싱 방법으로 MinerU 및 Docling을 지원합니다.
- 2025-10-15 조정된 데이터 파이프라인 지원.
- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.
- 2025-08-01 에이전트 워크플로우와 MCP를 지원합니다.

View File

@ -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.

View File

@ -83,6 +83,7 @@
## 🔥 近期更新
- 2025-10-23 支援 MinerU 和 Docling 作為文件解析方法。
- 2025-10-15 支援可編排的資料管道。
- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。
- 2025-08-01 支援 agentic workflow 和 MCP

View File

@ -83,6 +83,7 @@
## 🔥 近期更新
- 2025-10-23 支持 MinerU 和 Docling 作为文档解析方法。
- 2025-10-15 支持可编排的数据管道。
- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型。
- 2025-08-01 支持 agentic workflow 和 MCP。

View File

@ -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'

View File

@ -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.")

View 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]}...")

View File

@ -221,3 +221,4 @@ REGISTER_ENABLED=1
# - For OpenSearch:
# COMPOSE_PROFILES=opensearch,sandbox
USE_DOCLING=false
USE_MINERU=false

View File

@ -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

View File

@ -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'

View File

@ -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).
:::

View File

@ -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",

View File

@ -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:

View File

@ -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()

View File

@ -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)

View File

@ -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
View File

@ -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]]

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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;
}
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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]"

View File

@ -70,7 +70,7 @@ export function LargeModelFormField({
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant={'ghost'}>
<Funnel />
<Funnel className="text-text-disabled" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>

View File

@ -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,

View File

@ -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
)}

View File

@ -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,
)}
>

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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}

View File

@ -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

View File

@ -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>

View File

@ -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,
)}

View File

@ -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}

View File

@ -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

View File

@ -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: `

View File

@ -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: `

View File

@ -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 (
<>

View File

@ -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>

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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`,
)}

View 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>
);
}

View File

@ -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}

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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>
);

View File

@ -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}

View File

@ -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]);

View 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)
);
}

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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" />

View File

@ -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

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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

View File

@ -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 && (

View File

@ -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

View File

@ -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}
>

View File

@ -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') {

View File

@ -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

View File

@ -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)}

View File

@ -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>

View File

@ -151,7 +151,7 @@ export default function McpServer() {
onOk={onImportOk}
></ImportMcpDialog>
)}
<Spotlight className="z-0" opcity={0.7} coverage={70} />
<Spotlight />
</ProfileSettingWrapperCard>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>
);

View File

@ -1,9 +0,0 @@
.teamWrapper {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
.teamCard {
// width: 100%;
}
}

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
);
};