mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-04 03:25:30 +08:00
Compare commits
11 Commits
3ea84ad9c8
...
a95f22fa88
| Author | SHA1 | Date | |
|---|---|---|---|
| a95f22fa88 | |||
| 38ac6a7c27 | |||
| e5f3d5ae26 | |||
| 4cbc91f2fa | |||
| 6d3d3a40ab | |||
| 51b12841d6 | |||
| 993bf7c2c8 | |||
| b42b5fcf65 | |||
| 5d391fb1f9 | |||
| 2ddfcc7cf6 | |||
| 5ba51b21c9 |
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -247,7 +247,7 @@ jobs:
|
||||
echo "Waiting for service to be available..."
|
||||
sleep 5
|
||||
done
|
||||
source .venv/bin/activate && DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/test_sdk_api
|
||||
source .venv/bin/activate && DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/test_sdk_api > infinity_sdk_test.log
|
||||
|
||||
- name: Run frontend api tests against Infinity
|
||||
run: |
|
||||
@ -256,7 +256,7 @@ jobs:
|
||||
echo "Waiting for service to be available..."
|
||||
sleep 5
|
||||
done
|
||||
source .venv/bin/activate && DOC_ENGINE=infinity pytest -s --tb=short sdk/python/test/test_frontend_api/get_email.py sdk/python/test/test_frontend_api/test_dataset.py
|
||||
source .venv/bin/activate && DOC_ENGINE=infinity pytest -s --tb=short sdk/python/test/test_frontend_api/get_email.py sdk/python/test/test_frontend_api/test_dataset.py > infinity_api_test.log
|
||||
|
||||
- name: Run http api tests against Infinity
|
||||
run: |
|
||||
@ -265,7 +265,7 @@ jobs:
|
||||
echo "Waiting for service to be available..."
|
||||
sleep 5
|
||||
done
|
||||
source .venv/bin/activate && DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/test_http_api
|
||||
source .venv/bin/activate && DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/test_http_api > infinity_http_api_test.log
|
||||
|
||||
- name: Stop ragflow:nightly
|
||||
if: always() # always run this step even if previous steps failed
|
||||
|
||||
@ -108,7 +108,7 @@ def _load_user():
|
||||
authorization = request.headers.get("Authorization")
|
||||
g.user = None
|
||||
if not authorization:
|
||||
return
|
||||
return None
|
||||
|
||||
try:
|
||||
access_token = str(jwt.loads(authorization))
|
||||
|
||||
@ -25,7 +25,7 @@ from api.utils.api_utils import get_allowed_llm_factories, get_data_error_result
|
||||
from common.constants import StatusEnum, LLMType
|
||||
from api.db.db_models import TenantLLM
|
||||
from rag.utils.base64_image import test_image
|
||||
from rag.llm import EmbeddingModel, ChatModel, RerankModel, CvModel, TTSModel, OcrModel
|
||||
from rag.llm import EmbeddingModel, ChatModel, RerankModel, CvModel, TTSModel, OcrModel, Seq2txtModel
|
||||
|
||||
|
||||
@manager.route("/factories", methods=["GET"]) # noqa: F821
|
||||
@ -208,70 +208,83 @@ async def add_llm():
|
||||
msg = ""
|
||||
mdl_nm = llm["llm_name"].split("___")[0]
|
||||
extra = {"provider": factory}
|
||||
if llm["model_type"] == LLMType.EMBEDDING.value:
|
||||
assert factory in EmbeddingModel, f"Embedding model from {factory} is not supported yet."
|
||||
mdl = EmbeddingModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
||||
try:
|
||||
arr, tc = mdl.encode(["Test if the api key is available"])
|
||||
if len(arr[0]) == 0:
|
||||
raise Exception("Fail")
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access embedding model({mdl_nm})." + str(e)
|
||||
elif llm["model_type"] == LLMType.CHAT.value:
|
||||
assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
|
||||
mdl = ChatModel[factory](
|
||||
key=llm["api_key"],
|
||||
model_name=mdl_nm,
|
||||
base_url=llm["api_base"],
|
||||
**extra,
|
||||
)
|
||||
try:
|
||||
m, tc = await mdl.async_chat(None, [{"role": "user", "content": "Hello! How are you doing!"}], {"temperature": 0.9})
|
||||
if not tc and m.find("**ERROR**:") >= 0:
|
||||
raise Exception(m)
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
elif llm["model_type"] == LLMType.RERANK:
|
||||
assert factory in RerankModel, f"RE-rank model from {factory} is not supported yet."
|
||||
try:
|
||||
mdl = RerankModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
||||
arr, tc = mdl.similarity("Hello~ RAGFlower!", ["Hi, there!", "Ohh, my friend!"])
|
||||
if len(arr) == 0:
|
||||
raise Exception("Not known.")
|
||||
except KeyError:
|
||||
msg += f"{factory} dose not support this model({factory}/{mdl_nm})"
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
elif llm["model_type"] == LLMType.IMAGE2TEXT.value:
|
||||
assert factory in CvModel, f"Image to text model from {factory} is not supported yet."
|
||||
mdl = CvModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
||||
try:
|
||||
image_data = test_image
|
||||
m, tc = mdl.describe(image_data)
|
||||
if not tc and m.find("**ERROR**:") >= 0:
|
||||
raise Exception(m)
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
elif llm["model_type"] == LLMType.TTS:
|
||||
assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
|
||||
mdl = TTSModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
||||
try:
|
||||
for resp in mdl.tts("Hello~ RAGFlower!"):
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
elif llm["model_type"] == LLMType.OCR.value:
|
||||
assert factory in OcrModel, f"OCR model from {factory} is not supported yet."
|
||||
try:
|
||||
mdl = OcrModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm.get("api_base", ""))
|
||||
ok, reason = mdl.check_available()
|
||||
if not ok:
|
||||
raise RuntimeError(reason or "Model not available")
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
else:
|
||||
# TODO: check other type of models
|
||||
pass
|
||||
model_type = llm["model_type"]
|
||||
model_api_key = llm["api_key"]
|
||||
model_base_url = llm.get("api_base", "")
|
||||
match model_type:
|
||||
case LLMType.EMBEDDING.value:
|
||||
assert factory in EmbeddingModel, f"Embedding model from {factory} is not supported yet."
|
||||
mdl = EmbeddingModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
try:
|
||||
arr, tc = mdl.encode(["Test if the api key is available"])
|
||||
if len(arr[0]) == 0:
|
||||
raise Exception("Fail")
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access embedding model({mdl_nm})." + str(e)
|
||||
case LLMType.CHAT.value:
|
||||
assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
|
||||
mdl = ChatModel[factory](
|
||||
key=model_api_key,
|
||||
model_name=mdl_nm,
|
||||
base_url=model_base_url,
|
||||
**extra,
|
||||
)
|
||||
try:
|
||||
m, tc = await mdl.async_chat(None, [{"role": "user", "content": "Hello! How are you doing!"}],
|
||||
{"temperature": 0.9})
|
||||
if not tc and m.find("**ERROR**:") >= 0:
|
||||
raise Exception(m)
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
|
||||
case LLMType.RERANK.value:
|
||||
assert factory in RerankModel, f"RE-rank model from {factory} is not supported yet."
|
||||
try:
|
||||
mdl = RerankModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
arr, tc = mdl.similarity("Hello~ RAGFlower!", ["Hi, there!", "Ohh, my friend!"])
|
||||
if len(arr) == 0:
|
||||
raise Exception("Not known.")
|
||||
except KeyError:
|
||||
msg += f"{factory} dose not support this model({factory}/{mdl_nm})"
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
|
||||
case LLMType.IMAGE2TEXT.value:
|
||||
assert factory in CvModel, f"Image to text model from {factory} is not supported yet."
|
||||
mdl = CvModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
try:
|
||||
image_data = test_image
|
||||
m, tc = mdl.describe(image_data)
|
||||
if not tc and m.find("**ERROR**:") >= 0:
|
||||
raise Exception(m)
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
case LLMType.TTS.value:
|
||||
assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
|
||||
mdl = TTSModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
try:
|
||||
for resp in mdl.tts("Hello~ RAGFlower!"):
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
case LLMType.OCR.value:
|
||||
assert factory in OcrModel, f"OCR model from {factory} is not supported yet."
|
||||
try:
|
||||
mdl = OcrModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
ok, reason = mdl.check_available()
|
||||
if not ok:
|
||||
raise RuntimeError(reason or "Model not available")
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
case LLMType.SPEECH2TEXT:
|
||||
assert factory in Seq2txtModel, f"Speech model from {factory} is not supported yet."
|
||||
try:
|
||||
mdl = Seq2txtModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
# TODO: check the availability
|
||||
except Exception as e:
|
||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||
case _:
|
||||
raise RuntimeError(f"Unknown model type: {model_type}")
|
||||
|
||||
if msg:
|
||||
return get_data_error_result(message=msg)
|
||||
|
||||
@ -326,7 +326,6 @@ async def list_tools() -> Response:
|
||||
try:
|
||||
tools = await asyncio.to_thread(tool_call_session.get_tools, timeout)
|
||||
except Exception as e:
|
||||
tools = []
|
||||
return get_data_error_result(message=f"MCP list tools error: {e}")
|
||||
|
||||
results[server_key] = []
|
||||
@ -428,7 +427,6 @@ async def test_mcp() -> Response:
|
||||
try:
|
||||
tools = await asyncio.to_thread(tool_call_session.get_tools, timeout)
|
||||
except Exception as e:
|
||||
tools = []
|
||||
return get_data_error_result(message=f"Test MCP error: {e}")
|
||||
finally:
|
||||
# PERF: blocking call to close sessions — consider moving to background thread or task queue
|
||||
|
||||
@ -287,7 +287,7 @@ def list_chat(tenant_id):
|
||||
chats = DialogService.get_list(tenant_id, page_number, items_per_page, orderby, desc, id, name)
|
||||
if not chats:
|
||||
return get_result(data=[])
|
||||
list_assts = []
|
||||
list_assistants = []
|
||||
key_mapping = {
|
||||
"parameters": "variables",
|
||||
"prologue": "opener",
|
||||
@ -321,5 +321,5 @@ def list_chat(tenant_id):
|
||||
del res["kb_ids"]
|
||||
res["datasets"] = kb_list
|
||||
res["avatar"] = res.pop("icon")
|
||||
list_assts.append(res)
|
||||
return get_result(data=list_assts)
|
||||
list_assistants.append(res)
|
||||
return get_result(data=list_assistants)
|
||||
|
||||
@ -205,7 +205,8 @@ async def create(tenant_id):
|
||||
if not FileService.is_parent_folder_exist(pf_id):
|
||||
return get_json_result(data=False, message="Parent Folder Doesn't Exist!", code=RetCode.BAD_REQUEST)
|
||||
if FileService.query(name=req["name"], parent_id=pf_id):
|
||||
return get_json_result(data=False, message="Duplicated folder name in the same folder.", code=409)
|
||||
return get_json_result(data=False, message="Duplicated folder name in the same folder.",
|
||||
code=RetCode.CONFLICT)
|
||||
|
||||
if input_file_type == FileType.FOLDER.value:
|
||||
file_type = FileType.FOLDER.value
|
||||
@ -565,11 +566,13 @@ async def rename(tenant_id):
|
||||
|
||||
if file.type != FileType.FOLDER.value and pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
|
||||
file.name.lower()).suffix:
|
||||
return get_json_result(data=False, message="The extension of file can't be changed", code=RetCode.BAD_REQUEST)
|
||||
return get_json_result(data=False, message="The extension of file can't be changed",
|
||||
code=RetCode.BAD_REQUEST)
|
||||
|
||||
for existing_file in FileService.query(name=req["name"], pf_id=file.parent_id):
|
||||
if existing_file.name == req["name"]:
|
||||
return get_json_result(data=False, message="Duplicated file name in the same folder.", code=409)
|
||||
return get_json_result(data=False, message="Duplicated file name in the same folder.",
|
||||
code=RetCode.CONFLICT)
|
||||
|
||||
if not FileService.update_by_id(req["file_id"], {"name": req["name"]}):
|
||||
return get_json_result(message="Database error (File rename)!", code=RetCode.SERVER_ERROR)
|
||||
@ -631,9 +634,10 @@ async def get(tenant_id, file_id):
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/file/download/<attachment_id>", methods=["GET"]) # noqa: F821
|
||||
@token_required
|
||||
async def download_attachment(tenant_id,attachment_id):
|
||||
async def download_attachment(tenant_id, attachment_id):
|
||||
try:
|
||||
ext = request.args.get("ext", "markdown")
|
||||
data = await asyncio.to_thread(settings.STORAGE_IMPL.get, tenant_id, attachment_id)
|
||||
@ -645,6 +649,7 @@ async def download_attachment(tenant_id,attachment_id):
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/file/mv', methods=['POST']) # noqa: F821
|
||||
@token_required
|
||||
async def move(tenant_id):
|
||||
|
||||
@ -448,7 +448,7 @@ async def chat_completion_openai_like(tenant_id, chat_id):
|
||||
@token_required
|
||||
async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
||||
req = await get_request_json()
|
||||
tiktokenenc = tiktoken.get_encoding("cl100k_base")
|
||||
tiktoken_encode = tiktoken.get_encoding("cl100k_base")
|
||||
messages = req.get("messages", [])
|
||||
if not messages:
|
||||
return get_error_data_result("You must provide at least one message.")
|
||||
@ -456,7 +456,7 @@ async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
||||
return get_error_data_result(f"You don't own the agent {agent_id}")
|
||||
|
||||
filtered_messages = [m for m in messages if m["role"] in ["user", "assistant"]]
|
||||
prompt_tokens = sum(len(tiktokenenc.encode(m["content"])) for m in filtered_messages)
|
||||
prompt_tokens = sum(len(tiktoken_encode.encode(m["content"])) for m in filtered_messages)
|
||||
if not filtered_messages:
|
||||
return jsonify(
|
||||
get_data_openai(
|
||||
@ -464,7 +464,7 @@ async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
||||
content="No valid messages found (user or assistant).",
|
||||
finish_reason="stop",
|
||||
model=req.get("model", ""),
|
||||
completion_tokens=len(tiktokenenc.encode("No valid messages found (user or assistant).")),
|
||||
completion_tokens=len(tiktoken_encode.encode("No valid messages found (user or assistant).")),
|
||||
prompt_tokens=prompt_tokens,
|
||||
)
|
||||
)
|
||||
@ -501,6 +501,8 @@ async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
||||
):
|
||||
return jsonify(response)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@manager.route("/agents/<agent_id>/completions", methods=["POST"]) # noqa: F821
|
||||
@token_required
|
||||
@ -920,6 +922,7 @@ async def chatbot_completions(dialog_id):
|
||||
async for answer in iframe_completion(dialog_id, **req):
|
||||
return get_result(data=answer)
|
||||
|
||||
return None
|
||||
|
||||
@manager.route("/chatbots/<dialog_id>/info", methods=["GET"]) # noqa: F821
|
||||
async def chatbots_inputs(dialog_id):
|
||||
@ -967,6 +970,7 @@ async def agent_bot_completions(agent_id):
|
||||
async for answer in agent_completion(objs[0].tenant_id, agent_id, **req):
|
||||
return get_result(data=answer)
|
||||
|
||||
return None
|
||||
|
||||
@manager.route("/agentbots/<agent_id>/inputs", methods=["GET"]) # noqa: F821
|
||||
async def begin_inputs(agent_id):
|
||||
|
||||
@ -660,7 +660,7 @@ def user_register(user_id, user):
|
||||
tenant_llm = get_init_tenant_llm(user_id)
|
||||
|
||||
if not UserService.save(**user):
|
||||
return
|
||||
return None
|
||||
TenantService.insert(**tenant)
|
||||
UserTenantService.insert(**usr_tenant)
|
||||
TenantLLMService.insert_many(tenant_llm)
|
||||
|
||||
@ -54,6 +54,7 @@ class RetCode(IntEnum, CustomEnum):
|
||||
SERVER_ERROR = 500
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
CONFLICT = 409
|
||||
|
||||
|
||||
class StatusEnum(Enum):
|
||||
|
||||
@ -64,15 +64,23 @@ class BlobStorageConnector(LoadConnector, PollConnector):
|
||||
|
||||
elif self.bucket_type == BlobType.S3:
|
||||
authentication_method = credentials.get("authentication_method", "access_key")
|
||||
|
||||
if authentication_method == "access_key":
|
||||
if not all(
|
||||
credentials.get(key)
|
||||
for key in ["aws_access_key_id", "aws_secret_access_key"]
|
||||
):
|
||||
raise ConnectorMissingCredentialError("Amazon S3")
|
||||
|
||||
elif authentication_method == "iam_role":
|
||||
if not credentials.get("aws_role_arn"):
|
||||
raise ConnectorMissingCredentialError("Amazon S3 IAM role ARN is required")
|
||||
|
||||
elif authentication_method == "assume_role":
|
||||
pass
|
||||
|
||||
else:
|
||||
raise ConnectorMissingCredentialError("Unsupported S3 authentication method")
|
||||
|
||||
elif self.bucket_type == BlobType.GOOGLE_CLOUD_STORAGE:
|
||||
if not all(
|
||||
@ -293,4 +301,4 @@ if __name__ == "__main__":
|
||||
except ConnectorMissingCredentialError as e:
|
||||
print(f"Error: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
@ -254,18 +254,21 @@ def create_s3_client(bucket_type: BlobType, credentials: dict[str, Any], europea
|
||||
elif bucket_type == BlobType.S3:
|
||||
authentication_method = credentials.get("authentication_method", "access_key")
|
||||
|
||||
region_name = credentials.get("region") or None
|
||||
|
||||
if authentication_method == "access_key":
|
||||
session = boto3.Session(
|
||||
aws_access_key_id=credentials["aws_access_key_id"],
|
||||
aws_secret_access_key=credentials["aws_secret_access_key"],
|
||||
region_name=region_name,
|
||||
)
|
||||
return session.client("s3")
|
||||
return session.client("s3", region_name=region_name)
|
||||
|
||||
elif authentication_method == "iam_role":
|
||||
role_arn = credentials["aws_role_arn"]
|
||||
|
||||
def _refresh_credentials() -> dict[str, str]:
|
||||
sts_client = boto3.client("sts")
|
||||
sts_client = boto3.client("sts", region_name=credentials.get("region") or None)
|
||||
assumed_role_object = sts_client.assume_role(
|
||||
RoleArn=role_arn,
|
||||
RoleSessionName=f"onyx_blob_storage_{int(datetime.now().timestamp())}",
|
||||
@ -285,11 +288,11 @@ def create_s3_client(bucket_type: BlobType, credentials: dict[str, Any], europea
|
||||
)
|
||||
botocore_session = get_session()
|
||||
botocore_session._credentials = refreshable
|
||||
session = boto3.Session(botocore_session=botocore_session)
|
||||
return session.client("s3")
|
||||
session = boto3.Session(botocore_session=botocore_session, region_name=region_name)
|
||||
return session.client("s3", region_name=region_name)
|
||||
|
||||
elif authentication_method == "assume_role":
|
||||
return boto3.client("s3")
|
||||
return boto3.client("s3", region_name=region_name)
|
||||
|
||||
else:
|
||||
raise ValueError("Invalid authentication method for S3.")
|
||||
|
||||
@ -75,9 +75,12 @@ def init_root_logger(logfile_basename: str, log_format: str = "%(asctime)-15s %(
|
||||
def log_exception(e, *args):
|
||||
logging.exception(e)
|
||||
for a in args:
|
||||
if hasattr(a, "text"):
|
||||
logging.error(a.text)
|
||||
raise Exception(a.text)
|
||||
else:
|
||||
logging.error(str(a))
|
||||
try:
|
||||
text = getattr(a, "text")
|
||||
except Exception:
|
||||
text = None
|
||||
if text is not None:
|
||||
logging.error(text)
|
||||
raise Exception(text)
|
||||
logging.error(str(a))
|
||||
raise e
|
||||
|
||||
@ -44,23 +44,23 @@ def total_token_count_from_response(resp):
|
||||
if resp is None:
|
||||
return 0
|
||||
|
||||
if hasattr(resp, "usage") and hasattr(resp.usage, "total_tokens"):
|
||||
try:
|
||||
try:
|
||||
if hasattr(resp, "usage") and hasattr(resp.usage, "total_tokens"):
|
||||
return resp.usage.total_tokens
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(resp, "usage_metadata") and hasattr(resp.usage_metadata, "total_tokens"):
|
||||
try:
|
||||
try:
|
||||
if hasattr(resp, "usage_metadata") and hasattr(resp.usage_metadata, "total_tokens"):
|
||||
return resp.usage_metadata.total_tokens
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(resp, "meta") and hasattr(resp.meta, "billed_units") and hasattr(resp.meta.billed_units, "input_tokens"):
|
||||
try:
|
||||
return resp.meta.billed_units.input_tokens
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(resp, "meta") and hasattr(resp.meta, "billed_units") and hasattr(resp.meta.billed_units, "input_tokens"):
|
||||
return resp.meta.billed_units.input_tokens
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if isinstance(resp, dict) and 'usage' in resp and 'total_tokens' in resp['usage']:
|
||||
try:
|
||||
@ -85,4 +85,3 @@ def total_token_count_from_response(resp):
|
||||
def truncate(string: str, max_len: int) -> str:
|
||||
"""Returns truncated text if the length of text exceed max_len."""
|
||||
return encoder.decode(encoder.encode(string)[:max_len])
|
||||
|
||||
|
||||
554
web/package-lock.json
generated
554
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,7 @@ import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
||||
import {
|
||||
ChunkMethodItem,
|
||||
EnableTocToggle,
|
||||
ImageContextWindow,
|
||||
ParseTypeItem,
|
||||
} from '@/pages/dataset/dataset-setting/configuration/common-item';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@ -119,6 +120,7 @@ export function ChunkMethodDialog({
|
||||
auto_questions: z.coerce.number().optional(),
|
||||
html4excel: z.boolean().optional(),
|
||||
toc_extraction: z.boolean().optional(),
|
||||
image_context_window: z.coerce.number().optional(),
|
||||
mineru_parse_method: z.enum(['auto', 'txt', 'ocr']).optional(),
|
||||
mineru_formula_enable: z.boolean().optional(),
|
||||
mineru_table_enable: z.boolean().optional(),
|
||||
@ -364,7 +366,10 @@ export function ChunkMethodDialog({
|
||||
className="space-y-3"
|
||||
>
|
||||
{selectedTag === DocumentParserType.Naive && (
|
||||
<EnableTocToggle />
|
||||
<>
|
||||
<EnableTocToggle />
|
||||
<ImageContextWindow />
|
||||
</>
|
||||
)}
|
||||
{showAutoKeywords(selectedTag) && (
|
||||
<>
|
||||
|
||||
@ -18,6 +18,7 @@ export function useDefaultParserValues() {
|
||||
auto_questions: 0,
|
||||
html4excel: false,
|
||||
toc_extraction: false,
|
||||
image_context_window: 0,
|
||||
mineru_parse_method: 'auto',
|
||||
mineru_formula_enable: true,
|
||||
mineru_table_enable: true,
|
||||
|
||||
@ -145,6 +145,8 @@ interface FileUploaderProps
|
||||
*/
|
||||
maxFileCount?: DropzoneProps['maxFiles'];
|
||||
|
||||
hideDropzoneOnMaxFileCount?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the uploader should accept multiple files.
|
||||
* @type boolean
|
||||
@ -178,6 +180,7 @@ export function FileUploader(props: FileUploaderProps) {
|
||||
maxFileCount = 100000000000,
|
||||
multiple = false,
|
||||
disabled = false,
|
||||
hideDropzoneOnMaxFileCount = false,
|
||||
className,
|
||||
title,
|
||||
description,
|
||||
@ -189,6 +192,8 @@ export function FileUploader(props: FileUploaderProps) {
|
||||
onChange: onValueChange,
|
||||
});
|
||||
|
||||
const reachesMaxFileCount = (files?.length ?? 0) >= maxFileCount;
|
||||
|
||||
const onDrop = React.useCallback(
|
||||
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
|
||||
@ -263,65 +268,68 @@ export function FileUploader(props: FileUploaderProps) {
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-6 overflow-hidden">
|
||||
<Dropzone
|
||||
onDrop={onDrop}
|
||||
accept={accept}
|
||||
maxSize={maxSize}
|
||||
maxFiles={maxFileCount}
|
||||
multiple={maxFileCount > 1 || multiple}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
'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-border-button',
|
||||
isDisabled && 'pointer-events-none opacity-60',
|
||||
className,
|
||||
)}
|
||||
{...dropzoneProps}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<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-text-secondary transition-colors group-hover:text-text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p className="font-medium text-text-secondary">
|
||||
Drop the files here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<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-text-secondary transition-colors group-hover:text-text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-px">
|
||||
{!(hideDropzoneOnMaxFileCount && reachesMaxFileCount) && (
|
||||
<Dropzone
|
||||
onDrop={onDrop}
|
||||
accept={accept}
|
||||
maxSize={maxSize}
|
||||
maxFiles={maxFileCount}
|
||||
multiple={maxFileCount > 1 || multiple}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
'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-border-button',
|
||||
isDisabled && 'pointer-events-none opacity-60',
|
||||
className,
|
||||
)}
|
||||
{...dropzoneProps}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<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-text-secondary transition-colors group-hover:text-text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p className="font-medium text-text-secondary">
|
||||
{title || t('knowledgeDetails.uploadTitle')}
|
||||
</p>
|
||||
<p className="text-sm text-text-disabled">
|
||||
{description || t('knowledgeDetails.uploadDescription')}
|
||||
{/* You can upload
|
||||
{maxFileCount > 1
|
||||
? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount}
|
||||
files (up to ${formatBytes(maxSize)} each)`
|
||||
: ` a file with ${formatBytes(maxSize)}`} */}
|
||||
Drop the files here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
) : (
|
||||
<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-text-secondary transition-colors group-hover:text-text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-px">
|
||||
<p className="font-medium text-text-secondary">
|
||||
{title || t('knowledgeDetails.uploadTitle')}
|
||||
</p>
|
||||
<p className="text-sm text-text-disabled">
|
||||
{description || t('knowledgeDetails.uploadDescription')}
|
||||
{/* You can upload
|
||||
{maxFileCount > 1
|
||||
? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount}
|
||||
files (up to ${formatBytes(maxSize)} each)`
|
||||
: ` a file with ${formatBytes(maxSize)}`} */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
)}
|
||||
|
||||
{files?.length ? (
|
||||
<div className="h-fit w-full px-3">
|
||||
<div className="flex max-h-48 flex-col gap-4 overflow-auto scrollbar-auto">
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { api_host } from '@/utils/api';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
|
||||
interface IImage {
|
||||
interface IImage extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
id: string;
|
||||
className?: string;
|
||||
onClick?(): void;
|
||||
t?: string | number;
|
||||
}
|
||||
|
||||
|
||||
170
web/src/components/list-filter-bar/filter-field.tsx
Normal file
170
web/src/components/list-filter-bar/filter-field.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
|
||||
import { FormControl, FormField, FormItem, FormLabel } from '../ui/form';
|
||||
import { FilterType } from './interface';
|
||||
const handleCheckChange = ({
|
||||
checked,
|
||||
field,
|
||||
item,
|
||||
isNestedField = false,
|
||||
parentId = '',
|
||||
}: {
|
||||
checked: boolean;
|
||||
field: ControllerRenderProps<
|
||||
{ [x: string]: { [x: string]: any } | string[] },
|
||||
string
|
||||
>;
|
||||
item: FilterType;
|
||||
isNestedField?: boolean;
|
||||
parentId?: string;
|
||||
}) => {
|
||||
if (isNestedField && parentId) {
|
||||
const currentValue = field.value || {};
|
||||
const currentParentValues =
|
||||
(currentValue as Record<string, string[]>)[parentId] || [];
|
||||
|
||||
const newParentValues = checked
|
||||
? [...currentParentValues, item.id.toString()]
|
||||
: currentParentValues.filter(
|
||||
(value: string) => value !== item.id.toString(),
|
||||
);
|
||||
|
||||
const newValue = {
|
||||
...currentValue,
|
||||
[parentId]: newParentValues,
|
||||
};
|
||||
|
||||
if (newValue[parentId].length === 0) {
|
||||
delete newValue[parentId];
|
||||
}
|
||||
|
||||
return field.onChange(newValue);
|
||||
} else {
|
||||
const list = checked
|
||||
? [...(Array.isArray(field.value) ? field.value : []), item.id.toString()]
|
||||
: (Array.isArray(field.value) ? field.value : []).filter(
|
||||
(value) => value !== item.id.toString(),
|
||||
);
|
||||
return field.onChange(list);
|
||||
}
|
||||
};
|
||||
|
||||
const FilterItem = memo(
|
||||
({
|
||||
item,
|
||||
field,
|
||||
level = 0,
|
||||
}: {
|
||||
item: FilterType;
|
||||
field: ControllerRenderProps<
|
||||
{ [x: string]: { [x: string]: any } | string[] },
|
||||
string
|
||||
>;
|
||||
level: number;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between text-text-primary text-xs ${level > 0 ? 'ml-4' : ''}`}
|
||||
>
|
||||
<FormItem className="flex flex-row space-x-3 space-y-0 items-center">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.id.toString())}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
handleCheckChange({ checked, field, item })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel onClick={(e) => e.stopPropagation()}>
|
||||
{item.label}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
{item.count !== undefined && (
|
||||
<span className="text-sm">{item.count}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const FilterField = memo(
|
||||
({
|
||||
item,
|
||||
parent,
|
||||
level = 0,
|
||||
}: {
|
||||
item: FilterType;
|
||||
parent: FilterType;
|
||||
level?: number;
|
||||
}) => {
|
||||
const form = useFormContext();
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const hasNestedList = item.list && item.list.length > 0;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name={parent.field as string}
|
||||
render={({ field }) => {
|
||||
if (hasNestedList) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 ${level > 0 ? 'ml-4' : ''}`}>
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => {
|
||||
setShowAll(!showAll);
|
||||
}}
|
||||
>
|
||||
<FormLabel className="text-text-primary">
|
||||
{item.label}
|
||||
</FormLabel>
|
||||
{showAll ? (
|
||||
<ChevronUp size={12} />
|
||||
) : (
|
||||
<ChevronDown size={12} />
|
||||
)}
|
||||
</div>
|
||||
{showAll &&
|
||||
item.list?.map((child) => (
|
||||
<FilterField
|
||||
key={child.id}
|
||||
item={child}
|
||||
parent={{
|
||||
...item,
|
||||
field: `${parent.field}.${item.field}`,
|
||||
}}
|
||||
level={level + 1}
|
||||
/>
|
||||
// <FilterItem key={child.id} item={child} field={child.field} level={level+1} />
|
||||
// <div
|
||||
// className="flex flex-row space-x-3 space-y-0 items-center"
|
||||
// key={child.id}
|
||||
// >
|
||||
// <FormControl>
|
||||
// <Checkbox
|
||||
// checked={field.value?.includes(child.id.toString())}
|
||||
// onCheckedChange={(checked) =>
|
||||
// handleCheckChange({ checked, field, item: child })
|
||||
// }
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormLabel onClick={(e) => e.stopPropagation()}>
|
||||
// {child.label}
|
||||
// </FormLabel>
|
||||
// </div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <FilterItem item={item} field={field} level={level} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FilterField.displayName = 'FilterField';
|
||||
@ -4,21 +4,27 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ZodArray, ZodString, z } from 'zod';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { t } from 'i18next';
|
||||
import { FilterField } from './filter-field';
|
||||
import { FilterChange, FilterCollection, FilterValue } from './interface';
|
||||
|
||||
export type CheckboxFormMultipleProps = {
|
||||
@ -35,29 +41,71 @@ function CheckboxFormMultiple({
|
||||
onChange,
|
||||
setOpen,
|
||||
}: CheckboxFormMultipleProps) {
|
||||
const fieldsDict = filters?.reduce<Record<string, Array<any>>>((pre, cur) => {
|
||||
pre[cur.field] = [];
|
||||
return pre;
|
||||
}, {});
|
||||
const [resolvedFilters, setResolvedFilters] =
|
||||
useState<FilterCollection[]>(filters);
|
||||
|
||||
const FormSchema = z.object(
|
||||
filters.reduce<Record<string, ZodArray<ZodString, 'many'>>>((pre, cur) => {
|
||||
pre[cur.field] = z.array(z.string());
|
||||
useEffect(() => {
|
||||
if (filters && filters.length > 0) {
|
||||
setResolvedFilters(filters);
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
// .refine((value) => value.some((item) => item), {
|
||||
// message: 'You have to select at least one item.',
|
||||
// });
|
||||
const fieldsDict = useMemo(() => {
|
||||
if (resolvedFilters.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return resolvedFilters.reduce<Record<string, any>>((pre, cur) => {
|
||||
const hasNested = cur.list?.some(
|
||||
(item) => item.list && item.list.length > 0,
|
||||
);
|
||||
|
||||
if (hasNested) {
|
||||
pre[cur.field] = {};
|
||||
} else {
|
||||
pre[cur.field] = [];
|
||||
}
|
||||
return pre;
|
||||
}, {}),
|
||||
);
|
||||
}, {});
|
||||
}, [resolvedFilters]);
|
||||
|
||||
const FormSchema = useMemo(() => {
|
||||
if (resolvedFilters.length === 0) {
|
||||
return z.object({});
|
||||
}
|
||||
|
||||
return z.object(
|
||||
resolvedFilters.reduce<
|
||||
Record<
|
||||
string,
|
||||
ZodArray<ZodString, 'many'> | z.ZodObject<any> | z.ZodOptional<any>
|
||||
>
|
||||
>((pre, cur) => {
|
||||
const hasNested = cur.list?.some(
|
||||
(item) => item.list && item.list.length > 0,
|
||||
);
|
||||
|
||||
if (hasNested) {
|
||||
pre[cur.field] = z
|
||||
.record(z.string(), z.array(z.string().optional()).optional())
|
||||
.optional();
|
||||
} else {
|
||||
pre[cur.field] = z.array(z.string().optional()).optional();
|
||||
}
|
||||
|
||||
return pre;
|
||||
}, {}),
|
||||
);
|
||||
}, [resolvedFilters]);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
resolver: resolvedFilters.length > 0 ? zodResolver(FormSchema) : undefined,
|
||||
defaultValues: fieldsDict,
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
onChange?.(data);
|
||||
function onSubmit() {
|
||||
const formValues = form.getValues();
|
||||
onChange?.({ ...formValues });
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
@ -67,8 +115,10 @@ function CheckboxFormMultiple({
|
||||
}, [fieldsDict, onChange, setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(value);
|
||||
}, [form, value]);
|
||||
if (resolvedFilters.length > 0) {
|
||||
form.reset(value || fieldsDict);
|
||||
}
|
||||
}, [form, value, resolvedFilters, fieldsDict]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@ -85,44 +135,21 @@ function CheckboxFormMultiple({
|
||||
render={() => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel className="text-base text-text-sub-title-invert">
|
||||
{x.label}
|
||||
</FormLabel>
|
||||
<FormLabel className="text-text-primary">{x.label}</FormLabel>
|
||||
</div>
|
||||
{x.list.map((item) => (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name={x.field}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between text-text-primary text-xs">
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row space-x-3 space-y-0 items-center "
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, item.id])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== item.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>{item.label}</FormLabel>
|
||||
</FormItem>
|
||||
<span className=" text-sm">{item.count}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{x.list.map((item) => {
|
||||
return (
|
||||
<FilterField
|
||||
key={item.id}
|
||||
item={{ ...item }}
|
||||
parent={{
|
||||
...x,
|
||||
id: x.field,
|
||||
// field: `${x.field}${item.field ? '.' + item.field : ''}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@ -137,7 +164,13 @@ function CheckboxFormMultiple({
|
||||
>
|
||||
{t('common.clear')}
|
||||
</Button>
|
||||
<Button type="submit" size={'sm'}>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
console.log(form.formState.errors, form.getValues());
|
||||
}}
|
||||
size={'sm'}
|
||||
>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,7 @@ interface IProps {
|
||||
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
showFilter?: boolean;
|
||||
leftPanel?: ReactNode;
|
||||
preChildren?: ReactNode;
|
||||
}
|
||||
|
||||
export const FilterButton = React.forwardRef<
|
||||
@ -46,6 +47,7 @@ export const FilterButton = React.forwardRef<
|
||||
export default function ListFilterBar({
|
||||
title,
|
||||
children,
|
||||
preChildren,
|
||||
searchString,
|
||||
onSearchChange,
|
||||
showFilter = true,
|
||||
@ -63,7 +65,18 @@ export default function ListFilterBar({
|
||||
const filterCount = useMemo(() => {
|
||||
return typeof value === 'object' && value !== null
|
||||
? Object.values(value).reduce((pre, cur) => {
|
||||
return pre + cur.length;
|
||||
if (Array.isArray(cur)) {
|
||||
return pre + cur.length;
|
||||
}
|
||||
if (typeof cur === 'object') {
|
||||
return (
|
||||
pre +
|
||||
Object.values(cur).reduce((pre, cur) => {
|
||||
return pre + cur.length;
|
||||
}, 0)
|
||||
);
|
||||
}
|
||||
return pre;
|
||||
}, 0)
|
||||
: 0;
|
||||
}, [value]);
|
||||
@ -80,6 +93,7 @@ export default function ListFilterBar({
|
||||
{leftPanel || title}
|
||||
</div>
|
||||
<div className="flex gap-5 items-center">
|
||||
{preChildren}
|
||||
{showFilter && (
|
||||
<FilterPopover
|
||||
value={value}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
export type FilterType = {
|
||||
id: string;
|
||||
field?: string;
|
||||
label: string | JSX.Element;
|
||||
list?: FilterType[];
|
||||
value?: string | string[];
|
||||
count?: number;
|
||||
};
|
||||
|
||||
@ -10,6 +13,9 @@ export type FilterCollection = {
|
||||
list: FilterType[];
|
||||
};
|
||||
|
||||
export type FilterValue = Record<string, Array<string>>;
|
||||
export type FilterValue = Record<
|
||||
string,
|
||||
Array<string> | Record<string, Array<string>>
|
||||
>;
|
||||
|
||||
export type FilterChange = (value: FilterValue) => void;
|
||||
|
||||
@ -5,7 +5,6 @@ import { getExtension } from '@/utils/document-util';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
@ -18,7 +17,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
|
||||
import {
|
||||
currentReg,
|
||||
preprocessLaTeX,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
@ -31,6 +29,11 @@ import classNames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
import { pipe } from 'lodash/fp';
|
||||
import { CircleAlert } from 'lucide-react';
|
||||
import { ImageCarousel } from '../markdown-content/image-carousel';
|
||||
import {
|
||||
groupConsecutiveReferences,
|
||||
shouldShowCarousel,
|
||||
} from '../markdown-content/reference-utils';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
HoverCard,
|
||||
@ -39,6 +42,19 @@ import {
|
||||
} from '../ui/hover-card';
|
||||
import styles from './index.less';
|
||||
|
||||
// Helper function to convert IReferenceObject to IReference
|
||||
const convertReferenceObjectToReference = (
|
||||
referenceObject: IReferenceObject,
|
||||
) => {
|
||||
const chunks = Object.values(referenceObject.chunks);
|
||||
const docAggs = Object.values(referenceObject.doc_aggs);
|
||||
return {
|
||||
chunks,
|
||||
doc_aggs: docAggs,
|
||||
total: chunks.length,
|
||||
};
|
||||
};
|
||||
|
||||
const getChunkIndex = (match: string) => Number(match);
|
||||
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
||||
function MarkdownContent({
|
||||
@ -211,47 +227,95 @@ function MarkdownContent({
|
||||
|
||||
const renderReference = useCallback(
|
||||
(text: string) => {
|
||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const chunkIndex = getChunkIndex(match);
|
||||
const groups = groupConsecutiveReferences(text);
|
||||
const elements = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
|
||||
getReferenceInfo(chunkIndex);
|
||||
const convertedReference = reference
|
||||
? convertReferenceObjectToReference(reference)
|
||||
: null;
|
||||
|
||||
const docType = chunkItem?.doc_type;
|
||||
groups.forEach((group, groupIndex) => {
|
||||
if (group[0].start > lastIndex) {
|
||||
elements.push(text.substring(lastIndex, group[0].start));
|
||||
}
|
||||
|
||||
return showImage(docType) ? (
|
||||
<section>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceInnerChunkImage}
|
||||
onClick={
|
||||
documentId
|
||||
? handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
)
|
||||
: () => {}
|
||||
}
|
||||
></Image>
|
||||
<span className="text-accent-primary">{imageId}</span>
|
||||
</section>
|
||||
) : (
|
||||
<HoverCard key={i}>
|
||||
<HoverCardTrigger>
|
||||
<CircleAlert className="size-4 inline-block" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-w-3xl">
|
||||
{renderPopoverContent(chunkIndex)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
if (
|
||||
convertedReference &&
|
||||
shouldShowCarousel(group, convertedReference)
|
||||
) {
|
||||
elements.push(
|
||||
<ImageCarousel
|
||||
key={`carousel-${groupIndex}`}
|
||||
group={group}
|
||||
reference={convertedReference}
|
||||
fileThumbnails={fileThumbnails}
|
||||
onImageClick={handleDocumentButtonClick}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
group.forEach((ref) => {
|
||||
const chunkIndex = getChunkIndex(ref.id);
|
||||
const {
|
||||
documentUrl,
|
||||
fileExtension,
|
||||
imageId,
|
||||
chunkItem,
|
||||
documentId,
|
||||
} = getReferenceInfo(chunkIndex);
|
||||
const docType = chunkItem?.doc_type;
|
||||
|
||||
if (showImage(docType)) {
|
||||
elements.push(
|
||||
<section key={ref.id}>
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceInnerChunkImage}
|
||||
onClick={
|
||||
documentId
|
||||
? handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
fileExtension === 'pdf',
|
||||
documentUrl,
|
||||
)
|
||||
: () => {}
|
||||
}
|
||||
/>
|
||||
<span className="text-accent-primary"> {imageId}</span>
|
||||
</section>,
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<HoverCard key={ref.id}>
|
||||
<HoverCardTrigger>
|
||||
<CircleAlert className="size-4 inline-block" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-w-3xl">
|
||||
{renderPopoverContent(chunkIndex)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = group[group.length - 1].end;
|
||||
});
|
||||
|
||||
return replacedText;
|
||||
if (lastIndex < text.length) {
|
||||
elements.push(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
[renderPopoverContent, getReferenceInfo, handleDocumentButtonClick],
|
||||
[
|
||||
renderPopoverContent,
|
||||
getReferenceInfo,
|
||||
handleDocumentButtonClick,
|
||||
reference,
|
||||
fileThumbnails,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
19
web/src/components/webhook-response-status.tsx
Normal file
19
web/src/components/webhook-response-status.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RAGFlowFormItem } from './ragflow-form';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
type WebHookResponseStatusFormFieldProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function WebHookResponseStatusFormField({
|
||||
name,
|
||||
}: WebHookResponseStatusFormFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem name={name} label={t('flow.webhook.status')}>
|
||||
<Input type="number"></Input>
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
@ -124,6 +124,7 @@ export const useFetchDocumentList = () => {
|
||||
{
|
||||
suffix: filterValue.type,
|
||||
run_status: filterValue.run,
|
||||
metadata: filterValue.metadata,
|
||||
},
|
||||
);
|
||||
if (ret.data.code === 0) {
|
||||
@ -196,6 +197,7 @@ export const useGetDocumentFilter = (): {
|
||||
filter: data?.filter || {
|
||||
run_status: {},
|
||||
suffix: {},
|
||||
metadata: {},
|
||||
},
|
||||
onOpenChange: handleOnpenChange,
|
||||
};
|
||||
|
||||
@ -60,4 +60,5 @@ interface GraphRag {
|
||||
export type IDocumentInfoFilter = {
|
||||
run_status: Record<number, number>;
|
||||
suffix: Record<string, number>;
|
||||
metadata: Record<string, Record<string, number>>;
|
||||
};
|
||||
|
||||
@ -116,12 +116,15 @@ export interface ITenantInfo {
|
||||
tts_id: string;
|
||||
}
|
||||
|
||||
export type ChunkDocType = 'image' | 'table';
|
||||
|
||||
export interface IChunk {
|
||||
available_int: number; // Whether to enable, 0: not enabled, 1: enabled
|
||||
chunk_id: string;
|
||||
content_with_weight: string;
|
||||
doc_id: string;
|
||||
doc_name: string;
|
||||
doc_type_kwd?: ChunkDocType;
|
||||
image_id: string;
|
||||
important_kwd?: string[];
|
||||
question_kwd?: string[]; // keywords
|
||||
|
||||
@ -364,6 +364,9 @@ Procedural Memory: Learned skills, habits, and automated procedures.`,
|
||||
},
|
||||
knowledgeConfiguration: {
|
||||
settings: 'Settings',
|
||||
imageContextWindow: 'Image context window',
|
||||
imageContextWindowTip:
|
||||
'Captures N tokens of text above and below the image to provide richer background context for the image chunk.',
|
||||
autoMetadata: 'Auto metadata',
|
||||
mineruOptions: 'MinerU Options',
|
||||
mineruParseMethod: 'Parse Method',
|
||||
@ -604,6 +607,12 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
'The document being parsed cannot be deleted',
|
||||
},
|
||||
chunk: {
|
||||
type: 'Type',
|
||||
docType: {
|
||||
image: 'Image',
|
||||
table: 'Table',
|
||||
text: 'Text',
|
||||
},
|
||||
chunk: 'Chunk',
|
||||
bulk: 'Bulk',
|
||||
selectAll: 'Select all',
|
||||
@ -2084,12 +2093,14 @@ Important structured information may include: names, dates, locations, events, k
|
||||
schema: 'Schema',
|
||||
response: 'Response',
|
||||
executionMode: 'Execution mode',
|
||||
executionModeTip:
|
||||
'Accepted Response: The system returns an acknowledgment immediately after the request is validated, while the workflow continues to execute asynchronously in the background. /Final Response: The system returns a response only after the workflow execution is completed.',
|
||||
authMethods: 'Authentication methods',
|
||||
authType: 'Authentication type',
|
||||
limit: 'Request limit',
|
||||
per: 'Time period',
|
||||
maxBodySize: 'Maximum body size',
|
||||
ipWhitelist: 'Ip whitelist',
|
||||
ipWhitelist: 'IP whitelist',
|
||||
tokenHeader: 'Token header',
|
||||
tokenValue: 'Token value',
|
||||
username: 'Username',
|
||||
@ -2109,6 +2120,8 @@ Important structured information may include: names, dates, locations, events, k
|
||||
queryParameters: 'Query parameters',
|
||||
headerParameters: 'Header parameters',
|
||||
requestBodyParameters: 'Request body parameters',
|
||||
streaming: 'Accepted response',
|
||||
immediately: 'Final response',
|
||||
},
|
||||
},
|
||||
llmTools: {
|
||||
|
||||
@ -1835,6 +1835,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
schema: '模式',
|
||||
response: '响应',
|
||||
executionMode: '执行模式',
|
||||
executionModeTip:
|
||||
'Accepted Response:请求校验通过后立即返回接收成功响应,工作流在后台异步执行;Final Response:系统在工作流执行完成后返回最终处理结果',
|
||||
authMethods: '认证方法',
|
||||
authType: '认证类型',
|
||||
limit: '请求限制',
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { WebHookResponseStatusFormField } from '@/components/webhook-response-status';
|
||||
import { WebhookExecutionMode } from '@/pages/agent/constant';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
@ -24,19 +24,17 @@ export function WebhookResponse() {
|
||||
<RAGFlowFormItem
|
||||
name="execution_mode"
|
||||
label={t('flow.webhook.executionMode')}
|
||||
tooltip={t('flow.webhook.executionModeTip')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={buildOptions(WebhookExecutionMode)}
|
||||
options={buildOptions(WebhookExecutionMode, t, 'flow.webhook')}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
{executionMode === WebhookExecutionMode.Immediately && (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
<WebHookResponseStatusFormField
|
||||
name={'response.status'}
|
||||
label={t('flow.webhook.status')}
|
||||
>
|
||||
<Input type="number"></Input>
|
||||
</RAGFlowFormItem>
|
||||
></WebHookResponseStatusFormField>
|
||||
{/* <DynamicResponse
|
||||
name="response.headers_template"
|
||||
label={t('flow.webhook.headersTemplate')}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { RAGFlowSelect } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { WebHookResponseStatusFormField } from '@/components/webhook-response-status';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { X } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
@ -20,6 +21,7 @@ import { ExportFileType } from '../../constant';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { FormWrapper } from '../components/form-wrapper';
|
||||
import { PromptEditor } from '../components/prompt-editor';
|
||||
import { useShowWebhookResponseStatus } from './use-show-response-status';
|
||||
import { useValues } from './use-values';
|
||||
import { useWatchFormChange } from './use-watch-change';
|
||||
|
||||
@ -38,6 +40,7 @@ function MessageForm({ node }: INextOperatorForm) {
|
||||
.optional(),
|
||||
output_format: z.string().optional(),
|
||||
auto_play: z.boolean().optional(),
|
||||
status: z.number().optional(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
@ -56,9 +59,14 @@ function MessageForm({ node }: INextOperatorForm) {
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const showWebhookResponseStatus = useShowWebhookResponseStatus(form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
{showWebhookResponseStatus && (
|
||||
<WebHookResponseStatusFormField name="status"></WebHookResponseStatusFormField>
|
||||
)}
|
||||
<FormContainer>
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel>
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
AgentDialogueMode,
|
||||
BeginId,
|
||||
WebhookExecutionMode,
|
||||
} from '../../constant';
|
||||
import useGraphStore from '../../store';
|
||||
import { BeginFormSchemaType } from '../begin-form/schema';
|
||||
|
||||
export function useShowWebhookResponseStatus(form: UseFormReturn<any>) {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
const showWebhookResponseStatus = useMemo(() => {
|
||||
const formData: BeginFormSchemaType = getNode(BeginId)?.data.form;
|
||||
return (
|
||||
formData.mode === AgentDialogueMode.Webhook &&
|
||||
formData.execution_mode === WebhookExecutionMode.Streaming
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showWebhookResponseStatus && isEmpty(form.getValues('status'))) {
|
||||
form.setValue('status', 200, { shouldValidate: true, shouldDirty: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return showWebhookResponseStatus;
|
||||
}
|
||||
@ -4,6 +4,8 @@
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 50vw;
|
||||
max-height: 50vh;
|
||||
object-fit: contain;
|
||||
|
||||
@ -2,17 +2,19 @@ import Image from '@/components/image';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckedState } from '@radix-ui/react-checkbox';
|
||||
import classNames from 'classnames';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChunkTextMode } from '../../constant';
|
||||
import styles from './index.less';
|
||||
|
||||
@ -39,6 +41,7 @@ const ChunkCard = ({
|
||||
textMode,
|
||||
t: imageCacheKey,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const available = Number(item.available_int);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
@ -63,51 +66,59 @@ const ChunkCard = ({
|
||||
useEffect(() => {
|
||||
setEnabled(available === 1);
|
||||
}, [available]);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={classNames(styles.chunkCard, {
|
||||
className={classNames('relative flex-none', styles.chunkCard, {
|
||||
[`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]:
|
||||
selected,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className="
|
||||
absolute top-0 right-0 px-4 py-1
|
||||
leading-none text-xs text-text-disabled
|
||||
bg-bg-card rounded-bl-2xl rounded-tr-lg
|
||||
border-l-0.5 border-b-0.5 border-border-button"
|
||||
>
|
||||
{t(
|
||||
`chunk.docType.${item.doc_type_kwd ? String(item.doc_type_kwd).toLowerCase() : 'text'}`,
|
||||
)}
|
||||
</span>
|
||||
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<Checkbox onCheckedChange={handleCheck} checked={checked}></Checkbox>
|
||||
|
||||
{/* Using <Tooltip> instead of <Popover> to avoid flickering when hovering over the image */}
|
||||
{item.image_id && (
|
||||
<Popover open={open}>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
>
|
||||
<div>
|
||||
<Image
|
||||
t={imageCacheKey}
|
||||
id={item.image_id}
|
||||
className={styles.image}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Image
|
||||
t={imageCacheKey}
|
||||
id={item.image_id}
|
||||
className={styles.image}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="p-0"
|
||||
align={'start'}
|
||||
side={'right'}
|
||||
sideOffset={-20}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div>
|
||||
<Image
|
||||
t={imageCacheKey}
|
||||
id={item.image_id}
|
||||
className={styles.imagePreview}
|
||||
></Image>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Image
|
||||
t={imageCacheKey}
|
||||
id={item.image_id}
|
||||
className={styles.imagePreview}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<section
|
||||
onDoubleClick={handleContentDoubleClick}
|
||||
onClick={handleContentClick}
|
||||
className={styles.content}
|
||||
className={cn(styles.content, 'mt-2')}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
@ -118,7 +129,8 @@ const ChunkCard = ({
|
||||
})}
|
||||
></div>
|
||||
</section>
|
||||
<div>
|
||||
|
||||
<div className="mt-2">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={onChange}
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import Space from '@/components/ui/space';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@ -76,6 +77,7 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
||||
const { removeChunk } = useDeleteChunkByIds();
|
||||
const { data } = useFetchChunk(chunkId);
|
||||
const { t } = useTranslation();
|
||||
const isEditMode = !!chunkId;
|
||||
|
||||
const isTagParser = parserId === 'tag';
|
||||
const onSubmit = useCallback(
|
||||
@ -144,6 +146,28 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Do not display the type field in create mode */}
|
||||
{isEditMode && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="doc_type_kwd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t(`chunk.type`)}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
value={t(
|
||||
`chunk.docType.${field.value ? String(field.value).toLowerCase() : 'text'}`,
|
||||
)}
|
||||
readOnly
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="image"
|
||||
@ -151,18 +175,18 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
||||
<FormItem>
|
||||
<FormLabel className="gap-1">{t('chunk.image')}</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<div className="space-y-4">
|
||||
{data?.data?.img_id && (
|
||||
<Image
|
||||
id={data?.data?.img_id}
|
||||
className="w-full object-contain"
|
||||
className="mx-auto w-auto max-w-full object-contain max-h-[800px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="col-start-2 col-end-3 only:col-span-2">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
className="h-48"
|
||||
className="h-auto p-6"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
accept={{
|
||||
@ -171,6 +195,7 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
||||
'image/webp': [],
|
||||
}}
|
||||
maxFileCount={1}
|
||||
hideDropzoneOnMaxFileCount
|
||||
title={t('chunk.imageUploaderTitle')}
|
||||
description={<></>}
|
||||
/>
|
||||
|
||||
@ -45,7 +45,7 @@ export default (props: ICheckboxSetProps) => {
|
||||
}, [selectedChunkIds]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-[40px] py-4 px-2">
|
||||
<div className="flex gap-[40px] py-4 px-2 h-14">
|
||||
<div className="flex items-center gap-3 cursor-pointer text-muted-foreground hover:text-text-primary">
|
||||
<Checkbox
|
||||
id="all_chunks_checkbox"
|
||||
|
||||
@ -204,7 +204,8 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{metaData.restrictDefinedValues && (
|
||||
{((metaData.restrictDefinedValues && isShowValueSwitch) ||
|
||||
!isShowValueSwitch) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>{t('knowledgeDetails.metadata.values')}</div>
|
||||
|
||||
@ -310,6 +310,36 @@ export function EnableTocToggle() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageContextWindow() {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parser_config.image_context_window"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SliderInputFormField
|
||||
{...field}
|
||||
label={t('imageContextWindow')}
|
||||
tooltip={t('imageContextWindowTip')}
|
||||
defaultValue={0}
|
||||
min={0}
|
||||
max={256}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverlappedPercent() {
|
||||
return (
|
||||
<SliderInputFormField
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
AutoMetadata,
|
||||
EnableTocToggle,
|
||||
ImageContextWindow,
|
||||
OverlappedPercent,
|
||||
} from './common-item';
|
||||
|
||||
@ -26,6 +27,7 @@ export function NaiveConfiguration() {
|
||||
<DelimiterFormField></DelimiterFormField>
|
||||
<ChildrenDelimiterForm />
|
||||
<EnableTocToggle />
|
||||
<ImageContextWindow />
|
||||
<AutoMetadata />
|
||||
<OverlappedPercent />
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
@ -32,6 +32,7 @@ export const formSchema = z
|
||||
tag_kb_ids: z.array(z.string()).nullish(),
|
||||
topn_tags: z.number().optional(),
|
||||
toc_extraction: z.boolean().optional(),
|
||||
image_context_window: z.number().optional(),
|
||||
overlapped_percent: z.number().optional(),
|
||||
// MinerU-specific options
|
||||
mineru_parse_method: z.enum(['auto', 'txt', 'ocr']).optional(),
|
||||
|
||||
@ -70,6 +70,7 @@ export default function DatasetSettings() {
|
||||
html4excel: false,
|
||||
topn_tags: 3,
|
||||
toc_extraction: false,
|
||||
image_context_window: 0,
|
||||
overlapped_percent: 0,
|
||||
// MinerU-specific defaults
|
||||
mineru_parse_method: 'auto',
|
||||
|
||||
@ -216,12 +216,6 @@ const Generate: React.FC<GenerateProps> = (props) => {
|
||||
? graphRunData
|
||||
: raptorRunData
|
||||
) as ITraceInfo;
|
||||
console.log(
|
||||
name,
|
||||
'data',
|
||||
data,
|
||||
!data || (!data.progress && data.progress !== 0),
|
||||
);
|
||||
return (
|
||||
<div key={name}>
|
||||
<MenuItem
|
||||
|
||||
@ -25,12 +25,33 @@ export function useSelectDatasetFilters() {
|
||||
}));
|
||||
}
|
||||
}, [filter.run_status, t]);
|
||||
const metaDataList = useMemo(() => {
|
||||
if (filter.metadata) {
|
||||
return Object.keys(filter.metadata).map((x) => ({
|
||||
id: x.toString(),
|
||||
field: x.toString(),
|
||||
label: x.toString(),
|
||||
list: Object.keys(filter.metadata[x]).map((y) => ({
|
||||
id: y.toString(),
|
||||
field: y.toString(),
|
||||
label: y.toString(),
|
||||
value: [y],
|
||||
count: filter.metadata[x][y],
|
||||
})),
|
||||
count: Object.keys(filter.metadata[x]).reduce(
|
||||
(acc, cur) => acc + filter.metadata[x][cur],
|
||||
0,
|
||||
),
|
||||
}));
|
||||
}
|
||||
}, [filter.metadata]);
|
||||
const filters: FilterCollection[] = useMemo(() => {
|
||||
return [
|
||||
{ field: 'type', label: 'File Type', list: fileTypes },
|
||||
{ field: 'run', label: 'Status', list: fileStatus },
|
||||
{ field: 'metadata', label: 'metadata', list: metaDataList },
|
||||
] as FilterCollection[];
|
||||
}, [fileStatus, fileTypes]);
|
||||
}, [fileStatus, fileTypes, metaDataList]);
|
||||
|
||||
return { filters, onOpenChange };
|
||||
}
|
||||
|
||||
@ -4,9 +4,8 @@ import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
@ -18,8 +17,13 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
|
||||
import ImageCarousel from '@/components/markdown-content/image-carousel';
|
||||
import {
|
||||
groupConsecutiveReferences,
|
||||
shouldShowCarousel,
|
||||
type ReferenceGroup,
|
||||
} from '@/components/markdown-content/reference-utils';
|
||||
import {
|
||||
currentReg,
|
||||
preprocessLaTeX,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
@ -37,8 +41,6 @@ import classNames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
import { pipe } from 'lodash/fp';
|
||||
|
||||
const getChunkIndex = (match: string) => Number(match);
|
||||
|
||||
// Defining Tailwind CSS class name constants
|
||||
const styles = {
|
||||
referenceChunkImage: 'w-[10vw] object-contain',
|
||||
@ -86,18 +88,11 @@ const MarkdownContent = ({
|
||||
(
|
||||
documentId: string,
|
||||
chunk: IReferenceChunk,
|
||||
// isPdf: boolean,
|
||||
// documentUrl?: string,
|
||||
isPdf: boolean = false,
|
||||
documentUrl?: string,
|
||||
) =>
|
||||
() => {
|
||||
// if (!isPdf) {
|
||||
// if (!documentUrl) {
|
||||
// return;
|
||||
// }
|
||||
// window.open(documentUrl, '_blank');
|
||||
// } else {
|
||||
clickDocumentButton?.(documentId, chunk);
|
||||
// }
|
||||
},
|
||||
[clickDocumentButton],
|
||||
);
|
||||
@ -218,43 +213,83 @@ const MarkdownContent = ({
|
||||
|
||||
const renderReference = useCallback(
|
||||
(text: string) => {
|
||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
||||
const chunkIndex = getChunkIndex(match);
|
||||
const groups = groupConsecutiveReferences(text);
|
||||
const elements: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
const { imageId, chunkItem, documentId } = getReferenceInfo(chunkIndex);
|
||||
groups.forEach((group: ReferenceGroup) => {
|
||||
// Add text before the group
|
||||
if (group[0].start > lastIndex) {
|
||||
elements.push(text.slice(lastIndex, group[0].start));
|
||||
}
|
||||
|
||||
const docType = chunkItem?.doc_type;
|
||||
// Determine if this group should be a carousel
|
||||
if (shouldShowCarousel(group, reference)) {
|
||||
// Render carousel for consecutive image group
|
||||
elements.push(
|
||||
<ImageCarousel
|
||||
key={`carousel-${group[0].id}`}
|
||||
group={group}
|
||||
reference={reference}
|
||||
fileThumbnails={fileThumbnails}
|
||||
onImageClick={handleDocumentButtonClick}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
// Render individual references in the group
|
||||
group.forEach((ref) => {
|
||||
const chunkIndex = Number(ref.id);
|
||||
const { imageId, chunkItem, documentId } =
|
||||
getReferenceInfo(chunkIndex);
|
||||
const docType = chunkItem?.doc_type;
|
||||
|
||||
return showImage(docType) ? (
|
||||
<Image
|
||||
id={imageId}
|
||||
className={styles.referenceInnerChunkImage}
|
||||
onClick={
|
||||
documentId
|
||||
? handleDocumentButtonClick(
|
||||
documentId,
|
||||
chunkItem,
|
||||
// fileExtension === 'pdf',
|
||||
// documentUrl,
|
||||
)
|
||||
: () => {}
|
||||
if (showImage(docType)) {
|
||||
elements.push(
|
||||
<Image
|
||||
key={ref.id}
|
||||
id={imageId}
|
||||
className={styles.referenceInnerChunkImage}
|
||||
onClick={
|
||||
documentId
|
||||
? handleDocumentButtonClick(documentId, chunkItem)
|
||||
: () => {}
|
||||
}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<Popover key={ref.id}>
|
||||
<PopoverTrigger>
|
||||
<InfoCircleOutlined className={styles.referenceIcon} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="!w-fit">
|
||||
{getPopoverContent(chunkIndex)}
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
);
|
||||
}
|
||||
></Image>
|
||||
) : (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<InfoCircleOutlined className={styles.referenceIcon} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="!w-fit">
|
||||
{getPopoverContent(chunkIndex)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
// Add the original reference text
|
||||
elements.push(ref.fullMatch);
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = group[group.length - 1].end;
|
||||
});
|
||||
|
||||
return replacedText;
|
||||
// Add any remaining text after the last group
|
||||
if (lastIndex < text.length) {
|
||||
elements.push(text.slice(lastIndex));
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
[getPopoverContent, getReferenceInfo, handleDocumentButtonClick],
|
||||
[
|
||||
reference,
|
||||
fileThumbnails,
|
||||
handleDocumentButtonClick,
|
||||
getReferenceInfo,
|
||||
getPopoverContent,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -265,7 +300,11 @@ const MarkdownContent = ({
|
||||
components={
|
||||
{
|
||||
'custom-typography': ({ children }: { children: string }) =>
|
||||
renderReference(children),
|
||||
Array.isArray(renderReference(children))
|
||||
? renderReference(children).map((element, index) => (
|
||||
<React.Fragment key={index}>{element}</React.Fragment>
|
||||
))
|
||||
: renderReference(children),
|
||||
code(props: any) {
|
||||
const { children, className, ...rest } = props;
|
||||
const restProps = omit(rest, 'node');
|
||||
|
||||
@ -0,0 +1,247 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Segmented } from '@/components/ui/segmented';
|
||||
import { t } from 'i18next';
|
||||
|
||||
// UI-only auth modes for S3
|
||||
// access_key: Access Key ID + Secret
|
||||
// iam_role: only Role ARN
|
||||
// assume_role: no input fields (uses environment role)
|
||||
type AuthMode = 'access_key' | 'iam_role' | 'assume_role';
|
||||
type BlobMode = 's3' | 's3_compatible';
|
||||
|
||||
const modeOptions = [
|
||||
{ label: 'S3', value: 's3' },
|
||||
{ label: 'S3 Compatible', value: 's3_compatible' },
|
||||
];
|
||||
|
||||
const authOptions = [
|
||||
{ label: 'Access Key', value: 'access_key' },
|
||||
{ label: 'IAM Role', value: 'iam_role' },
|
||||
{ label: 'Assume Role', value: 'assume_role' },
|
||||
];
|
||||
|
||||
const addressingOptions = [
|
||||
{ label: 'Virtual Hosted Style', value: 'virtual' },
|
||||
{ label: 'Path Style', value: 'path' },
|
||||
];
|
||||
|
||||
const deriveInitialAuthMode = (credentials: any): AuthMode => {
|
||||
const authMethod = credentials?.authentication_method;
|
||||
if (authMethod === 'iam_role') return 'iam_role';
|
||||
if (authMethod === 'assume_role') return 'assume_role';
|
||||
if (credentials?.aws_role_arn) return 'iam_role';
|
||||
if (credentials?.aws_access_key_id || credentials?.aws_secret_access_key)
|
||||
return 'access_key';
|
||||
return 'access_key';
|
||||
};
|
||||
|
||||
const deriveInitialMode = (bucketType?: string): BlobMode =>
|
||||
bucketType === 's3_compatible' ? 's3_compatible' : 's3';
|
||||
|
||||
const BlobTokenField = () => {
|
||||
const form = useFormContext();
|
||||
const credentials = form.watch('config.credentials');
|
||||
const watchedBucketType = form.watch('config.bucket_type');
|
||||
|
||||
const [mode, setMode] = useState<BlobMode>(
|
||||
deriveInitialMode(watchedBucketType),
|
||||
);
|
||||
const [authMode, setAuthMode] = useState<AuthMode>(() =>
|
||||
deriveInitialAuthMode(credentials),
|
||||
);
|
||||
|
||||
// Keep bucket_type in sync with UI mode
|
||||
useEffect(() => {
|
||||
const nextMode = deriveInitialMode(watchedBucketType);
|
||||
setMode((prev) => (prev === nextMode ? prev : nextMode));
|
||||
}, [watchedBucketType]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('config.bucket_type', mode, { shouldDirty: true });
|
||||
// Default addressing style for compatible mode
|
||||
if (
|
||||
mode === 's3_compatible' &&
|
||||
!form.getValues('config.credentials.addressing_style')
|
||||
) {
|
||||
form.setValue('config.credentials.addressing_style', 'virtual', {
|
||||
shouldDirty: false,
|
||||
});
|
||||
}
|
||||
if (mode === 's3_compatible' && authMode !== 'access_key') {
|
||||
setAuthMode('access_key');
|
||||
}
|
||||
// Persist authentication_method for backend
|
||||
const nextAuthMethod: AuthMode =
|
||||
mode === 's3_compatible' ? 'access_key' : authMode;
|
||||
form.setValue('config.credentials.authentication_method', nextAuthMethod, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
// Clear errors for fields that are not relevant in the current mode/auth selection
|
||||
const inactiveFields: string[] = [];
|
||||
if (mode === 's3_compatible') {
|
||||
inactiveFields.push('config.credentials.aws_role_arn');
|
||||
} else {
|
||||
if (authMode === 'iam_role') {
|
||||
inactiveFields.push('config.credentials.aws_access_key_id');
|
||||
inactiveFields.push('config.credentials.aws_secret_access_key');
|
||||
}
|
||||
if (authMode === 'assume_role') {
|
||||
inactiveFields.push('config.credentials.aws_access_key_id');
|
||||
inactiveFields.push('config.credentials.aws_secret_access_key');
|
||||
inactiveFields.push('config.credentials.aws_role_arn');
|
||||
}
|
||||
}
|
||||
if (inactiveFields.length) {
|
||||
form.clearErrors(inactiveFields as any);
|
||||
}
|
||||
}, [form, mode, authMode]);
|
||||
|
||||
const isS3 = mode === 's3';
|
||||
const requiresAccessKey =
|
||||
authMode === 'access_key' || mode === 's3_compatible';
|
||||
const requiresRoleArn = isS3 && authMode === 'iam_role';
|
||||
|
||||
// Help text for assume role (no inputs)
|
||||
const assumeRoleNote = useMemo(
|
||||
() => t('No credentials required. Uses the default environment role.'),
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm text-text-secondary">Mode</div>
|
||||
<Segmented
|
||||
options={modeOptions}
|
||||
value={mode}
|
||||
onChange={(val) => setMode(val as BlobMode)}
|
||||
className="w-full"
|
||||
itemClassName="flex-1 justify-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isS3 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm text-text-secondary">Authentication</div>
|
||||
<Segmented
|
||||
options={authOptions}
|
||||
value={authMode}
|
||||
onChange={(val) => setAuthMode(val as AuthMode)}
|
||||
className="w-full"
|
||||
itemClassName="flex-1 justify-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requiresAccessKey && (
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.aws_access_key_id"
|
||||
label="AWS Access Key ID"
|
||||
required={requiresAccessKey}
|
||||
rules={{
|
||||
validate: (val) =>
|
||||
requiresAccessKey
|
||||
? Boolean(val) || 'Access Key ID is required'
|
||||
: true,
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<Input {...field} placeholder="AKIA..." autoComplete="off" />
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{requiresAccessKey && (
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.aws_secret_access_key"
|
||||
label="AWS Secret Access Key"
|
||||
required={requiresAccessKey}
|
||||
rules={{
|
||||
validate: (val) =>
|
||||
requiresAccessKey
|
||||
? Boolean(val) || 'Secret Access Key is required'
|
||||
: true,
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="****************"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{requiresRoleArn && (
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.aws_role_arn"
|
||||
label="Role ARN"
|
||||
required={requiresRoleArn}
|
||||
tooltip="The role will be assumed by the runtime environment."
|
||||
rules={{
|
||||
validate: (val) =>
|
||||
requiresRoleArn ? Boolean(val) || 'Role ARN is required' : true,
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/YourRole"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
{isS3 && authMode === 'assume_role' && (
|
||||
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
|
||||
{assumeRoleNote}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 's3_compatible' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.addressing_style"
|
||||
label="Addressing Style"
|
||||
tooltip={t('setting.S3CompatibleAddressingStyleTip')}
|
||||
required={false}
|
||||
>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
triggerClassName="!shrink"
|
||||
options={addressingOptions}
|
||||
value={field.value || 'virtual'}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
<RAGFlowFormItem
|
||||
name="config.credentials.endpoint_url"
|
||||
label="Endpoint URL"
|
||||
required={false}
|
||||
tooltip={t('setting.S3CompatibleEndpointUrlTip')}
|
||||
>
|
||||
{(field) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://fsn1.your-objectstorage.com"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlobTokenField;
|
||||
@ -3,6 +3,8 @@ import SvgIcon from '@/components/svg-icon';
|
||||
import { t, TFunction } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BedrockRegionList } from '../setting-model/constant';
|
||||
import BlobTokenField from './component/blob-token-field';
|
||||
import BoxTokenField from './component/box-token-field';
|
||||
import { ConfluenceIndexingModeField } from './component/confluence-token-field';
|
||||
import GmailTokenField from './component/gmail-token-field';
|
||||
@ -105,6 +107,11 @@ export const generateDataSourceInfo = (t: TFunction) => {
|
||||
};
|
||||
};
|
||||
|
||||
const awsRegionOptions = BedrockRegionList.map((r) => ({
|
||||
label: r,
|
||||
value: r,
|
||||
}));
|
||||
|
||||
export const useDataSourceInfo = () => {
|
||||
const { t } = useTranslation();
|
||||
const [dataSourceInfo, setDataSourceInfo] = useState<IDataSourceInfoMap>(
|
||||
@ -222,18 +229,6 @@ export const DataSourceFormFields = {
|
||||
},
|
||||
],
|
||||
[DataSourceKey.S3]: [
|
||||
{
|
||||
label: 'AWS Access Key ID',
|
||||
name: 'config.credentials.aws_access_key_id',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'AWS Secret Access Key',
|
||||
name: 'config.credentials.aws_secret_access_key',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Bucket Name',
|
||||
name: 'config.bucket_name',
|
||||
@ -241,39 +236,21 @@ export const DataSourceFormFields = {
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Bucket Type',
|
||||
name: 'config.bucket_type',
|
||||
label: 'Region',
|
||||
name: 'config.credentials.region',
|
||||
type: FormFieldType.Select,
|
||||
options: [
|
||||
{ label: 'S3', value: 's3' },
|
||||
{ label: 'S3 Compatible', value: 's3_compatible' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Addressing Style',
|
||||
name: 'config.credentials.addressing_style',
|
||||
type: FormFieldType.Select,
|
||||
options: [
|
||||
{ label: 'Virtual Hosted Style', value: 'virtual' },
|
||||
{ label: 'Path Style', value: 'path' },
|
||||
],
|
||||
required: false,
|
||||
placeholder: 'Virtual Hosted Style',
|
||||
tooltip: t('setting.S3CompatibleAddressingStyleTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
return formValues?.config?.bucket_type === 's3_compatible';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Endpoint URL',
|
||||
name: 'config.credentials.endpoint_url',
|
||||
type: FormFieldType.Text,
|
||||
required: false,
|
||||
placeholder: 'https://fsn1.your-objectstorage.com',
|
||||
tooltip: t('setting.S3CompatibleEndpointUrlTip'),
|
||||
shouldRender: (formValues: any) => {
|
||||
return formValues?.config?.bucket_type === 's3_compatible';
|
||||
options: awsRegionOptions,
|
||||
customValidate: (val: string, formValues: any) => {
|
||||
const credentials = formValues?.config?.credentials || {};
|
||||
const bucketType = formValues?.config?.bucket_type || 's3';
|
||||
const hasAccessKey = Boolean(
|
||||
credentials.aws_access_key_id || credentials.aws_secret_access_key,
|
||||
);
|
||||
if (bucketType === 's3' && hasAccessKey) {
|
||||
return Boolean(val) || 'Region is required when using access key';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -283,6 +260,14 @@ export const DataSourceFormFields = {
|
||||
required: false,
|
||||
tooltip: t('setting.s3PrefixTip'),
|
||||
},
|
||||
{
|
||||
label: 'Credentials',
|
||||
name: 'config.credentials.__blob_token',
|
||||
type: FormFieldType.Custom,
|
||||
hideLabel: true,
|
||||
required: false,
|
||||
render: () => <BlobTokenField />,
|
||||
},
|
||||
],
|
||||
[DataSourceKey.NOTION]: [
|
||||
{
|
||||
@ -700,6 +685,9 @@ export const DataSourceFormDefaultValues = {
|
||||
credentials: {
|
||||
aws_access_key_id: '',
|
||||
aws_secret_access_key: '',
|
||||
region: '',
|
||||
authentication_method: 'access_key',
|
||||
aws_role_arn: '',
|
||||
endpoint_url: '',
|
||||
addressing_style: 'virtual',
|
||||
},
|
||||
|
||||
@ -10,8 +10,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { RunningStatus } from '@/constants/knowledge';
|
||||
import { t } from 'i18next';
|
||||
import { debounce } from 'lodash';
|
||||
import { CirclePause, Repeat } from 'lucide-react';
|
||||
import { CirclePause, Loader2, Repeat } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FieldValues } from 'react-hook-form';
|
||||
import {
|
||||
@ -120,11 +119,11 @@ const SourceDetailPage = () => {
|
||||
];
|
||||
}, [detail, runSchedule]);
|
||||
|
||||
const { handleAddOk } = useAddDataSource();
|
||||
const { addLoading, handleAddOk } = useAddDataSource();
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
formRef?.current?.submit();
|
||||
}, [formRef]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (detail) {
|
||||
@ -140,9 +139,7 @@ const SourceDetailPage = () => {
|
||||
return {
|
||||
...field,
|
||||
horizontal: true,
|
||||
onChange: () => {
|
||||
onSubmit();
|
||||
},
|
||||
onChange: undefined,
|
||||
};
|
||||
});
|
||||
setFields(newFields);
|
||||
@ -175,12 +172,23 @@ const SourceDetailPage = () => {
|
||||
<DynamicForm.Root
|
||||
ref={formRef}
|
||||
fields={fields}
|
||||
onSubmit={debounce((data) => {
|
||||
handleAddOk(data);
|
||||
}, 500)}
|
||||
onSubmit={(data) => handleAddOk(data)}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[1200px] flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={addLoading}
|
||||
className="flex items-center justify-center min-w-[100px] px-4 py-2 bg-primary text-white rounded-md disabled:opacity-60"
|
||||
>
|
||||
{addLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{addLoading
|
||||
? t('modal.loadingText', { defaultValue: 'Submitting...' })
|
||||
: t('modal.okText', { defaultValue: 'Submit' })}
|
||||
</button>
|
||||
</div>
|
||||
<section className="flex flex-col gap-2">
|
||||
<div className="text-2xl text-text-primary mb-2">
|
||||
{t('setting.log')}
|
||||
|
||||
Reference in New Issue
Block a user