Merge branch 'main' into webdav_storage

This commit is contained in:
Jonah Hartmann 2025-11-21 11:51:47 +01:00 committed by GitHub
commit 839c7a69af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1263 additions and 1384 deletions

View file

@ -86,7 +86,7 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
## 🔥 Latest Updates
- 2025-11-19 Supports Gemini 3 Pro.
- 2025-11-12 Supports data synchronization from Confluence, AWS S3, Discord, Google Drive.
- 2025-11-12 Supports data synchronization from Confluence, S3, Notion, Discord, Google Drive.
- 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.

View file

@ -86,7 +86,7 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
## 🔥 Pembaruan Terbaru
- 2025-11-19 Mendukung Gemini 3 Pro.
- 2025-11-12 Mendukung sinkronisasi data dari Confluence, AWS S3, Discord, Google Drive.
- 2025-11-12 Mendukung sinkronisasi data dari Confluence, S3, Notion, Discord, Google Drive.
- 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.

View file

@ -67,7 +67,7 @@
## 🔥 最新情報
- 2025-11-19 Gemini 3 Proをサポートしています
- 2025-11-12 Confluence、AWS S3、Discord、Google Drive からのデータ同期をサポートします。
- 2025-11-12 Confluence、S3、Notion、Discord、Google Drive からのデータ同期をサポートします。
- 2025-10-23 ドキュメント解析方法として MinerU と Docling をサポートします。
- 2025-10-15 オーケストレーションされたデータパイプラインのサポート。
- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。

View file

@ -68,7 +68,7 @@
## 🔥 업데이트
- 2025-11-19 Gemini 3 Pro를 지원합니다.
- 2025-11-12 Confluence, AWS S3, Discord, Google Drive에서 데이터 동기화를 지원합니다.
- 2025-11-12 Confluence, S3, Notion, Discord, Google Drive에서 데이터 동기화를 지원합니다.
- 2025-10-23 문서 파싱 방법으로 MinerU 및 Docling을 지원합니다.
- 2025-10-15 조정된 데이터 파이프라인 지원.
- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.

View file

@ -87,7 +87,7 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
## 🔥 Últimas Atualizações
- 19-11-2025 Suporta Gemini 3 Pro.
- 12-11-2025 Suporta a sincronização de dados do Confluence, AWS S3, Discord e Google Drive.
- 12-11-2025 Suporta a sincronização de dados do Confluence, S3, Notion, Discord e Google Drive.
- 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.

View file

@ -86,7 +86,7 @@
## 🔥 近期更新
- 2025-11-19 支援 Gemini 3 Pro.
- 2025-11-12 支援從 Confluence、AWS S3、Discord、Google Drive 進行資料同步。
- 2025-11-12 支援從 Confluence、S3、Notion、Discord、Google Drive 進行資料同步。
- 2025-10-23 支援 MinerU 和 Docling 作為文件解析方法。
- 2025-10-15 支援可編排的資料管道。
- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。

View file

@ -86,7 +86,7 @@
## 🔥 近期更新
- 2025-11-19 支持 Gemini 3 Pro.
- 2025-11-12 支持从 Confluence、AWS S3、Discord、Google Drive 进行数据同步。
- 2025-11-12 支持从 Confluence、S3、Notion、Discord、Google Drive 进行数据同步。
- 2025-10-23 支持 MinerU 和 Docling 作为文档解析方法。
- 2025-10-15 支持可编排的数据管道。
- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型。

View file

@ -32,7 +32,7 @@ class IterationParam(ComponentParamBase):
def __init__(self):
super().__init__()
self.items_ref = ""
self.veriable={}
self.variable={}
def get_input_form(self) -> dict[str, dict]:
return {

View file

@ -137,7 +137,7 @@ class Retrieval(ToolBase, ABC):
if not doc_ids:
doc_ids = None
elif self._param.meta_data_filter.get("method") == "manual":
filters=self._param.meta_data_filter["manual"]
filters = self._param.meta_data_filter["manual"]
for flt in filters:
pat = re.compile(self.variable_ref_patt)
s = flt["value"]
@ -166,8 +166,8 @@ class Retrieval(ToolBase, ABC):
out_parts.append(s[last:])
flt["value"] = "".join(out_parts)
doc_ids.extend(meta_filter(metas, filters, self._param.meta_data_filter.get("logic", "and")))
if not doc_ids:
doc_ids = None
if filters and not doc_ids:
doc_ids = ["-999"]
if self._param.cross_languages:
query = cross_languages(kbs[0].tenant_id, None, query, self._param.cross_languages)

View file

@ -24,7 +24,7 @@ from flasgger import Swagger
from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
from quart_cors import cors
from common.constants import StatusEnum
from api.db.db_models import close_connection
from api.db.db_models import close_connection, APIToken
from api.db.services import UserService
from api.utils.json_encode import CustomJSONEncoder
from api.utils import commands
@ -124,6 +124,10 @@ def _load_user():
user = UserService.query(
access_token=access_token, status=StatusEnum.VALID.value
)
if not user and len(authorization.split()) == 2:
objs = APIToken.query(token=authorization.split()[1])
if objs:
user = UserService.query(id=objs[0].tenant_id, status=StatusEnum.VALID.value)
if user:
if not user[0].access_token or not user[0].access_token.strip():
logging.warning(f"User {user[0].email} has empty access_token in database")

View file

@ -311,8 +311,8 @@ async def retrieval_test():
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"], meta_data_filter.get("logic", "and")))
if not doc_ids:
doc_ids = None
if meta_data_filter["manual"] and not doc_ids:
doc_ids = ["-999"]
try:
tenants = UserTenantService.query(user_id=current_user.id)

View file

@ -1434,6 +1434,7 @@ async def retrieval_test(tenant_id):
question = req["question"]
doc_ids = req.get("document_ids", [])
use_kg = req.get("use_kg", False)
toc_enhance = req.get("toc_enhance", False)
langs = req.get("cross_languages", [])
if not isinstance(doc_ids, list):
return get_error_data_result("`documents` should be a list")
@ -1445,6 +1446,8 @@ async def retrieval_test(tenant_id):
metadata_condition = req.get("metadata_condition", {}) or {}
metas = DocumentService.get_meta_by_kbs(kb_ids)
doc_ids = meta_filter(metas, convert_conditions(metadata_condition), metadata_condition.get("logic", "and"))
if metadata_condition and not doc_ids:
doc_ids = ["-999"]
similarity_threshold = float(req.get("similarity_threshold", 0.2))
vector_similarity_weight = float(req.get("vector_similarity_weight", 0.3))
top = int(req.get("top_k", 1024))
@ -1485,6 +1488,11 @@ async def retrieval_test(tenant_id):
highlight=highlight,
rank_feature=label_question(question, kbs),
)
if toc_enhance:
chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
cks = settings.retriever.retrieval_by_toc(question, ranks["chunks"], tenant_ids, chat_mdl, size)
if cks:
ranks["chunks"] = cks
if use_kg:
ck = settings.kg_retriever.retrieval(question, [k.tenant_id for k in kbs], kb_ids, embd_mdl, LLMBundle(kb.tenant_id, LLMType.CHAT))
if ck["content_with_weight"]:

View file

@ -446,8 +446,8 @@ async def agent_completions(tenant_id, agent_id):
if req.get("stream", True):
def generate():
for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req):
async def generate():
async for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req):
if isinstance(answer, str):
try:
ans = json.loads(answer[5:]) # remove "data:"
@ -471,7 +471,7 @@ async def agent_completions(tenant_id, agent_id):
full_content = ""
reference = {}
final_ans = ""
for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req):
async for answer in agent_completion(tenant_id=tenant_id, agent_id=agent_id, **req):
try:
ans = json.loads(answer[5:])
@ -873,7 +873,7 @@ async def agent_bot_completions(agent_id):
resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
return resp
for answer in agent_completion(objs[0].tenant_id, agent_id, **req):
async for answer in agent_completion(objs[0].tenant_id, agent_id, **req):
return get_result(data=answer)
@ -981,8 +981,8 @@ async def retrieval_test_embedded():
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"], meta_data_filter.get("logic", "and")))
if not doc_ids:
doc_ids = None
if meta_data_filter["manual"] and not doc_ids:
doc_ids = ["-999"]
try:
tenants = UserTenantService.query(user_id=tenant_id)

View file

@ -415,9 +415,10 @@ def chat(dialog, messages, stream=True, **kwargs):
if not attachments:
attachments = None
elif dialog.meta_data_filter.get("method") == "manual":
attachments.extend(meta_filter(metas, dialog.meta_data_filter["manual"], dialog.meta_data_filter.get("logic", "and")))
if not attachments:
attachments = None
conds = dialog.meta_data_filter["manual"]
attachments.extend(meta_filter(metas, conds, dialog.meta_data_filter.get("logic", "and")))
if conds and not attachments:
attachments = ["-999"]
if prompt_config.get("keyword", False):
questions[-1] += keyword_extraction(chat_mdl, questions[-1])
@ -787,8 +788,8 @@ def ask(question, kb_ids, tenant_id, chat_llm_name=None, search_config={}):
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"], meta_data_filter.get("logic", "and")))
if not doc_ids:
doc_ids = None
if meta_data_filter["manual"] and not doc_ids:
doc_ids = ["-999"]
kbinfos = retriever.retrieval(
question=question,
@ -862,8 +863,8 @@ def gen_mindmap(question, kb_ids, tenant_id, search_config={}):
doc_ids = None
elif meta_data_filter.get("method") == "manual":
doc_ids.extend(meta_filter(metas, meta_data_filter["manual"], meta_data_filter.get("logic", "and")))
if not doc_ids:
doc_ids = None
if meta_data_filter["manual"] and not doc_ids:
doc_ids = ["-999"]
ranks = settings.retriever.retrieval(
question=question,

View file

@ -1,38 +1,45 @@
import html
import logging
from collections.abc import Generator
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
from urllib.parse import urlparse
from retry import retry
from common.data_source.config import (
INDEX_BATCH_SIZE,
DocumentSource, NOTION_CONNECTOR_DISABLE_RECURSIVE_PAGE_LOOKUP
NOTION_CONNECTOR_DISABLE_RECURSIVE_PAGE_LOOKUP,
DocumentSource,
)
from common.data_source.exceptions import (
ConnectorMissingCredentialError,
ConnectorValidationError,
CredentialExpiredError,
InsufficientPermissionsError,
UnexpectedValidationError,
)
from common.data_source.interfaces import (
LoadConnector,
PollConnector,
SecondsSinceUnixEpoch
SecondsSinceUnixEpoch,
)
from common.data_source.models import (
Document,
TextSection, GenerateDocumentsOutput
)
from common.data_source.exceptions import (
ConnectorValidationError,
CredentialExpiredError,
InsufficientPermissionsError,
UnexpectedValidationError, ConnectorMissingCredentialError
)
from common.data_source.models import (
NotionPage,
GenerateDocumentsOutput,
NotionBlock,
NotionSearchResponse
NotionPage,
NotionSearchResponse,
TextSection,
)
from common.data_source.utils import (
rl_requests,
batch_generator,
datetime_from_string,
fetch_notion_data,
filter_pages_by_time,
properties_to_str,
filter_pages_by_time, datetime_from_string
rl_requests,
)
@ -61,11 +68,9 @@ class NotionConnector(LoadConnector, PollConnector):
self.recursive_index_enabled = recursive_index_enabled or bool(root_page_id)
@retry(tries=3, delay=1, backoff=2)
def _fetch_child_blocks(
self, block_id: str, cursor: Optional[str] = None
) -> dict[str, Any] | None:
def _fetch_child_blocks(self, block_id: str, cursor: Optional[str] = None) -> dict[str, Any] | None:
"""Fetch all child blocks via the Notion API."""
logging.debug(f"Fetching children of block with ID '{block_id}'")
logging.debug(f"[Notion]: Fetching children of block with ID {block_id}")
block_url = f"https://api.notion.com/v1/blocks/{block_id}/children"
query_params = {"start_cursor": cursor} if cursor else None
@ -79,49 +84,42 @@ class NotionConnector(LoadConnector, PollConnector):
response.raise_for_status()
return response.json()
except Exception as e:
if hasattr(e, 'response') and e.response.status_code == 404:
logging.error(
f"Unable to access block with ID '{block_id}'. "
f"This is likely due to the block not being shared with the integration."
)
if hasattr(e, "response") and e.response.status_code == 404:
logging.error(f"[Notion]: Unable to access block with ID {block_id}. This is likely due to the block not being shared with the integration.")
return None
else:
logging.exception(f"Error fetching blocks: {e}")
logging.exception(f"[Notion]: Error fetching blocks: {e}")
raise
@retry(tries=3, delay=1, backoff=2)
def _fetch_page(self, page_id: str) -> NotionPage:
"""Fetch a page from its ID via the Notion API."""
logging.debug(f"Fetching page for ID '{page_id}'")
logging.debug(f"[Notion]: Fetching page for ID {page_id}")
page_url = f"https://api.notion.com/v1/pages/{page_id}"
try:
data = fetch_notion_data(page_url, self.headers, "GET")
return NotionPage(**data)
except Exception as e:
logging.warning(f"Failed to fetch page, trying database for ID '{page_id}': {e}")
logging.warning(f"[Notion]: Failed to fetch page, trying database for ID {page_id}: {e}")
return self._fetch_database_as_page(page_id)
@retry(tries=3, delay=1, backoff=2)
def _fetch_database_as_page(self, database_id: str) -> NotionPage:
"""Attempt to fetch a database as a page."""
logging.debug(f"Fetching database for ID '{database_id}' as a page")
logging.debug(f"[Notion]: Fetching database for ID {database_id} as a page")
database_url = f"https://api.notion.com/v1/databases/{database_id}"
data = fetch_notion_data(database_url, self.headers, "GET")
database_name = data.get("title")
database_name = (
database_name[0].get("text", {}).get("content") if database_name else None
)
database_name = database_name[0].get("text", {}).get("content") if database_name else None
return NotionPage(**data, database_name=database_name)
@retry(tries=3, delay=1, backoff=2)
def _fetch_database(
self, database_id: str, cursor: Optional[str] = None
) -> dict[str, Any]:
def _fetch_database(self, database_id: str, cursor: Optional[str] = None) -> dict[str, Any]:
"""Fetch a database from its ID via the Notion API."""
logging.debug(f"Fetching database for ID '{database_id}'")
logging.debug(f"[Notion]: Fetching database for ID {database_id}")
block_url = f"https://api.notion.com/v1/databases/{database_id}/query"
body = {"start_cursor": cursor} if cursor else None
@ -129,17 +127,12 @@ class NotionConnector(LoadConnector, PollConnector):
data = fetch_notion_data(block_url, self.headers, "POST", body)
return data
except Exception as e:
if hasattr(e, 'response') and e.response.status_code in [404, 400]:
logging.error(
f"Unable to access database with ID '{database_id}'. "
f"This is likely due to the database not being shared with the integration."
)
if hasattr(e, "response") and e.response.status_code in [404, 400]:
logging.error(f"[Notion]: Unable to access database with ID {database_id}. This is likely due to the database not being shared with the integration.")
return {"results": [], "next_cursor": None}
raise
def _read_pages_from_database(
self, database_id: str
) -> tuple[list[NotionBlock], list[str]]:
def _read_pages_from_database(self, database_id: str) -> tuple[list[NotionBlock], list[str]]:
"""Returns a list of top level blocks and all page IDs in the database."""
result_blocks: list[NotionBlock] = []
result_pages: list[str] = []
@ -158,10 +151,10 @@ class NotionConnector(LoadConnector, PollConnector):
if self.recursive_index_enabled:
if obj_type == "page":
logging.debug(f"Found page with ID '{obj_id}' in database '{database_id}'")
logging.debug(f"[Notion]: Found page with ID {obj_id} in database {database_id}")
result_pages.append(result["id"])
elif obj_type == "database":
logging.debug(f"Found database with ID '{obj_id}' in database '{database_id}'")
logging.debug(f"[Notion]: Found database with ID {obj_id} in database {database_id}")
_, child_pages = self._read_pages_from_database(obj_id)
result_pages.extend(child_pages)
@ -172,44 +165,229 @@ class NotionConnector(LoadConnector, PollConnector):
return result_blocks, result_pages
def _read_blocks(self, base_block_id: str) -> tuple[list[NotionBlock], list[str]]:
"""Reads all child blocks for the specified block, returns blocks and child page ids."""
def _extract_rich_text(self, rich_text_array: list[dict[str, Any]]) -> str:
collected_text: list[str] = []
for rich_text in rich_text_array:
content = ""
r_type = rich_text.get("type")
if r_type == "equation":
expr = rich_text.get("equation", {}).get("expression")
if expr:
content = expr
elif r_type == "mention":
mention = rich_text.get("mention", {}) or {}
mention_type = mention.get("type")
mention_value = mention.get(mention_type, {}) if mention_type else {}
if mention_type == "date":
start = mention_value.get("start")
end = mention_value.get("end")
if start and end:
content = f"{start} - {end}"
elif start:
content = start
elif mention_type in {"page", "database"}:
content = mention_value.get("id", rich_text.get("plain_text", ""))
elif mention_type == "link_preview":
content = mention_value.get("url", rich_text.get("plain_text", ""))
else:
content = rich_text.get("plain_text", "") or str(mention_value)
else:
if rich_text.get("plain_text"):
content = rich_text["plain_text"]
elif "text" in rich_text and rich_text["text"].get("content"):
content = rich_text["text"]["content"]
href = rich_text.get("href")
if content and href:
content = f"{content} ({href})"
if content:
collected_text.append(content)
return "".join(collected_text).strip()
def _build_table_html(self, table_block_id: str) -> str | None:
rows: list[str] = []
cursor = None
while True:
data = self._fetch_child_blocks(table_block_id, cursor)
if data is None:
break
for result in data["results"]:
if result.get("type") != "table_row":
continue
cells_html: list[str] = []
for cell in result["table_row"].get("cells", []):
cell_text = self._extract_rich_text(cell)
cell_html = html.escape(cell_text) if cell_text else ""
cells_html.append(f"<td>{cell_html}</td>")
rows.append(f"<tr>{''.join(cells_html)}</tr>")
if data.get("next_cursor") is None:
break
cursor = data["next_cursor"]
if not rows:
return None
return "<table>\n" + "\n".join(rows) + "\n</table>"
def _download_file(self, url: str) -> bytes | None:
try:
response = rl_requests.get(url, timeout=60)
response.raise_for_status()
return response.content
except Exception as exc:
logging.warning(f"[Notion]: Failed to download Notion file from {url}: {exc}")
return None
def _extract_file_metadata(self, result_obj: dict[str, Any], block_id: str) -> tuple[str | None, str, str | None]:
file_source_type = result_obj.get("type")
file_source = result_obj.get(file_source_type, {}) if file_source_type else {}
url = file_source.get("url")
name = result_obj.get("name") or file_source.get("name")
if url and not name:
parsed_name = Path(urlparse(url).path).name
name = parsed_name or f"notion_file_{block_id}"
elif not name:
name = f"notion_file_{block_id}"
caption = self._extract_rich_text(result_obj.get("caption", [])) if "caption" in result_obj else None
return url, name, caption
def _build_attachment_document(
self,
block_id: str,
url: str,
name: str,
caption: Optional[str],
page_last_edited_time: Optional[str],
) -> Document | None:
file_bytes = self._download_file(url)
if file_bytes is None:
return None
extension = Path(name).suffix or Path(urlparse(url).path).suffix or ".bin"
if extension and not extension.startswith("."):
extension = f".{extension}"
if not extension:
extension = ".bin"
updated_at = datetime_from_string(page_last_edited_time) if page_last_edited_time else datetime.now(timezone.utc)
semantic_identifier = caption or name or f"Notion file {block_id}"
return Document(
id=block_id,
blob=file_bytes,
source=DocumentSource.NOTION,
semantic_identifier=semantic_identifier,
extension=extension,
size_bytes=len(file_bytes),
doc_updated_at=updated_at,
)
def _read_blocks(self, base_block_id: str, page_last_edited_time: Optional[str] = None) -> tuple[list[NotionBlock], list[str], list[Document]]:
result_blocks: list[NotionBlock] = []
child_pages: list[str] = []
attachments: list[Document] = []
cursor = None
while True:
data = self._fetch_child_blocks(base_block_id, cursor)
if data is None:
return result_blocks, child_pages
return result_blocks, child_pages, attachments
for result in data["results"]:
logging.debug(f"Found child block for block with ID '{base_block_id}': {result}")
logging.debug(f"[Notion]: Found child block for block with ID {base_block_id}: {result}")
result_block_id = result["id"]
result_type = result["type"]
result_obj = result[result_type]
if result_type in ["ai_block", "unsupported", "external_object_instance_page"]:
logging.warning(f"Skipping unsupported block type '{result_type}'")
logging.warning(f"[Notion]: Skipping unsupported block type {result_type}")
continue
if result_type == "table":
table_html = self._build_table_html(result_block_id)
if table_html:
result_blocks.append(
NotionBlock(
id=result_block_id,
text=table_html,
prefix="\n\n",
)
)
continue
if result_type == "equation":
expr = result_obj.get("expression")
if expr:
result_blocks.append(
NotionBlock(
id=result_block_id,
text=expr,
prefix="\n",
)
)
continue
cur_result_text_arr = []
if "rich_text" in result_obj:
for rich_text in result_obj["rich_text"]:
if "text" in rich_text:
text = rich_text["text"]["content"]
cur_result_text_arr.append(text)
text = self._extract_rich_text(result_obj["rich_text"])
if text:
cur_result_text_arr.append(text)
if result_type == "bulleted_list_item":
if cur_result_text_arr:
cur_result_text_arr[0] = f"- {cur_result_text_arr[0]}"
else:
cur_result_text_arr = ["- "]
if result_type == "numbered_list_item":
if cur_result_text_arr:
cur_result_text_arr[0] = f"1. {cur_result_text_arr[0]}"
else:
cur_result_text_arr = ["1. "]
if result_type == "to_do":
checked = result_obj.get("checked")
checkbox_prefix = "[x]" if checked else "[ ]"
if cur_result_text_arr:
cur_result_text_arr = [f"{checkbox_prefix} {cur_result_text_arr[0]}"] + cur_result_text_arr[1:]
else:
cur_result_text_arr = [checkbox_prefix]
if result_type in {"file", "image", "pdf", "video", "audio"}:
file_url, file_name, caption = self._extract_file_metadata(result_obj, result_block_id)
if file_url:
attachment_doc = self._build_attachment_document(
block_id=result_block_id,
url=file_url,
name=file_name,
caption=caption,
page_last_edited_time=page_last_edited_time,
)
if attachment_doc:
attachments.append(attachment_doc)
attachment_label = caption or file_name
if attachment_label:
cur_result_text_arr.append(f"{result_type.capitalize()}: {attachment_label}")
if result["has_children"]:
if result_type == "child_page":
child_pages.append(result_block_id)
else:
logging.debug(f"Entering sub-block: {result_block_id}")
subblocks, subblock_child_pages = self._read_blocks(result_block_id)
logging.debug(f"Finished sub-block: {result_block_id}")
logging.debug(f"[Notion]: Entering sub-block: {result_block_id}")
subblocks, subblock_child_pages, subblock_attachments = self._read_blocks(result_block_id, page_last_edited_time)
logging.debug(f"[Notion]: Finished sub-block: {result_block_id}")
result_blocks.extend(subblocks)
child_pages.extend(subblock_child_pages)
attachments.extend(subblock_attachments)
if result_type == "child_database":
inner_blocks, inner_child_pages = self._read_pages_from_database(result_block_id)
@ -231,7 +409,7 @@ class NotionConnector(LoadConnector, PollConnector):
cursor = data["next_cursor"]
return result_blocks, child_pages
return result_blocks, child_pages, attachments
def _read_page_title(self, page: NotionPage) -> Optional[str]:
"""Extracts the title from a Notion page."""
@ -245,9 +423,7 @@ class NotionConnector(LoadConnector, PollConnector):
return None
def _read_pages(
self, pages: list[NotionPage]
) -> Generator[Document, None, None]:
def _read_pages(self, pages: list[NotionPage], start: SecondsSinceUnixEpoch | None = None, end: SecondsSinceUnixEpoch | None = None) -> Generator[Document, None, None]:
"""Reads pages for rich text content and generates Documents."""
all_child_page_ids: list[str] = []
@ -255,11 +431,17 @@ class NotionConnector(LoadConnector, PollConnector):
if isinstance(page, dict):
page = NotionPage(**page)
if page.id in self.indexed_pages:
logging.debug(f"Already indexed page with ID '{page.id}'. Skipping.")
logging.debug(f"[Notion]: Already indexed page with ID {page.id}. Skipping.")
continue
logging.info(f"Reading page with ID '{page.id}', with url {page.url}")
page_blocks, child_page_ids = self._read_blocks(page.id)
if start is not None and end is not None:
page_ts = datetime_from_string(page.last_edited_time).timestamp()
if not (page_ts > start and page_ts <= end):
logging.debug(f"[Notion]: Skipping page {page.id} outside polling window.")
continue
logging.info(f"[Notion]: Reading page with ID {page.id}, with url {page.url}")
page_blocks, child_page_ids, attachment_docs = self._read_blocks(page.id, page.last_edited_time)
all_child_page_ids.extend(child_page_ids)
self.indexed_pages.add(page.id)
@ -268,14 +450,12 @@ class NotionConnector(LoadConnector, PollConnector):
if not page_blocks:
if not raw_page_title:
logging.warning(f"No blocks OR title found for page with ID '{page.id}'. Skipping.")
logging.warning(f"[Notion]: No blocks OR title found for page with ID {page.id}. Skipping.")
continue
text = page_title
if page.properties:
text += "\n\n" + "\n".join(
[f"{key}: {value}" for key, value in page.properties.items()]
)
text += "\n\n" + "\n".join([f"{key}: {value}" for key, value in page.properties.items()])
sections = [TextSection(link=page.url, text=text)]
else:
sections = [
@ -286,45 +466,39 @@ class NotionConnector(LoadConnector, PollConnector):
for block in page_blocks
]
blob = ("\n".join([sec.text for sec in sections])).encode("utf-8")
joined_text = "\n".join(sec.text for sec in sections)
blob = joined_text.encode("utf-8")
yield Document(
id=page.id,
blob=blob,
source=DocumentSource.NOTION,
semantic_identifier=page_title,
extension=".txt",
size_bytes=len(blob),
doc_updated_at=datetime_from_string(page.last_edited_time)
id=page.id, blob=blob, source=DocumentSource.NOTION, semantic_identifier=page_title, extension=".txt", size_bytes=len(blob), doc_updated_at=datetime_from_string(page.last_edited_time)
)
for attachment_doc in attachment_docs:
yield attachment_doc
if self.recursive_index_enabled and all_child_page_ids:
for child_page_batch_ids in batch_generator(all_child_page_ids, INDEX_BATCH_SIZE):
child_page_batch = [
self._fetch_page(page_id)
for page_id in child_page_batch_ids
if page_id not in self.indexed_pages
]
yield from self._read_pages(child_page_batch)
child_page_batch = [self._fetch_page(page_id) for page_id in child_page_batch_ids if page_id not in self.indexed_pages]
yield from self._read_pages(child_page_batch, start, end)
@retry(tries=3, delay=1, backoff=2)
def _search_notion(self, query_dict: dict[str, Any]) -> NotionSearchResponse:
"""Search for pages from a Notion database."""
logging.debug(f"Searching for pages in Notion with query_dict: {query_dict}")
logging.debug(f"[Notion]: Searching for pages in Notion with query_dict: {query_dict}")
data = fetch_notion_data("https://api.notion.com/v1/search", self.headers, "POST", query_dict)
return NotionSearchResponse(**data)
def _recursive_load(self) -> Generator[list[Document], None, None]:
def _recursive_load(self, start: SecondsSinceUnixEpoch | None = None, end: SecondsSinceUnixEpoch | None = None) -> Generator[list[Document], None, None]:
"""Recursively load pages starting from root page ID."""
if self.root_page_id is None or not self.recursive_index_enabled:
raise RuntimeError("Recursive page lookup is not enabled")
logging.info(f"Recursively loading pages from Notion based on root page with ID: {self.root_page_id}")
logging.info(f"[Notion]: Recursively loading pages from Notion based on root page with ID: {self.root_page_id}")
pages = [self._fetch_page(page_id=self.root_page_id)]
yield from batch_generator(self._read_pages(pages), self.batch_size)
yield from batch_generator(self._read_pages(pages, start, end), self.batch_size)
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
"""Applies integration token to headers."""
self.headers["Authorization"] = f'Bearer {credentials["notion_integration_token"]}'
self.headers["Authorization"] = f"Bearer {credentials['notion_integration_token']}"
return None
def load_from_state(self) -> GenerateDocumentsOutput:
@ -348,12 +522,10 @@ class NotionConnector(LoadConnector, PollConnector):
else:
break
def poll_source(
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
) -> GenerateDocumentsOutput:
def poll_source(self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch) -> GenerateDocumentsOutput:
"""Poll Notion for updated pages within a time period."""
if self.recursive_index_enabled and self.root_page_id:
yield from self._recursive_load()
yield from self._recursive_load(start, end)
return
query_dict = {
@ -367,7 +539,7 @@ class NotionConnector(LoadConnector, PollConnector):
pages = filter_pages_by_time(db_res.results, start, end, "last_edited_time")
if pages:
yield from batch_generator(self._read_pages(pages), self.batch_size)
yield from batch_generator(self._read_pages(pages, start, end), self.batch_size)
if db_res.has_more:
query_dict["start_cursor"] = db_res.next_cursor
else:

View file

@ -1091,7 +1091,7 @@ class RAGFlowPdfParser:
logging.debug("Images converted.")
self.is_english = [
re.search(r"[a-zA-Z0-9,/¸;:'\[\]\(\)!@#$%^&*\"?<>._-]{30,}", "".join(random.choices([c["text"] for c in self.page_chars[i]], k=min(100, len(self.page_chars[i])))))
re.search(r"[ a-zA-Z0-9,/¸;:'\[\]\(\)!@#$%^&*\"?<>._-]{30,}", "".join(random.choices([c["text"] for c in self.page_chars[i]], k=min(100, len(self.page_chars[i])))))
for i in range(len(self.page_chars))
]
if sum([1 if e else 0 for e in self.is_english]) > len(self.page_images) / 2:
@ -1148,7 +1148,7 @@ class RAGFlowPdfParser:
if not self.is_english and not any([c for c in self.page_chars]) and self.boxes:
bxes = [b for bxs in self.boxes for b in bxs]
self.is_english = re.search(r"[\na-zA-Z0-9,/¸;:'\[\]\(\)!@#$%^&*\"?<>._-]{30,}", "".join([b["text"] for b in random.choices(bxes, k=min(30, len(bxes)))]))
self.is_english = re.search(r"[ \na-zA-Z0-9,/¸;:'\[\]\(\)!@#$%^&*\"?<>._-]{30,}", "".join([b["text"] for b in random.choices(bxes, k=min(30, len(bxes)))]))
logging.debug(f"Is it English: {self.is_english}")

View file

@ -2072,6 +2072,7 @@ Retrieves chunks from specified datasets.
- `"cross_languages"`: `list[string]`
- `"metadata_condition"`: `object`
- `"use_kg"`: `boolean`
- `"toc_enhance"`: `boolean`
##### Request example
```bash
@ -2122,6 +2123,8 @@ curl --request POST \
The number of chunks engaged in vector cosine computation. Defaults to `1024`.
- `"use_kg"`: (*Body parameter*), `boolean`
The search includes text chunks related to the knowledge graph of the selected dataset to handle complex multi-hop queries. Defaults to `False`.
- `"toc_enhance"`: (*Body parameter*), `boolean`
The search includes table of content enhancement in order to boost rank of relevant chunks. Files parsed with `TOC Enhance` enabled is prerequisite. Defaults to `False`.
- `"rerank_id"`: (*Body parameter*), `integer`
The ID of the rerank model.
- `"keyword"`: (*Body parameter*), `boolean`
@ -2136,6 +2139,9 @@ curl --request POST \
The languages that should be translated into, in order to achieve keywords retrievals in different languages.
- `"metadata_condition"`: (*Body parameter*), `object`
The metadata condition used for filtering chunks:
- `"logic"`: (*Body parameter*), `string`
- `"and"` Intersection of the result from each condition (default).
- `"or"` union of the result from each condition.
- `"conditions"`: (*Body parameter*), `array`
A list of metadata filter conditions.
- `"name"`: `string` - The metadata field name to filter by, e.g., `"author"`, `"company"`, `"url"`. Ensure this parameter before use. See [Set metadata](../guides/dataset/set_metadata.md) for details.

View file

@ -113,6 +113,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
lang = lang,
callback = callback,
pdf_cls = Pdf,
layout_recognizer = layout_recognizer,
**kwargs
)

View file

@ -172,6 +172,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
lang = lang,
callback = callback,
pdf_cls = Pdf,
layout_recognizer = layout_recognizer,
**kwargs
)

View file

@ -213,6 +213,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
lang = lang,
callback = callback,
pdf_cls = Pdf,
layout_recognizer = layout_recognizer,
parse_method = "manual",
**kwargs
)

View file

@ -99,6 +99,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
lang = lang,
callback = callback,
pdf_cls = Pdf,
layout_recognizer = layout_recognizer,
**kwargs
)

View file

@ -142,6 +142,7 @@ def chunk(filename, binary=None, from_page=0, to_page=100000,
lang = lang,
callback = callback,
pdf_cls = Pdf,
layout_recognizer = layout_recognizer,
**kwargs
)

View file

@ -437,16 +437,16 @@ def not_title(txt):
return re.search(r"[,;,。;!!]", txt)
def tree_merge(bull, sections, depth):
if not sections or bull < 0:
return sections
if isinstance(sections[0], type("")):
sections = [(s, "") for s in sections]
# filter out position information in pdf sections
sections = [(t, o) for t, o in sections if
t and len(t.split("@")[0].strip()) > 1 and not re.match(r"[0-9]+$", t.split("@")[0].strip())]
def get_level(bull, section):
text, layout = section
text = re.sub(r"\u3000", " ", text).strip()
@ -465,7 +465,7 @@ def tree_merge(bull, sections, depth):
level, text = get_level(bull, section)
if not text.strip("\n"):
continue
lines.append((level, text))
level_set.add(level)
@ -608,6 +608,26 @@ def naive_merge(sections: str | list, chunk_token_num=128, delimiter="\n。
cks[-1] += t
tk_nums[-1] += tnum
custom_delimiters = [m.group(1) for m in re.finditer(r"`([^`]+)`", delimiter)]
has_custom = bool(custom_delimiters)
if has_custom:
custom_pattern = "|".join(re.escape(t) for t in sorted(set(custom_delimiters), key=len, reverse=True))
cks, tk_nums = [], []
for sec, pos in sections:
split_sec = re.split(r"(%s)" % custom_pattern, sec, flags=re.DOTALL)
for sub_sec in split_sec:
if re.fullmatch(custom_pattern, sub_sec or ""):
continue
text = "\n" + sub_sec
local_pos = pos
if num_tokens_from_string(text) < 8:
local_pos = ""
if local_pos and text.find(local_pos) < 0:
text += local_pos
cks.append(text)
tk_nums.append(num_tokens_from_string(text))
return cks
dels = get_delimiters(delimiter)
for sec, pos in sections:
if num_tokens_from_string(sec) < chunk_token_num:
@ -657,6 +677,29 @@ def naive_merge_with_images(texts, images, chunk_token_num=128, delimiter="\n。
result_images[-1] = concat_img(result_images[-1], image)
tk_nums[-1] += tnum
custom_delimiters = [m.group(1) for m in re.finditer(r"`([^`]+)`", delimiter)]
has_custom = bool(custom_delimiters)
if has_custom:
custom_pattern = "|".join(re.escape(t) for t in sorted(set(custom_delimiters), key=len, reverse=True))
cks, result_images, tk_nums = [], [], []
for text, image in zip(texts, images):
text_str = text[0] if isinstance(text, tuple) else text
text_pos = text[1] if isinstance(text, tuple) and len(text) > 1 else ""
split_sec = re.split(r"(%s)" % custom_pattern, text_str)
for sub_sec in split_sec:
if re.fullmatch(custom_pattern, sub_sec or ""):
continue
text_seg = "\n" + sub_sec
local_pos = text_pos
if num_tokens_from_string(text_seg) < 8:
local_pos = ""
if local_pos and text_seg.find(local_pos) < 0:
text_seg += local_pos
cks.append(text_seg)
result_images.append(image)
tk_nums.append(num_tokens_from_string(text_seg))
return cks, result_images
dels = get_delimiters(delimiter)
for text, image in zip(texts, images):
# if text is tuple, unpack it
@ -748,6 +791,23 @@ def naive_merge_docx(sections, chunk_token_num=128, delimiter="\n。"):
images[-1] = concat_img(images[-1], image)
tk_nums[-1] += tnum
custom_delimiters = [m.group(1) for m in re.finditer(r"`([^`]+)`", delimiter)]
has_custom = bool(custom_delimiters)
if has_custom:
custom_pattern = "|".join(re.escape(t) for t in sorted(set(custom_delimiters), key=len, reverse=True))
cks, images, tk_nums = [], [], []
pattern = r"(%s)" % custom_pattern
for sec, image in sections:
split_sec = re.split(pattern, sec)
for sub_sec in split_sec:
if not sub_sec or re.fullmatch(custom_pattern, sub_sec):
continue
text_seg = "\n" + sub_sec
cks.append(text_seg)
images.append(image)
tk_nums.append(num_tokens_from_string(text_seg))
return cks, images
dels = get_delimiters(delimiter)
pattern = r"(%s)" % dels
@ -789,7 +849,7 @@ class Node:
self.level = level
self.depth = depth
self.texts = texts or []
self.children = []
self.children = []
def add_child(self, child_node):
self.children.append(child_node)
@ -835,7 +895,7 @@ class Node:
return self
def get_tree(self):
tree_list = []
tree_list = []
self._dfs(self, tree_list, [])
return tree_list
@ -860,7 +920,7 @@ class Node:
# A leaf title within depth emits its title path as a chunk (header-only section)
elif not child and (1 <= level <= self.depth):
tree_list.append("\n".join(path_titles))
# Recurse into children with the updated title path
for c in child:
self._dfs(c, tree_list, path_titles)
self._dfs(c, tree_list, path_titles)

View file

@ -29,7 +29,10 @@ const BackButton: React.FC<BackButtonProps> = ({
return (
<Button
variant="ghost"
className={cn('gap-2 bg-bg-card border border-border-default', className)}
className={cn(
'gap-2 bg-bg-card border border-border-default hover:bg-border-button hover:text-text-primary',
className,
)}
onClick={handleClick}
{...props}
>

View file

@ -44,6 +44,7 @@ export function ConfirmDeleteDialog({
<AlertDialogContent
onSelect={(e) => e.preventDefault()}
onClick={(e) => e.stopPropagation()}
className="bg-bg-base"
>
<AlertDialogHeader>
<AlertDialogTitle>
@ -59,7 +60,7 @@ export function ConfirmDeleteDialog({
{t('common.no')}
</AlertDialogCancel>
<AlertDialogAction
className="bg-state-error text-text-primary"
className="bg-state-error text-text-primary hover:text-text-primary hover:bg-state-error"
onClick={onOk}
>
{t('common.yes')}

View file

@ -68,6 +68,7 @@ export interface FormFieldConfig {
dependencies?: string[];
schema?: ZodSchema;
shouldRender?: (formValues: any) => boolean;
labelClassName?: string;
}
// Component props interface
@ -81,6 +82,7 @@ interface DynamicFormProps<T extends FieldValues> {
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => void;
labelClassName?: string;
}
// Form ref interface
@ -295,6 +297,7 @@ const DynamicForm = {
children,
defaultValues: formDefaultValues = {} as DefaultValues<T>,
onFieldUpdate,
labelClassName,
}: DynamicFormProps<T>,
ref: React.Ref<any>,
) => {
@ -353,6 +356,13 @@ const DynamicForm = {
...combinedErrors,
...fieldErrors,
} as any;
console.log('combinedErrors', combinedErrors);
for (const key in combinedErrors) {
if (Array.isArray(combinedErrors[key])) {
combinedErrors[key] = combinedErrors[key][0];
}
}
console.log('combinedErrors', combinedErrors);
return {
values: Object.keys(combinedErrors).length ? {} : data,
@ -456,6 +466,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -481,6 +492,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -511,6 +523,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -551,7 +564,10 @@ const DynamicForm = {
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel
className="font-normal"
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
@ -564,7 +580,10 @@ const DynamicForm = {
{field.label && field.horizontal && (
<div className="space-y-1 leading-none w-1/4">
<FormLabel
className="font-normal"
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
@ -600,6 +619,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -629,6 +649,7 @@ const DynamicForm = {
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
@ -706,9 +727,7 @@ const DynamicForm = {
type="button"
disabled={submitLoading}
onClick={() => {
console.log('form submit');
(async () => {
console.log('form submit2');
try {
let beValid = await form.formControl.trigger();
console.log('form valid', beValid, form, form.formControl);
@ -748,7 +767,7 @@ const DynamicForm = {
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
className="px-2 py-1 border border-border-button rounded-md text-text-secondary hover:bg-bg-card hover:text-primary"
>
{cancelText ?? t('modal.cancelText')}
</button>

View file

@ -102,8 +102,8 @@ const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
{Array.isArray(tagChild) && tagChild.length > 0 && <>{tagChild}</>}
{!inputVisible && (
<Button
variant="dashed"
className="w-fit flex items-center justify-center gap-2 bg-bg-card"
variant="ghost"
className="w-fit flex items-center justify-center gap-2 bg-bg-card border-dashed border"
onClick={showInput}
style={tagPlusStyle}
>

View file

@ -272,7 +272,7 @@ export function FileUploader(props: FileUploaderProps) {
<div
{...getRootProps()}
className={cn(
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-border-default px-5 py-2.5 text-center transition hover:bg-muted/25 bg-accent-primary-5',
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border border-dashed border-border-default px-5 py-2.5 text-center transition hover:bg-border-button bg-bg-card',
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
isDragActive && 'border-muted-foreground/50',
isDisabled && 'pointer-events-none opacity-60',
@ -285,11 +285,11 @@ export function FileUploader(props: FileUploaderProps) {
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload
className="size-7 text-muted-foreground"
className="size-7 text-text-secondary"
aria-hidden="true"
/>
</div>
<p className="font-medium text-muted-foreground">
<p className="font-medium text-text-secondary">
Drop the files here
</p>
</div>
@ -297,15 +297,15 @@ export function FileUploader(props: FileUploaderProps) {
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload
className="size-7 text-muted-foreground"
className="size-7 text-text-secondary"
aria-hidden="true"
/>
</div>
<div className="flex flex-col gap-px">
<p className="font-medium text-muted-foreground">
<p className="font-medium text-text-secondary">
{t('knowledgeDetails.uploadTitle')}
</p>
<p className="text-sm text-text-secondary">
<p className="text-sm text-text-disabled">
{description || t('knowledgeDetails.uploadDescription')}
{/* You can upload
{maxFileCount > 1

View file

@ -1,6 +1,9 @@
import { ModelVariableType } from '@/constants/knowledge';
import {
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { camelCase } from 'lodash';
import { camelCase, isEqual } from 'lodash';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
@ -25,6 +28,13 @@ import { useHandleFreedomChange } from './use-watch-change';
interface LlmSettingFieldItemsProps {
prefix?: string;
options?: any[];
showFields?: Array<
| 'temperature'
| 'top_p'
| 'presence_penalty'
| 'frequency_penalty'
| 'max_tokens'
>;
}
export const LLMIdFormField = {
@ -56,6 +66,13 @@ export const LlmSettingSchema = {
export function LlmSettingFieldItems({
prefix,
options,
showFields = [
'temperature',
'top_p',
'presence_penalty',
'frequency_penalty',
'max_tokens',
],
}: LlmSettingFieldItemsProps) {
const form = useFormContext();
const { t } = useTranslate('chat');
@ -72,14 +89,53 @@ export function LlmSettingFieldItems({
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: t(camelCase(x)),
value: x,
}));
})) as { label: string; value: ModelVariableType | 'Custom' }[];
parameterOptions.push({
label: t(camelCase('Custom')),
value: 'Custom',
});
const checkParameterIsEqual = () => {
const [
parameter,
topPValue,
frequencyPenaltyValue,
temperatureValue,
presencePenaltyValue,
maxTokensValue,
] = form.getValues([
getFieldWithPrefix('parameter'),
getFieldWithPrefix('temperature'),
getFieldWithPrefix('top_p'),
getFieldWithPrefix('frequency_penalty'),
getFieldWithPrefix('presence_penalty'),
getFieldWithPrefix('max_tokens'),
]);
if (parameter && parameter !== 'Custom') {
const parameterValue =
settledModelVariableMap[parameter as keyof typeof ModelVariableType];
const parameterRealValue = {
top_p: topPValue,
temperature: temperatureValue,
frequency_penalty: frequencyPenaltyValue,
presence_penalty: presencePenaltyValue,
max_tokens: maxTokensValue,
};
if (!isEqual(parameterValue, parameterRealValue)) {
form.setValue(getFieldWithPrefix('parameter'), 'Custom');
}
}
};
return (
<div className="space-y-5">
<LLMFormField options={options}></LLMFormField>
<LLMFormField
options={options}
name={getFieldWithPrefix('llm_id')}
></LLMFormField>
<FormField
control={form.control}
name={'parameter'}
name={getFieldWithPrefix('parameter')}
render={({ field }) => (
<FormItem className="flex justify-between items-center">
<FormLabel className="flex-1">{t('freedom')}</FormLabel>
@ -107,45 +163,71 @@ export function LlmSettingFieldItems({
</FormItem>
)}
/>
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
label="temperature"
max={1}
step={0.01}
min={0}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
label="topP"
max={1}
step={0.01}
min={0}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
label="presencePenalty"
max={1}
step={0.01}
min={0}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
label="frequencyPenalty"
max={1}
step={0.01}
min={0}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
label="maxTokens"
max={128000}
min={0}
></SliderInputSwitchFormField>
{showFields.some((item) => item === 'temperature') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
label="temperature"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'top_p') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
label="topP"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'presence_penalty') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
label="presencePenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'frequency_penalty') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
label="frequencyPenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'max_tokens') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
numberInputClassName="w-20"
label="maxTokens"
max={128000}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
</div>
);
}

View file

@ -22,6 +22,7 @@ type SliderInputSwitchFormFieldProps = {
onChange?: (value: number) => void;
className?: string;
checkName: string;
numberInputClassName?: string;
};
export function SliderInputSwitchFormField({
@ -34,6 +35,7 @@ export function SliderInputSwitchFormField({
onChange,
className,
checkName,
numberInputClassName,
}: SliderInputSwitchFormFieldProps) {
const form = useFormContext();
const disabled = !form.watch(checkName);
@ -81,7 +83,10 @@ export function SliderInputSwitchFormField({
<FormControl>
<NumberInput
disabled={disabled}
className="h-7 w-20"
className={cn(
'h-6 w-10 p-1 border border-border-button rounded-sm',
numberInputClassName,
)}
max={max}
min={min}
step={step}

View file

@ -14,6 +14,7 @@ type MetadataFilterProps = {
export const MetadataFilterSchema = {
meta_data_filter: z
.object({
logic: z.string().optional(),
method: z.string().optional(),
manual: z
.array(

View file

@ -15,14 +15,17 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { SwitchOperatorOptions } from '@/constants/agent';
import { SwitchLogicOperator, SwitchOperatorOptions } from '@/constants/agent';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
import { Plus, X } from 'lucide-react';
import { useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { RAGFlowFormItem } from '../ragflow-form';
import { RAGFlowSelect } from '../ui/select';
export function MetadataFilterConditions({
kbIds,
@ -36,10 +39,13 @@ export function MetadataFilterConditions({
const { t } = useTranslation();
const form = useFormContext();
const name = prefix + 'meta_data_filter.manual';
const logic = prefix + 'meta_data_filter.logic';
const metadata = useFetchKnowledgeMetadata(kbIds);
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
const { fields, remove, append } = useFieldArray({
name,
control: form.control,
@ -47,13 +53,14 @@ export function MetadataFilterConditions({
const add = useCallback(
(key: string) => () => {
form.setValue(logic, SwitchLogicOperator.And);
append({
key,
value: '',
op: SwitchOperatorOptions[0].value,
});
},
[append],
[append, form, logic],
);
return (
@ -77,73 +84,92 @@ export function MetadataFilterConditions({
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-5">
{fields.map((field, index) => {
const typeField = `${name}.${index}.key`;
return (
<div key={field.id} className="flex w-full items-center gap-2">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.op`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<SelectWithSearch
{...field}
options={switchOperatorOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
{canReference ? (
<PromptEditor
{...field}
multiLine={false}
showToolbar={false}
></PromptEditor>
) : (
<Input
placeholder={t('common.pleaseInput')}
{...field}
/>
<section className="flex">
{fields.length > 1 && (
<div className="relative min-w-14">
<RAGFlowFormItem
name={logic}
className="absolute top-1/2 -translate-y-1/2 right-1 left-0 z-10 bg-bg-base"
>
<RAGFlowSelect
options={switchLogicOperatorOptions}
triggerClassName="w-full text-xs px-1 py-0 h-6"
></RAGFlowSelect>
</RAGFlowFormItem>
<div className="absolute border-l border-y w-5 right-0 top-4 bottom-4 rounded-l-lg"></div>
</div>
)}
<div className="space-y-5 flex-1">
{fields.map((field, index) => {
const typeField = `${name}.${index}.key`;
return (
<section key={field.id} className="flex gap-2">
<div className="w-full space-y-2">
<div className="flex items-center gap-1">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
</div>
/>
<Separator className="w-1 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.op`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<SelectWithSearch
{...field}
options={switchOperatorOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
{canReference ? (
<PromptEditor
{...field}
multiLine={false}
showToolbar={false}
></PromptEditor>
) : (
<Input
placeholder={t('common.pleaseInput')}
{...field}
/>
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</section>
);
})}
</div>
</section>
</section>
);
}

View file

@ -32,13 +32,13 @@ const Input = function ({
type={type}
data-slot="input"
className={cn(
'border-input file:text-foreground placeholder:text-muted-foreground/70 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'border-border-button file:text-foreground placeholder:text-text-disabled flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
'focus-visible:border-border-button focus-visible:ring-text-primary/50 focus-visible:ring-1',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
type === 'search' &&
'[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none',
type === 'file' &&
'text-muted-foreground/70 file:border-input file:text-foreground p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic',
'text-text-disabled file:border-input file:text-foreground p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic',
icon && iconPosition === 'left' && 'pl-7',
icon && iconPosition === 'right' && 'pr-7',
className,

View file

@ -24,20 +24,6 @@ export default React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
/>
<button
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={toggleVisibility}
aria-label={isVisible ? 'Hide password' : 'Show password'}
aria-pressed={isVisible}
aria-controls="password"
>
{/* {isVisible ? (
<EyeOffIcon size={16} aria-hidden="true" />
) : (
<EyeIcon size={16} aria-hidden="true" />
)} */}
</button>
</div>
</div>
);

View file

@ -140,7 +140,7 @@ export const SelectWithSearch = forwardRef<
ref={ref}
disabled={disabled}
className={cn(
'!bg-bg-input hover:bg-background border-border-button 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-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto group',
triggerClassName,
)}
>
@ -155,12 +155,12 @@ export const SelectWithSearch = forwardRef<
{value && allowClear && (
<>
<XIcon
className="h-4 mx-2 cursor-pointer text-text-disabled"
className="h-4 mx-2 cursor-pointer text-text-disabled hidden group-hover:block"
onClick={handleClear}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
className=" min-h-6 h-full hidden group-hover:flex"
/>
</>
)}
@ -173,12 +173,17 @@ export const SelectWithSearch = forwardRef<
</Button>
</PopoverTrigger>
<PopoverContent
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
className="border-border-button w-full min-w-[var(--radix-popper-anchor-width)] p-0"
align="start"
>
<Command>
<CommandInput placeholder={t('common.search') + '...'} />
<CommandList>
<Command className="p-5">
{options && options.length > 0 && (
<CommandInput
placeholder={t('common.search') + '...'}
className=" placeholder:text-text-disabled"
/>
)}
<CommandList className="mt-2">
<CommandEmpty>{t('common.noDataFound')}</CommandEmpty>
{options.map((group, idx) => {
if (group.options) {
@ -209,6 +214,7 @@ export const SelectWithSearch = forwardRef<
value={group.value}
disabled={group.disabled}
onSelect={handleSelect}
className="min-h-10"
>
<span className="leading-none">{group.label}</span>

View file

@ -221,10 +221,12 @@ const RaptorFormFields = ({
defaultValue={0}
type="number"
suffix={
<Shuffle
className="size-3.5 cursor-pointer"
onClick={handleGenerate}
/>
<div className="w-7 flex justify-center items-center">
<Shuffle
className="size-3.5 cursor-pointer"
onClick={handleGenerate}
/>
</div>
}
/>
</FormControl>

View file

@ -59,6 +59,7 @@ interface SimilaritySliderFormFieldProps {
similarityName?: string;
vectorSimilarityWeightName?: string;
isTooltipShown?: boolean;
numberInputClassName?: string;
}
export const initialSimilarityThresholdValue = {
@ -86,6 +87,7 @@ export function SimilaritySliderFormField({
similarityName = 'similarity_threshold',
vectorSimilarityWeightName = 'vector_similarity_weight',
isTooltipShown,
numberInputClassName,
}: SimilaritySliderFormFieldProps) {
const { t } = useTranslate('knowledgeDetails');
const form = useFormContext();
@ -101,6 +103,7 @@ export function SimilaritySliderFormField({
step={0.01}
layout={FormLayout.Vertical}
tooltip={isTooltipShown && t('similarityThresholdTip')}
numberInputClassName={numberInputClassName}
></SliderInputFormField>
<FormField
control={form.control}
@ -124,7 +127,7 @@ export function SimilaritySliderFormField({
isVector ? 'vectorSimilarityWeight' : 'keywordSimilarityWeight',
)}
</FormLabel>
<div className={cn('flex items-end gap-14 justify-between')}>
<div className={cn('flex items-end gap-4 justify-between')}>
<FormControl>
<div className="flex flex-col flex-1 gap-2">
<div className="flex justify-between items-center">
@ -158,6 +161,7 @@ export function SimilaritySliderFormField({
className={cn(
'h-6 w-10 p-0 text-center bg-bg-input border-border-default border text-text-secondary',
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
numberInputClassName,
)}
max={1}
min={0}

View file

@ -25,6 +25,7 @@ type SliderInputFormFieldProps = {
tooltip?: ReactNode;
defaultValue?: number;
className?: string;
numberInputClassName?: string;
} & FormLayoutType;
export function SliderInputFormField({
@ -36,6 +37,7 @@ export function SliderInputFormField({
tooltip,
defaultValue,
className,
numberInputClassName,
layout = FormLayout.Horizontal,
}: SliderInputFormFieldProps) {
const form = useFormContext();
@ -61,7 +63,7 @@ export function SliderInputFormField({
</FormLabel>
<div
className={cn(
'flex items-center gap-14 justify-between',
'flex items-center gap-4 justify-between',
{ 'w-3/4': isHorizontal },
className,
)}
@ -82,6 +84,7 @@ export function SliderInputFormField({
className={cn(
'h-6 w-10 p-0 text-center bg-bg-input border border-border-default text-text-secondary',
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
numberInputClassName,
)}
max={max}
min={min}

View file

@ -39,12 +39,15 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" data-cmdk-input-wrapper="">
<div
className="flex items-center border rounded-md border-border-default bg-bg-input px-3"
data-cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
'flex min-h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-text-secondary disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
@ -66,7 +69,10 @@ const CommandList = React.forwardRef<
*/
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
className={cn(
'max-h-[300px] overflow-y-auto overflow-x-hidden scrollbar-auto',
className,
)}
onWheel={(e) => e.stopPropagation()}
onMouseEnter={(e) => e.currentTarget.focus()}
tabIndex={-1}
@ -96,7 +102,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-text-secondary',
className,
)}
{...props}

View file

@ -17,7 +17,7 @@ const Divider: React.FC<DividerProps> = ({
direction = 'horizontal',
type = 'horizontal',
text,
color = 'border-muted-foreground/50',
color = 'border-border-button',
margin = 'my-4',
className = '',
}) => {

View file

@ -2,8 +2,7 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
import { Eye, EyeOff, Search } from 'lucide-react';
import { useState } from 'react';
import { Button } from './button';
import { useEffect, useMemo, useRef, useState } from 'react';
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
@ -18,6 +17,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
const { defaultValue, ...restProps } = props;
const inputValue = isControlled ? value : defaultValue;
const [showPassword, setShowPassword] = useState(false);
const [prefixWidth, setPrefixWidth] = useState(0);
const [suffixWidth, setSuffixWidth] = useState(0);
const prefixRef = useRef<HTMLSpanElement>(null);
const suffixRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (prefixRef.current) {
setPrefixWidth(prefixRef.current.offsetWidth);
}
if (suffixRef.current) {
setSuffixWidth(suffixRef.current.offsetWidth);
}
}, [prefix, suffix, prefixRef, suffixRef]);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (type === 'number') {
const numValue = e.target.value === '' ? '' : Number(e.target.value);
@ -35,40 +48,60 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
const isPasswordInput = type === 'password';
const inputEl = (
<input
ref={ref}
type={isPasswordInput && showPassword ? 'text' : type}
className={cn(
'peer/input',
'flex h-8 w-full rounded-md border-0.5 border-border-button bg-bg-input px-3 py-2 outline-none text-sm text-text-primary',
'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-1 focus-visible:ring-accent-primary',
'disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
{
'pl-12': !!prefix,
'pr-12': !!suffix || isPasswordInput,
'pr-24': !!suffix && isPasswordInput,
},
className,
)}
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
const inputEl = useMemo(
() => (
<input
ref={ref}
type={isPasswordInput && showPassword ? 'text' : type}
className={cn(
'peer/input',
'flex h-8 w-full rounded-md border-0.5 border-border-button bg-bg-input px-3 py-2 outline-none text-sm text-text-primary',
'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-1 focus-visible:ring-accent-primary',
'disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
type === 'number' &&
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
className,
)}
style={{
paddingLeft: !!prefix ? `${prefixWidth}px` : '',
paddingRight: isPasswordInput
? '40px'
: !!suffix
? `${suffixWidth}px`
: '',
}}
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
),
[
prefixWidth,
suffixWidth,
isPasswordInput,
inputValue,
className,
handleChange,
restProps,
],
);
if (prefix || suffix || isPasswordInput) {
return (
<div className="relative">
{prefix && (
<span className="absolute left-0 top-[50%] translate-y-[-50%]">
<span
ref={prefixRef}
className="absolute left-0 top-[50%] translate-y-[-50%]"
>
{prefix}
</span>
)}
{inputEl}
{suffix && (
<span
ref={suffixRef}
className={cn('absolute right-0 top-[50%] translate-y-[-50%]', {
'right-14': isPasswordInput,
})}
@ -77,10 +110,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</span>
)}
{isPasswordInput && (
<Button
variant="transparent"
<button
type="button"
className="
p-2 text-text-secondary
absolute border-0 right-1 top-[50%] translate-y-[-50%]
dark:peer-autofill/input:text-text-secondary-inverse
dark:peer-autofill/input:hover:text-text-primary-inverse
@ -93,7 +126,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
) : (
<Eye className="size-[1em]" />
)}
</Button>
</button>
)}
</div>
);

View file

@ -27,7 +27,10 @@ export interface ModalProps {
okText?: ReactNode | string;
onOk?: () => void;
onCancel?: () => void;
okButtonClassName?: string;
cancelButtonClassName?: string;
disabled?: boolean;
style?: React.CSSProperties;
}
export interface ModalType extends FC<ModalProps> {
show: typeof modalIns.show;
@ -56,7 +59,10 @@ const Modal: ModalType = ({
confirmLoading,
cancelText,
okText,
okButtonClassName,
cancelButtonClassName,
disabled = false,
style,
}) => {
const sizeClasses = {
small: 'max-w-md',
@ -111,7 +117,10 @@ const Modal: ModalType = ({
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary "
className={cn(
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary ',
cancelButtonClassName,
)}
>
{cancelText ?? t('modal.cancelText')}
</button>
@ -122,6 +131,7 @@ const Modal: ModalType = ({
className={cn(
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
{ 'cursor-not-allowed': disabled },
okButtonClassName,
)}
>
{confirmLoading && (
@ -153,23 +163,26 @@ const Modal: ModalType = ({
handleOk,
showfooter,
footerClassName,
okButtonClassName,
cancelButtonClassName,
]);
return (
<DialogPrimitive.Root open={open} onOpenChange={handleChange}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 z-50 bg-colors-background-neutral-weak/50 backdrop-blur-sm flex items-center justify-center p-4"
className="fixed inset-0 z-50 bg-bg-card backdrop-blur-sm flex items-center justify-center p-4"
onClick={() => maskClosable && onOpenChange?.(false)}
>
<DialogPrimitive.Content
className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`}
style={style}
onClick={(e) => e.stopPropagation()}
>
{/* title */}
{(title || closable) && (
<div
className={cn(
'flex items-center px-6 py-4',
'flex items-start px-6 py-4',
{
'justify-end': closable && !title,
'justify-between': closable && title,
@ -187,7 +200,7 @@ const Modal: ModalType = ({
<DialogPrimitive.Close asChild>
<button
type="button"
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted focus-visible:outline-none"
className="flex h-7 w-7 items-center justify-center text-text-secondary rounded-full hover:bg-bg-card focus-visible:outline-none"
onClick={handleCancel}
>
{closeIcon}
@ -198,7 +211,7 @@ const Modal: ModalType = ({
)}
{/* content */}
<div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[80vh] focus-visible:!outline-none">
<div className="py-2 px-6 overflow-y-auto scrollbar-auto max-h-[calc(100vh-280px)] focus-visible:!outline-none">
{destroyOnClose && !open ? null : children}
</div>

View file

@ -289,12 +289,12 @@ export const MultiSelect = React.forwardRef<
{...props}
onClick={handleTogglePopover}
className={cn(
'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
'flex w-full p-1 rounded-md border border-border-button min-h-10 h-auto placeholder:text-text-disabled items-center justify-between bg-bg-input hover:bg-bg-input [&_svg]:pointer-events-auto',
className,
)}
>
{selectedValues.length > 0 ? (
<div className="flex justify-between items-center w-full">
<div className="flex justify-between items-center w-full group">
<div className="flex flex-wrap items-center">
{selectedValues?.slice(0, maxCount)?.map((value) => {
const option = flatOptions.find((o) => o.value === value);
@ -348,9 +348,9 @@ export const MultiSelect = React.forwardRef<
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center justify-between ">
<XIcon
className="h-4 mx-2 cursor-pointer text-muted-foreground"
className="h-4 mx-2 cursor-pointer text-text-secondary hidden group-hover:block"
onClick={(event) => {
event.stopPropagation();
handleClear();
@ -358,17 +358,17 @@ export const MultiSelect = React.forwardRef<
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
className="min-h-6 h-full hidden group-hover:flex"
/>
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
<ChevronDown className="h-4 mx-2 cursor-pointer text-text-secondary" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full mx-auto">
<span className="text-sm text-muted-foreground mx-3">
<span className="text-sm text-text-secondary mx-3">
{placeholder}
</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
<ChevronDown className="h-4 cursor-pointer text-text-secondary mx-2" />
</div>
)}
</Button>
@ -378,15 +378,17 @@ export const MultiSelect = React.forwardRef<
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command>
<CommandInput
placeholder={t('common.search') + '...'}
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<Command className="p-5 pb-8">
{options && options.length > 0 && (
<CommandInput
placeholder={t('common.search') + '...'}
onKeyDown={handleInputKeyDown}
/>
)}
<CommandList className="mt-2">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{showSelectAll && (
{showSelectAll && options && options.length > 0 && (
<CommandItem
key="all"
onSelect={toggleAll}
@ -437,9 +439,9 @@ export const MultiSelect = React.forwardRef<
})}
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
<div className=" absolute bottom-1 left-1 right-1 flex items-center justify-between mx-5 bg-bg-base border-t border-border-button">
<CommandSeparator />
{selectedValues.length > 0 && (
<>
<CommandItem
@ -454,12 +456,14 @@ export const MultiSelect = React.forwardRef<
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
{t('common.close')}
</CommandItem>
{options && options.length > 0 && (
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
{t('common.close')}
</CommandItem>
)}
</div>
</CommandGroup>
</CommandList>

View file

@ -59,7 +59,7 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
'flex cursor-default items-center justify-center py-1 text-text-secondary hover:text-text-primary',
className,
)}
{...props}
@ -76,7 +76,7 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
'flex cursor-default items-center justify-center py-1 text-text-secondary hover:text-text-primary',
className,
)}
{...props}

View file

@ -54,7 +54,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full bg-bg-input rounded-md border border-input px-3 py-2 text-base ring-offset-background placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden',
'flex min-h-[80px] w-full bg-bg-input rounded-md border border-border-button px-3 py-2 text-base ring-offset-background placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden',
className,
)}
rows={autoSize?.minRows ?? props.rows ?? undefined}

View file

@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-auto scrollbar-auto rounded-md whitespace-pre-wrap border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-w-[30vw]',
'z-50 overflow-auto scrollbar-auto rounded-md whitespace-pre-wrap border bg-bg-base px-3 py-1.5 text-sm text-text-primary shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-w-[30vw]',
className,
)}
{...props}
@ -39,7 +39,7 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => {
e.preventDefault(); // Prevent clicking the tooltip from triggering form save
}}
>
<CircleQuestionMark className="size-3 ml-2" />
<CircleQuestionMark className="size-3 ml-[2px] -translate-y-1" />
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>

View file

@ -179,3 +179,8 @@ export enum JsonSchemaDataType {
Array = 'array',
Object = 'object',
}
export enum SwitchLogicOperator {
And = 'and',
Or = 'or',
}

View file

@ -1,5 +1,4 @@
import { ExclamationCircleFilled } from '@ant-design/icons';
import { App } from 'antd';
import { Modal } from '@/components/ui/modal/modal';
import isEqual from 'lodash/isEqual';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -84,20 +83,27 @@ interface IProps {
}
export const useShowDeleteConfirm = () => {
const { modal } = App.useApp();
const { t } = useTranslation();
const showDeleteConfirm = useCallback(
({ title, content, onOk, onCancel }: IProps): Promise<number> => {
return new Promise((resolve, reject) => {
modal.confirm({
Modal.show({
title: title ?? t('common.deleteModalTitle'),
icon: <ExclamationCircleFilled />,
content,
visible: true,
onVisibleChange: () => {
Modal.hide();
},
footer: null,
closable: true,
maskClosable: false,
okText: t('common.yes'),
okType: 'danger',
cancelText: t('common.no'),
async onOk() {
style: {
width: '400px',
},
okButtonClassName:
'bg-state-error text-white hover:bg-state-error hover:text-white',
onOk: async () => {
try {
const ret = await onOk?.();
resolve(ret);
@ -106,13 +112,15 @@ export const useShowDeleteConfirm = () => {
reject(error);
}
},
onCancel() {
onCancel: () => {
onCancel?.();
Modal.hide();
},
children: content,
});
});
},
[t, modal],
[t],
);
return showDeleteConfirm;

View file

@ -0,0 +1,12 @@
import { SwitchLogicOperator } from '@/constants/agent';
import { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next';
export function useBuildSwitchLogicOperatorOptions() {
const { t } = useTranslation();
return buildOptions(
SwitchLogicOperator,
t,
'flow.switchLogicOperatorOptions',
);
}

View file

@ -696,6 +696,8 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach is derived from mimicking the behavioral logic of how humans search for knowledge in books.`,
},
setting: {
seconds: 'seconds',
minutes: 'minutes',
edit: 'Edit',
cropTip:
'Drag the selection area to choose the cropping position of the image, and scroll to zoom in/out',
@ -1047,7 +1049,7 @@ Example: https://fsn1.your-objectstorage.com`,
downloadFileType: 'Download file type',
formatTypeError: 'Format or type error',
variableNameMessage:
'Variable name can only contain letters and underscores',
'Variable name can only contain letters and underscores and numbers',
variableDescription: 'Variable Description',
defaultValue: 'Default Value',
conversationVariable: 'Conversation variable',

View file

@ -685,6 +685,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
tocEnhanceTip: `解析文档时生成了目录信息见General方法的启用目录抽取让大模型返回和用户问题相关的目录项从而利用目录项拿到相关chunk对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`,
},
setting: {
seconds: '秒',
minutes: '分',
edit: '编辑',
cropTip: '拖动选区可以选择要图片的裁剪位置,滚动可以放大/缩小选区',
cropImage: '剪裁图片',
@ -978,7 +980,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
downloadFileTypeTip: '文件下载的类型',
downloadFileType: '文件类型',
formatTypeError: '格式或类型错误',
variableNameMessage: '名称只能包含字母和下划线',
variableNameMessage: '名称只能包含字母,数字和下划线',
variableDescription: '变量的描述',
defaultValue: '默认值',
conversationVariable: '会话变量',

View file

@ -25,8 +25,8 @@ const ThemeSwitch = forwardRef<
setTheme(value ? ThemeEnum.Dark : ThemeEnum.Light)
}
>
<div className="self-center p-3 py-2 rounded-full bg-bg-card transition-[background-color] duration-300">
<div className="h-full flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color] duration-300 delay-75">
<div className="self-center p-3 py-2 rounded-full bg-bg-card transition-[background-color]">
<div className="h-full flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color]">
<LucideSun
className={cn('size-[1em]', !isDark && 'text-text-primary')}
/>
@ -39,7 +39,7 @@ const ThemeSwitch = forwardRef<
<Thumb
className={cn(
'absolute top-0 left-0 w-[calc(50%+.25rem)] p-0.5 h-full rounded-full overflow-hidden',
'transition-all ease-out duration-300',
'transition-all ease-out',
'group-hover/theme-switch:w-[calc(50%+.66rem)] group-focus-visible/theme-switch:w-[calc(50%+.66rem)]',
{
'left-[calc(50%-.25rem)] group-hover/theme-switch:left-[calc(50%-.66rem)] group-focus-visible/theme-switch:left-[calc(50%-.66rem)]':
@ -47,12 +47,7 @@ const ThemeSwitch = forwardRef<
},
)}
>
<div
className="
size-full rounded-full bg-bg-base shadow-md
transition-colors ease-out duration-300 delay-75
"
/>
<div className="size-full rounded-full bg-bg-base shadow-md transition-colors ease-out" />
</Thumb>
</Root>
);

View file

@ -8,4 +8,7 @@
border: 0;
background-color: transparent;
}
:global(.react-flow__node-group.selectable.selected) {
box-shadow: none;
}
}

View file

@ -8,7 +8,6 @@ import { memo } from 'react';
import { NodeHandleId, Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { CommonHandle, LeftEndHandle } from './handle';
import styles from './index.less';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ResizeIcon, controlStyle } from './resize-icon';
@ -23,9 +22,12 @@ export function InnerIterationNode({
return (
<ToolBar selected={selected} id={id} label={data.label} showRun={false}>
<section
className={cn('h-full bg-transparent rounded-b-md group', {
[styles.selectedHeader]: selected,
})}
className={cn(
'h-full bg-transparent rounded-b-md group border border-border-button border-t-0',
{
['border-x border-accent-primary']: selected,
},
)}
>
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
<ResizeIcon />
@ -43,9 +45,9 @@ export function InnerIterationNode({
name={data.name}
label={data.label}
wrapperClassName={cn(
'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]',
'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-38px] left-[-0.3px] border-x border-t border-border-button',
{
[styles.selectedHeader]: selected,
['border-x border-t border-accent-primary']: selected,
},
)}
></NodeHeader>

View file

@ -10,6 +10,7 @@ import {
JsonSchemaDataType,
Operator,
ProgrammingLanguage,
SwitchLogicOperator,
SwitchOperatorOptions,
initialLlmBaseValues,
} from '@/constants/agent';
@ -51,8 +52,6 @@ import {
export const BeginId = 'begin';
export const SwitchLogicOperatorOptions = ['and', 'or'];
export const CommonOperatorList = Object.values(Operator).filter(
(x) => x !== Operator.Note,
);
@ -308,7 +307,7 @@ export const initialExeSqlValues = {
export const initialSwitchValues = {
conditions: [
{
logical_operator: SwitchLogicOperatorOptions[0],
logical_operator: SwitchLogicOperator.And,
items: [
{
operator: SwitchOperatorOptions[0].value,

View file

@ -0,0 +1,253 @@
import { KeyInput } from '@/components/key-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { useIsDarkTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { buildOptions } from '@/utils/form';
import { Editor, loader } from '@monaco-editor/react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { X } from 'lucide-react';
import { ReactNode, useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { TypesWithArray } from '../../constant';
import { buildConversationVariableSelectOptions } from '../../utils';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
import { QueryVariable } from '../components/query-variable';
loader.config({ paths: { vs: '/vs' } });
enum InputMode {
Constant = 'constant',
Variable = 'variable',
}
const InputModeOptions = buildOptions(InputMode);
type SelectKeysProps = {
name: string;
label: ReactNode;
tooltip?: string;
keyField?: string;
valueField?: string;
operatorField?: string;
};
type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;
type RadioButtonProps = Partial<
Omit<RadioGroupProps, 'onValueChange'> & {
onChange: RadioGroupProps['onValueChange'];
}
>;
function RadioButton({ value, onChange }: RadioButtonProps) {
return (
<RadioGroup
defaultValue="yes"
className="flex"
value={value}
onValueChange={onChange}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="yes" id="r1" />
<Label htmlFor="r1">Yes</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="no" id="r2" />
<Label htmlFor="r2">No</Label>
</div>
</RadioGroup>
);
}
const VariableTypeOptions = buildConversationVariableSelectOptions();
const modeField = 'mode';
const ConstantValueMap = {
[TypesWithArray.Boolean]: 'yes',
[TypesWithArray.Number]: 0,
[TypesWithArray.String]: '',
[TypesWithArray.ArrayBoolean]: '[]',
[TypesWithArray.ArrayNumber]: '[]',
[TypesWithArray.ArrayString]: '[]',
[TypesWithArray.ArrayObject]: '[]',
[TypesWithArray.Object]: '{}',
};
export function DynamicVariables({
name,
label,
tooltip,
keyField = 'variable',
valueField = 'parameter',
operatorField = 'operator',
}: SelectKeysProps) {
const form = useFormContext();
const isDarkTheme = useIsDarkTheme();
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
const initializeValue = useCallback(
(mode: string, variableType: string, valueFieldAlias: string) => {
if (mode === InputMode.Variable) {
form.setValue(valueFieldAlias, '', { shouldDirty: true });
} else {
const val = ConstantValueMap[variableType as TypesWithArray];
form.setValue(valueFieldAlias, val, { shouldDirty: true });
}
},
[form],
);
const handleModeChange = useCallback(
(mode: string, valueFieldAlias: string, operatorFieldAlias: string) => {
const variableType = form.getValues(operatorFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
// if (mode === InputMode.Variable) {
// form.setValue(valueFieldAlias, '');
// } else {
// const val = ConstantValueMap[variableType as TypesWithArray];
// form.setValue(valueFieldAlias, val);
// }
},
[form, initializeValue],
);
const handleVariableTypeChange = useCallback(
(variableType: string, valueFieldAlias: string, modeFieldAlias: string) => {
const mode = form.getValues(modeFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
);
const renderParameter = useCallback(
(operatorFieldName: string, modeFieldName: string) => {
const mode = form.getValues(modeFieldName);
const logicalOperator = form.getValues(operatorFieldName);
if (mode === InputMode.Constant) {
if (logicalOperator === TypesWithArray.Boolean) {
return <RadioButton></RadioButton>;
}
if (logicalOperator === TypesWithArray.Number) {
return <Input className="w-full" type="number"></Input>;
}
if (logicalOperator === TypesWithArray.String) {
return <Textarea></Textarea>;
}
return (
<Editor
height={300}
theme={isDarkTheme ? 'vs-dark' : 'vs'}
language={'json'}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
);
}
return (
<QueryVariable
types={[logicalOperator]}
hideLabel
pureQuery
></QueryVariable>
);
},
[form, isDarkTheme],
);
return (
<section className="space-y-2">
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() =>
append({
[keyField]: '',
[valueField]: '',
[modeField]: InputMode.Constant,
[operatorField]: TypesWithArray.String,
})
}
></DynamicFormHeader>
<div className="space-y-5">
{fields.map((field, index) => {
const keyFieldAlias = `${name}.${index}.${keyField}`;
const valueFieldAlias = `${name}.${index}.${valueField}`;
const operatorFieldAlias = `${name}.${index}.${operatorField}`;
const modeFieldAlias = `${name}.${index}.${modeField}`;
return (
<section key={field.id} className="flex gap-2">
<div className="flex-1 space-y-3 min-w-0">
<div className="flex items-center">
<RAGFlowFormItem name={keyFieldAlias} className="flex-1 ">
<KeyInput></KeyInput>
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
handleVariableTypeChange(
val,
valueFieldAlias,
modeFieldAlias,
);
field.onChange(val);
}}
options={VariableTypeOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name={modeFieldAlias} className="flex-1">
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
handleModeChange(
val,
valueFieldAlias,
operatorFieldAlias,
);
field.onChange(val);
}}
options={InputModeOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
</div>
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
{renderParameter(operatorFieldAlias, modeFieldAlias)}
</RAGFlowFormItem>
</div>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X />
</Button>
</section>
);
})}
</div>
</section>
);
}

View file

@ -1,4 +1,3 @@
import { FormContainer } from '@/components/form-container';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
@ -10,12 +9,21 @@ import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { DynamicOutput } from './dynamic-output';
import { DynamicVariables } from './dynamic-variables';
import { OutputArray } from './interface';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-form-change';
const FormSchema = z.object({
query: z.string().optional(),
variables: z.array(
z.object({
variable: z.string().optional(),
operator: z.string().optional(),
parameter: z.string().or(z.number()).or(z.boolean()).optional(),
mode: z.string(),
}),
),
outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(),
});
@ -41,12 +49,11 @@ function IterationForm({ node }: INextOperatorForm) {
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable
name="items_ref"
types={ArrayFields as any[]}
></QueryVariable>
</FormContainer>
<QueryVariable
name="items_ref"
types={ArrayFields as any[]}
></QueryVariable>
<DynamicVariables name="variables" label="Variables"></DynamicVariables>
<DynamicOutput node={node}></DynamicOutput>
<Output list={outputList}></Output>
</FormWrapper>

View file

@ -0,0 +1,59 @@
import { buildOptions } from '@/utils/form';
import { camelCase } from 'lodash';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
JsonSchemaDataType,
VariableAssignerLogicalArrayOperator,
VariableAssignerLogicalNumberOperator,
VariableAssignerLogicalNumberOperatorLabelMap,
VariableAssignerLogicalOperator,
} from '../../constant';
export function useBuildLogicalOptions() {
const { t } = useTranslation();
const buildVariableAssignerLogicalOptions = useCallback(
(record: Record<string, any>) => {
return buildOptions(
record,
t,
'flow.variableAssignerLogicalOperatorOptions',
true,
);
},
[t],
);
const buildLogicalOptions = useCallback(
(type: string) => {
if (
type?.toLowerCase().startsWith(JsonSchemaDataType.Array.toLowerCase())
) {
return buildVariableAssignerLogicalOptions(
VariableAssignerLogicalArrayOperator,
);
}
if (type === JsonSchemaDataType.Number) {
return Object.values(VariableAssignerLogicalNumberOperator).map(
(val) => ({
label: t(
`flow.variableAssignerLogicalOperatorOptions.${camelCase(VariableAssignerLogicalNumberOperatorLabelMap[val as keyof typeof VariableAssignerLogicalNumberOperatorLabelMap] || val)}`,
),
value: val,
}),
);
}
return buildVariableAssignerLogicalOptions(
VariableAssignerLogicalOperator,
);
},
[buildVariableAssignerLogicalOptions, t],
);
return {
buildLogicalOptions,
};
}

View file

@ -11,16 +11,17 @@ import {
import { RAGFlowSelect } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { SwitchLogicOperator } from '@/constants/agent';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { X } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { SwitchLogicOperatorOptions } from '../../constant';
import { IOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { QueryVariable } from '../components/query-variable';
@ -185,12 +186,7 @@ function SwitchForm({ node }: IOperatorForm) {
control: form.control,
});
const switchLogicOperatorOptions = useMemo(() => {
return SwitchLogicOperatorOptions.map((x) => ({
value: x,
label: t(`flow.switchLogicOperatorOptions.${x}`),
}));
}, [t]);
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
useWatchFormChange(node?.id, form);
@ -253,7 +249,7 @@ function SwitchForm({ node }: IOperatorForm) {
<BlockButton
onClick={() =>
append({
logical_operator: SwitchLogicOperatorOptions[0],
logical_operator: SwitchLogicOperator.And,
[ItemKey]: [
{
operator: switchOperatorOptions[0].value,

View file

@ -1,7 +1,7 @@
import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form';
import { buildSelectOptions } from '@/utils/component-util';
import { t } from 'i18next';
import { TypesWithArray } from '../constant';
import { buildConversationVariableSelectOptions } from '../utils';
export { TypesWithArray } from '../constant';
// const TypesWithoutArray = Object.values(JsonSchemaDataType).filter(
// (item) => item !== JsonSchemaDataType.Array,
@ -18,7 +18,7 @@ export const GlobalFormFields = [
placeholder: t('common.namePlaceholder'),
required: true,
validation: {
pattern: /^[a-zA-Z_]+$/,
pattern: /^[a-zA-Z_0-9]+$/,
message: t('flow.variableNameMessage'),
},
type: FormFieldType.Text,
@ -29,7 +29,7 @@ export const GlobalFormFields = [
placeholder: '',
required: true,
type: FormFieldType.Select,
options: buildSelectOptions(Object.values(TypesWithArray)),
options: buildConversationVariableSelectOptions(),
},
{
label: t('flow.defaultValue'),

View file

@ -3,6 +3,7 @@ import { BlockButton, Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Segmented } from '@/components/ui/segmented';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { Trash2, X } from 'lucide-react';
import { useCallback } from 'react';
import { FieldValues } from 'react-hook-form';
@ -36,14 +37,19 @@ export const useObjectFields = () => {
path: (string | number)[] = [],
): Array<{ path: (string | number)[]; message: string }> => {
const errors: Array<{ path: (string | number)[]; message: string }> = [];
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
if (typeof obj === 'object' && !Array.isArray(obj)) {
if (isEmpty(obj)) {
errors.push({
path: [...path],
message: 'No empty parameters are allowed.',
});
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (!/^[a-zA-Z_]+$/.test(key)) {
if (!/^[a-zA-Z_0-9]+$/.test(key)) {
errors.push({
path: [...path, key],
message: `Key "${key}" is invalid. Keys can only contain letters and underscores.`,
message: `Key "${key}" is invalid. Keys can only contain letters and underscores and numbers.`,
});
}
const nestedErrors = validateKeys(obj[key], [...path, key]);
@ -108,6 +114,21 @@ export const useObjectFields = () => {
}
}, []);
const arrayObjectValidate = useCallback((value: any) => {
try {
if (validateKeys(value, [])?.length > 0) {
throw new Error(t('flow.formatTypeError'));
}
if (value && typeof value === 'string' && !JSON.parse(value)) {
throw new Error(t('flow.formatTypeError'));
}
return true;
} catch (e) {
console.log('object-render-error', e, value);
throw new Error(t('flow.formatTypeError'));
}
}, []);
const arrayStringRender = useCallback((field: FieldValues, type = 'text') => {
const values = Array.isArray(field.value)
? field.value
@ -253,8 +274,9 @@ export const useObjectFields = () => {
const handleCustomValidate = (value: TypesWithArray) => {
switch (value) {
case TypesWithArray.Object:
case TypesWithArray.ArrayObject:
return objectValidate;
case TypesWithArray.ArrayObject:
return arrayObjectValidate;
case TypesWithArray.ArrayString:
return arrayStringValidate;
case TypesWithArray.ArrayNumber:
@ -284,6 +306,7 @@ export const useObjectFields = () => {
return {
objectRender,
objectValidate,
arrayObjectValidate,
arrayStringRender,
arrayStringValidate,
arrayNumberRender,

View file

@ -7,6 +7,7 @@ import {
ICategorizeItemResult,
} from '@/interfaces/database/agent';
import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow';
import { buildSelectOptions } from '@/utils/component-util';
import { removeUselessFieldsFromValues } from '@/utils/form';
import { Edge, Node, XYPosition } from '@xyflow/react';
import { FormInstance, FormListFieldData } from 'antd';
@ -30,6 +31,7 @@ import {
NoDebugOperatorsList,
NodeHandleId,
Operator,
TypesWithArray,
} from './constant';
import { DataOperationsFormSchemaType } from './form/data-operations-form';
import { ExtractorFormSchemaType } from './form/extractor-form';
@ -766,3 +768,7 @@ export function buildBeginQueryWithObject(
export function getArrayElementType(type: string) {
return typeof type === 'string' ? type.match(/<([^>]+)>/)?.at(1) ?? '' : '';
}
export function buildConversationVariableSelectOptions() {
return buildSelectOptions(Object.values(TypesWithArray));
}

View file

@ -92,7 +92,7 @@ export function SideBar({ refreshCount }: PropType) {
key={itemIdx}
variant={active ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-sub-title-invert',
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-secondary',
{
'bg-bg-card': active,
'text-text-primary': active,

View file

@ -1,236 +0,0 @@
import { SliderInputSwitchFormField } from '@/components/llm-setting-items/slider';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import { camelCase, isEqual } from 'lodash';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
interface LlmSettingFieldItemsProps {
prefix?: string;
options?: any[];
}
const LlmSettingEnableSchema = {
temperatureEnabled: z.boolean(),
topPEnabled: z.boolean(),
presencePenaltyEnabled: z.boolean(),
frequencyPenaltyEnabled: z.boolean(),
};
export const LlmSettingSchema = {
llm_id: z.string(),
parameter: z.string().optional(),
temperature: z.coerce.number().optional(),
top_p: z.coerce.number().optional(),
presence_penalty: z.coerce.number().optional(),
frequency_penalty: z.coerce.number().optional(),
...LlmSettingEnableSchema,
// maxTokensEnabled: z.boolean(),
};
export function LlmSettingFieldItems({
prefix,
options,
}: LlmSettingFieldItemsProps) {
const form = useFormContext();
const { t } = useTranslate('chat');
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const handleChange = useCallback(
(parameter: string) => {
const values =
settledModelVariableMap[
parameter as keyof typeof settledModelVariableMap
];
const enabledKeys = Object.keys(LlmSettingEnableSchema);
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const element = values[key as keyof typeof values];
form.setValue(`${prefix}.${key}`, element);
}
}
if (enabledKeys && enabledKeys.length) {
for (const key of enabledKeys) {
form.setValue(`${prefix}.${key}`, true);
}
}
},
[form, prefix],
);
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: t(camelCase(x)),
value: x,
})) as unknown as { label: string; value: ModelVariableType | 'Custom' }[];
parameterOptions.push({
label: t(camelCase('Custom')),
value: 'Custom',
});
const getFieldWithPrefix = useCallback(
(name: string) => {
return prefix ? `${prefix}.${name}` : name;
},
[prefix],
);
const checkParameterIsEquel = () => {
const [
parameter,
topPValue,
frequencyPenaltyValue,
temperatureValue,
presencePenaltyValue,
] = form.getValues([
getFieldWithPrefix('parameter'),
getFieldWithPrefix('temperature'),
getFieldWithPrefix('top_p'),
getFieldWithPrefix('frequency_penalty'),
getFieldWithPrefix('presence_penalty'),
]);
if (parameter && parameter !== 'Custom') {
const parameterValue =
settledModelVariableMap[parameter as keyof typeof ModelVariableType];
const parameterRealValue = {
top_p: topPValue,
temperature: temperatureValue,
frequency_penalty: frequencyPenaltyValue,
presence_penalty: presencePenaltyValue,
};
if (!isEqual(parameterValue, parameterRealValue)) {
form.setValue(getFieldWithPrefix('parameter'), 'Custom');
}
}
};
return (
<div className="space-y-5">
<FormField
control={form.control}
name={getFieldWithPrefix('llm_id')}
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('model')}
</FormLabel>
<FormControl>
<SelectWithSearch
options={options || modelOptions}
triggerClassName="!bg-bg-input"
{...field}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={getFieldWithPrefix('parameter')}
render={({ field }) => (
<FormItem className="flex justify-between gap-4 items-center">
<FormLabel>{t('freedom')}</FormLabel>
<FormControl>
<div className="w-28">
<Select
{...field}
onValueChange={(val) => {
handleChange(val);
field.onChange(val);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{parameterOptions.map((x) => (
<SelectItem value={x.value} key={x.value}>
{x.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName={getFieldWithPrefix('temperatureEnabled')}
label="temperature"
max={1}
min={0}
step={0.01}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName={getFieldWithPrefix('topPEnabled')}
label="topP"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName={getFieldWithPrefix('presencePenaltyEnabled')}
label="presencePenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName={getFieldWithPrefix('frequencyPenaltyEnabled')}
label="frequencyPenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
{/* <SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
label="maxTokens"
max={128000}
></SliderInputSwitchFormField> */}
</div>
);
}

View file

@ -1,6 +1,10 @@
// src/pages/next-search/search-setting.tsx
import { AvatarUpload } from '@/components/avatar-upload';
import {
LlmSettingFieldItems,
LlmSettingSchema,
} from '@/components/llm-setting-items/next';
import {
MetadataFilter,
MetadataFilterSchema,
@ -46,10 +50,10 @@ import {
IllmSettingProps,
useUpdateSearch,
} from '../next-searches/hooks';
import {
LlmSettingFieldItems,
LlmSettingSchema,
} from './search-setting-aisummery-config';
// import {
// LlmSettingFieldItems,
// LlmSettingSchema,
// } from './search-setting-aisummery-config';
interface SearchSettingProps {
open: boolean;
@ -397,6 +401,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
isTooltipShown
similarityName="search_config.similarity_threshold"
vectorSimilarityWeightName="search_config.vector_similarity_weight"
numberInputClassName="rounded-sm"
></SimilaritySliderFormField>
{/* Rerank Model */}
<FormField
@ -462,7 +467,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
className="h-7 w-20 bg-bg-card border border-border-button rounded-sm"
max={2048}
min={0}
step={1}
@ -493,9 +498,19 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
)}
/>
{aiSummaryDisabled && (
// <LlmSettingFieldItems
// prefix="search_config.llm_setting"
// options={aiSummeryModelOptions}
// ></LlmSettingFieldItems>
<LlmSettingFieldItems
prefix="search_config.llm_setting"
options={aiSummeryModelOptions}
showFields={[
'temperature',
'top_p',
'presence_penalty',
'frequency_penalty',
]}
></LlmSettingFieldItems>
)}
{/* Feature Controls */}

View file

@ -62,6 +62,7 @@ const AddDataSourceModal = ({
sourceData?.id as keyof typeof DataSourceFormDefaultValues
] as FieldValues
}
labelClassName="font-normal"
>
<div className=" absolute bottom-0 right-0 left-0 flex items-center justify-end w-full gap-2 py-6 px-6">
<DynamicForm.CancelButton

View file

@ -1,725 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react';
import {
DefaultValues,
FieldValues,
SubmitHandler,
useForm,
useFormContext,
} from 'react-hook-form';
import { ZodSchema, z } from 'zod';
import EditTag from '@/components/edit-tag';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { Loader } from 'lucide-react';
// Field type enumeration
export enum FormFieldType {
Text = 'text',
Email = 'email',
Password = 'password',
Number = 'number',
Textarea = 'textarea',
Select = 'select',
Checkbox = 'checkbox',
Tag = 'tag',
}
// Field configuration interface
export interface FormFieldConfig {
name: string;
label: string;
type: FormFieldType;
hidden?: boolean;
required?: boolean;
placeholder?: string;
options?: { label: string; value: string }[];
defaultValue?: any;
validation?: {
pattern?: RegExp;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
message?: string;
};
render?: (fieldProps: any) => React.ReactNode;
horizontal?: boolean;
onChange?: (value: any) => void;
}
// Component props interface
interface DynamicFormProps<T extends FieldValues> {
fields: FormFieldConfig[];
onSubmit: SubmitHandler<T>;
className?: string;
children?: React.ReactNode;
defaultValues?: DefaultValues<T>;
}
// Form ref interface
export interface DynamicFormRef {
submit: () => void;
getValues: () => any;
reset: (values?: any) => void;
}
// Generate Zod validation schema based on field configurations
const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
const schema: Record<string, ZodSchema> = {};
const nestedSchemas: Record<string, Record<string, ZodSchema>> = {};
fields.forEach((field) => {
let fieldSchema: ZodSchema;
// Create base validation schema based on field type
switch (field.type) {
case FormFieldType.Email:
fieldSchema = z.string().email('Please enter a valid email address');
break;
case FormFieldType.Number:
fieldSchema = z.coerce.number();
if (field.validation?.min !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).min(
field.validation.min,
field.validation.message ||
`Value cannot be less than ${field.validation.min}`,
);
}
if (field.validation?.max !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).max(
field.validation.max,
field.validation.message ||
`Value cannot be greater than ${field.validation.max}`,
);
}
break;
case FormFieldType.Checkbox:
fieldSchema = z.boolean();
break;
case FormFieldType.Tag:
fieldSchema = z.array(z.string());
break;
default:
fieldSchema = z.string();
break;
}
// Handle required fields
if (field.required) {
if (field.type === FormFieldType.Checkbox) {
fieldSchema = (fieldSchema as z.ZodBoolean).refine(
(val) => val === true,
{
message: `${field.label} is required`,
},
);
} else if (field.type === FormFieldType.Tag) {
fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(1, {
message: `${field.label} is required`,
});
} else {
fieldSchema = (fieldSchema as z.ZodString).min(1, {
message: `${field.label} is required`,
});
}
}
if (!field.required) {
fieldSchema = fieldSchema.optional();
}
// Handle other validation rules
if (
field.type !== FormFieldType.Number &&
field.type !== FormFieldType.Checkbox &&
field.type !== FormFieldType.Tag &&
field.required
) {
fieldSchema = fieldSchema as z.ZodString;
if (field.validation?.minLength !== undefined) {
fieldSchema = (fieldSchema as z.ZodString).min(
field.validation.minLength,
field.validation.message ||
`Enter at least ${field.validation.minLength} characters`,
);
}
if (field.validation?.maxLength !== undefined) {
fieldSchema = (fieldSchema as z.ZodString).max(
field.validation.maxLength,
field.validation.message ||
`Enter up to ${field.validation.maxLength} characters`,
);
}
if (field.validation?.pattern) {
fieldSchema = (fieldSchema as z.ZodString).regex(
field.validation.pattern,
field.validation.message || 'Invalid input format',
);
}
}
if (field.name.includes('.')) {
const keys = field.name.split('.');
const firstKey = keys[0];
if (!nestedSchemas[firstKey]) {
nestedSchemas[firstKey] = {};
}
let currentSchema = nestedSchemas[firstKey];
for (let i = 1; i < keys.length - 1; i++) {
const key = keys[i];
if (!currentSchema[key]) {
currentSchema[key] = {};
}
currentSchema = currentSchema[key];
}
const lastKey = keys[keys.length - 1];
currentSchema[lastKey] = fieldSchema;
} else {
schema[field.name] = fieldSchema;
}
});
Object.keys(nestedSchemas).forEach((key) => {
const buildNestedSchema = (obj: Record<string, any>): ZodSchema => {
const nestedSchema: Record<string, ZodSchema> = {};
Object.keys(obj).forEach((subKey) => {
if (
typeof obj[subKey] === 'object' &&
!(obj[subKey] instanceof z.ZodType)
) {
nestedSchema[subKey] = buildNestedSchema(obj[subKey]);
} else {
nestedSchema[subKey] = obj[subKey];
}
});
return z.object(nestedSchema);
};
schema[key] = buildNestedSchema(nestedSchemas[key]);
});
return z.object(schema);
};
// Generate default values based on field configurations
const generateDefaultValues = <T extends FieldValues>(
fields: FormFieldConfig[],
): DefaultValues<T> => {
const defaultValues: Record<string, any> = {};
fields.forEach((field) => {
if (field.name.includes('.')) {
const keys = field.name.split('.');
let current = defaultValues;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (field.defaultValue !== undefined) {
current[lastKey] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
current[lastKey] = false;
} else if (field.type === FormFieldType.Tag) {
current[lastKey] = [];
} else {
current[lastKey] = '';
}
} else {
if (field.defaultValue !== undefined) {
defaultValues[field.name] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
defaultValues[field.name] = false;
} else if (field.type === FormFieldType.Tag) {
defaultValues[field.name] = [];
} else {
defaultValues[field.name] = '';
}
}
});
return defaultValues as DefaultValues<T>;
};
// Dynamic form component
const DynamicForm = {
Root: forwardRef(
<T extends FieldValues>(
{
fields,
onSubmit,
className = '',
children,
defaultValues: formDefaultValues = {} as DefaultValues<T>,
}: DynamicFormProps<T>,
ref: React.Ref<any>,
) => {
// Generate validation schema and default values
const schema = useMemo(() => generateSchema(fields), [fields]);
const defaultValues = useMemo(() => {
const value = {
...generateDefaultValues(fields),
...formDefaultValues,
};
console.log('generateDefaultValues', fields, value);
return value;
}, [fields, formDefaultValues]);
// Initialize form
const form = useForm<T>({
resolver: zodResolver(schema),
defaultValues,
});
// Expose form methods via ref
useImperativeHandle(ref, () => ({
submit: () => form.handleSubmit(onSubmit)(),
getValues: () => form.getValues(),
reset: (values?: T) => {
if (values) {
form.reset(values);
} else {
form.reset();
}
},
setError: form.setError,
clearErrors: form.clearErrors,
trigger: form.trigger,
}));
useEffect(() => {
if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
form.reset({
...generateDefaultValues(fields),
...formDefaultValues,
});
}
}, [form, formDefaultValues, fields]);
// Submit handler
// const handleSubmit = form.handleSubmit(onSubmit);
// Render form fields
const renderField = (field: FormFieldConfig) => {
if (field.render) {
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target?.value ?? e);
},
}
: fieldProps;
return field.render?.(finalFieldProps);
}}
</RAGFlowFormItem>
);
}
switch (field.type) {
case FormFieldType.Textarea:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Textarea
{...finalFieldProps}
placeholder={field.placeholder}
className="resize-none"
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Select:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string) => {
console.log('select value', value);
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
}
: fieldProps;
return (
<SelectWithSearch
triggerClassName="!shrink"
{...finalFieldProps}
options={field.options}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Checkbox:
return (
<FormField
control={form.control}
name={field.name as any}
render={({ field: formField }) => (
<FormItem
className={cn('flex items-center', {
'flex-row items-start space-x-3 space-y-0':
!field.horizontal,
})}
>
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormControl>
<Checkbox
checked={formField.value}
onCheckedChange={(checked) => {
formField.onChange(checked);
field.onChange?.(checked);
}}
/>
</FormControl>
{field.label && field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
);
case FormFieldType.Tag:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string[]) => {
fieldProps.onChange(value);
field.onChange?.(value);
},
}
: fieldProps;
return (
// <TagInput {...fieldProps} placeholder={field.placeholder} />
<div className="w-full">
<EditTag {...finalFieldProps}></EditTag>
</div>
);
}}
</RAGFlowFormItem>
);
default:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Input
{...finalFieldProps}
type={field.type}
placeholder={field.placeholder}
/>
);
}}
</RAGFlowFormItem>
);
}
};
return (
<Form {...form}>
<form
className={`space-y-6 ${className}`}
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit(onSubmit)(e);
}}
>
<>
{fields.map((field) => (
<div key={field.name} className={cn({ hidden: field.hidden })}>
{renderField(field)}
</div>
))}
{children}
</>
</form>
</Form>
);
},
) as <T extends FieldValues>(
props: DynamicFormProps<T> & { ref?: React.Ref<DynamicFormRef> },
) => React.ReactElement,
SavingButton: ({
submitLoading,
buttonText,
submitFunc,
}: {
submitLoading: boolean;
buttonText?: string;
submitFunc?: (values: FieldValues) => void;
}) => {
const form = useFormContext();
return (
<button
type="button"
disabled={submitLoading}
onClick={() => {
console.log('form submit');
(async () => {
console.log('form submit2');
try {
let beValid = await form.formControl.trigger();
console.log('form valid', beValid, form, form.formControl);
if (beValid) {
form.handleSubmit(async (values) => {
console.log('form values', values);
submitFunc?.(values);
})();
}
} catch (e) {
console.error(e);
} finally {
console.log('form submit3');
}
})();
}}
className={cn(
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
)}
>
{submitLoading && (
<Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
)}
{buttonText ?? t('modal.okText')}
</button>
);
},
CancelButton: ({
handleCancel,
cancelText,
}: {
handleCancel: () => void;
cancelText?: string;
}) => {
return (
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
>
{cancelText ?? t('modal.cancelText')}
</button>
);
},
};
export { DynamicForm };
/**
* Usage Example 1: Basic Form
*
* <DynamicForm
* fields={[
* {
* name: "username",
* label: "Username",
* type: FormFieldType.Text,
* required: true,
* placeholder: "Please enter username"
* },
* {
* name: "email",
* label: "Email",
* type: FormFieldType.Email,
* required: true,
* placeholder: "Please enter email address"
* }
* ]}
* onSubmit={(data) => {
* console.log(data); // { username: "...", email: "..." }
* }}
* />
*
* Usage Example 2: Nested Object Form
*
* <DynamicForm
* fields={[
* {
* name: "user.name",
* label: "Name",
* type: FormFieldType.Text,
* required: true,
* placeholder: "Please enter name"
* },
* {
* name: "user.email",
* label: "Email",
* type: FormFieldType.Email,
* required: true,
* placeholder: "Please enter email address"
* },
* {
* name: "user.profile.age",
* label: "Age",
* type: FormFieldType.Number,
* required: true,
* validation: {
* min: 18,
* max: 100,
* message: "Age must be between 18 and 100"
* }
* },
* {
* name: "user.profile.bio",
* label: "Bio",
* type: FormFieldType.Textarea,
* placeholder: "Please briefly introduce yourself"
* },
* {
* name: "settings.notifications",
* label: "Enable Notifications",
* type: FormFieldType.Checkbox
* }
* ]}
* onSubmit={(data) => {
* console.log(data);
* // {
* // user: {
* // name: "...",
* // email: "...",
* // profile: {
* // age: ...,
* // bio: "..."
* // }
* // },
* // settings: {
* // notifications: true/false
* // }
* // }
* }}
* />
*
* Usage Example 3: Tag Type Form
*
* <DynamicForm
* fields={[
* {
* name: "skills",
* label: "Skill Tags",
* type: FormFieldType.Tag,
* required: true,
* placeholder: "Enter skill and press Enter to add tag"
* },
* {
* name: "user.hobbies",
* label: "Hobbies",
* type: FormFieldType.Tag,
* placeholder: "Enter hobby and press Enter to add tag"
* }
* ]}
* onSubmit={(data) => {
* console.log(data);
* // {
* // skills: ["JavaScript", "React", "TypeScript"],
* // user: {
* // hobbies: ["Reading", "Swimming", "Travel"]
* // }
* // }
* }}
* />
*/

View file

@ -67,11 +67,11 @@ const SourceDetailPage = () => {
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-[58px] text-text-secondary italic ">
minutes
{t('setting.minutes')}
</span>
<button
type="button"
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button "
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button hover:bg-border-button hover:text-text-primary"
onClick={() => {
runSchedule();
}}
@ -112,7 +112,7 @@ const SourceDetailPage = () => {
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-6 text-text-secondary italic ">
seconds
{t('setting.seconds')}
</span>
</div>
),

View file

@ -150,7 +150,7 @@ export function EditMcpDialog({
</Button>
}
>
<div className="overflow-auto max-h-80 divide-y bg-bg-card rounded-md px-2.5">
<div className="overflow-auto max-h-80 divide-y-[0.5px] divide-border-button bg-bg-card rounded-md px-2.5 scrollbar-auto">
{nextTools?.map((x) => (
<McpToolCard key={x.name} data={x}></McpToolCard>
))}

View file

@ -86,7 +86,7 @@ export default function McpServer() {
{t('mcp.import')}
</Button>
<Button onClick={showEditModal('')}>
<Plus className="size-3.5" /> {t('mcp.addMCP')}
<Plus className="size-3.5 font-medium" /> {t('mcp.addMCP')}
</Button>
</div>
</section>

View file

@ -129,7 +129,9 @@ export const AvailableModels: FC<{
<div className="flex items-center space-x-3 mb-3">
<LlmIcon name={model.name} imgClass="h-8 w-8 text-text-primary" />
<div className="flex-1">
<h3 className="font-medium truncate">{model.name}</h3>
<div className="font-normal text-base truncate">
{model.name}
</div>
</div>
<Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex">
<Plus size={12} />

View file

@ -19,7 +19,7 @@ import { TenantRole } from '../constants';
import { useHandleDeleteUser } from './hooks';
const ColorMap: Record<string, string> = {
[TenantRole.Normal]: 'bg-transparent text-text-primary',
[TenantRole.Normal]: 'bg-transparent text-white',
[TenantRole.Invite]: 'bg-accent-primary-5 bg-accent-primary rounded-sm',
[TenantRole.Owner]: 'bg-red-100 text-red-800',
};
@ -137,7 +137,7 @@ const UserTable = ({ searchUser }: { searchUser: string }) => {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
className="h-8 w-8 p-0 hover:bg-state-error-5 hover:text-state-error"
onClick={handleDeleteTenantUser(record.user_id)}
>
<Trash2 className="h-4 w-4" />

View file

@ -100,8 +100,8 @@ export function SideBar() {
<ThemeToggle />
</div>
<Button
variant="outline"
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
variant="ghost"
className="w-full gap-3 bg-bg-base border border-border-button"
onClick={() => {
logout();
}}