Merge branch 'main' into webdav_storage
This commit is contained in:
commit
839c7a69af
73 changed files with 1263 additions and 1384 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 シリーズモデルをサポートします。
|
||||
|
|
|
|||
|
|
@ -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 시리즈 모델을 지원합니다.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 系列模型。
|
||||
|
|
|
|||
|
|
@ -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 系列模型。
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = '',
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -179,3 +179,8 @@ export enum JsonSchemaDataType {
|
|||
Array = 'array',
|
||||
Object = 'object',
|
||||
}
|
||||
|
||||
export enum SwitchLogicOperator {
|
||||
And = 'and',
|
||||
Or = 'or',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
12
web/src/hooks/logic-hooks/use-build-options.ts
Normal file
12
web/src/hooks/logic-hooks/use-build-options.ts
Normal 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',
|
||||
);
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: '会话变量',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,4 +8,7 @@
|
|||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
:global(.react-flow__node-group.selectable.selected) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
253
web/src/pages/agent/form/iteration-form/dynamic-variables.tsx
Normal file
253
web/src/pages/agent/form/iteration-form/dynamic-variables.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
* // }
|
||||
* // }
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
|
|
@ -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>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue