mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-07 02:55:08 +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..."
|
echo "Waiting for service to be available..."
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
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
|
- name: Run frontend api tests against Infinity
|
||||||
run: |
|
run: |
|
||||||
@ -256,7 +256,7 @@ jobs:
|
|||||||
echo "Waiting for service to be available..."
|
echo "Waiting for service to be available..."
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
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
|
- name: Run http api tests against Infinity
|
||||||
run: |
|
run: |
|
||||||
@ -265,7 +265,7 @@ jobs:
|
|||||||
echo "Waiting for service to be available..."
|
echo "Waiting for service to be available..."
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
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
|
- name: Stop ragflow:nightly
|
||||||
if: always() # always run this step even if previous steps failed
|
if: always() # always run this step even if previous steps failed
|
||||||
|
|||||||
@ -108,7 +108,7 @@ def _load_user():
|
|||||||
authorization = request.headers.get("Authorization")
|
authorization = request.headers.get("Authorization")
|
||||||
g.user = None
|
g.user = None
|
||||||
if not authorization:
|
if not authorization:
|
||||||
return
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
access_token = str(jwt.loads(authorization))
|
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 common.constants import StatusEnum, LLMType
|
||||||
from api.db.db_models import TenantLLM
|
from api.db.db_models import TenantLLM
|
||||||
from rag.utils.base64_image import test_image
|
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
|
@manager.route("/factories", methods=["GET"]) # noqa: F821
|
||||||
@ -208,70 +208,83 @@ async def add_llm():
|
|||||||
msg = ""
|
msg = ""
|
||||||
mdl_nm = llm["llm_name"].split("___")[0]
|
mdl_nm = llm["llm_name"].split("___")[0]
|
||||||
extra = {"provider": factory}
|
extra = {"provider": factory}
|
||||||
if llm["model_type"] == LLMType.EMBEDDING.value:
|
model_type = llm["model_type"]
|
||||||
assert factory in EmbeddingModel, f"Embedding model from {factory} is not supported yet."
|
model_api_key = llm["api_key"]
|
||||||
mdl = EmbeddingModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
model_base_url = llm.get("api_base", "")
|
||||||
try:
|
match model_type:
|
||||||
arr, tc = mdl.encode(["Test if the api key is available"])
|
case LLMType.EMBEDDING.value:
|
||||||
if len(arr[0]) == 0:
|
assert factory in EmbeddingModel, f"Embedding model from {factory} is not supported yet."
|
||||||
raise Exception("Fail")
|
mdl = EmbeddingModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||||
except Exception as e:
|
try:
|
||||||
msg += f"\nFail to access embedding model({mdl_nm})." + str(e)
|
arr, tc = mdl.encode(["Test if the api key is available"])
|
||||||
elif llm["model_type"] == LLMType.CHAT.value:
|
if len(arr[0]) == 0:
|
||||||
assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
|
raise Exception("Fail")
|
||||||
mdl = ChatModel[factory](
|
except Exception as e:
|
||||||
key=llm["api_key"],
|
msg += f"\nFail to access embedding model({mdl_nm})." + str(e)
|
||||||
model_name=mdl_nm,
|
case LLMType.CHAT.value:
|
||||||
base_url=llm["api_base"],
|
assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
|
||||||
**extra,
|
mdl = ChatModel[factory](
|
||||||
)
|
key=model_api_key,
|
||||||
try:
|
model_name=mdl_nm,
|
||||||
m, tc = await mdl.async_chat(None, [{"role": "user", "content": "Hello! How are you doing!"}], {"temperature": 0.9})
|
base_url=model_base_url,
|
||||||
if not tc and m.find("**ERROR**:") >= 0:
|
**extra,
|
||||||
raise Exception(m)
|
)
|
||||||
except Exception as e:
|
try:
|
||||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
m, tc = await mdl.async_chat(None, [{"role": "user", "content": "Hello! How are you doing!"}],
|
||||||
elif llm["model_type"] == LLMType.RERANK:
|
{"temperature": 0.9})
|
||||||
assert factory in RerankModel, f"RE-rank model from {factory} is not supported yet."
|
if not tc and m.find("**ERROR**:") >= 0:
|
||||||
try:
|
raise Exception(m)
|
||||||
mdl = RerankModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
except Exception as e:
|
||||||
arr, tc = mdl.similarity("Hello~ RAGFlower!", ["Hi, there!", "Ohh, my friend!"])
|
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||||
if len(arr) == 0:
|
|
||||||
raise Exception("Not known.")
|
case LLMType.RERANK.value:
|
||||||
except KeyError:
|
assert factory in RerankModel, f"RE-rank model from {factory} is not supported yet."
|
||||||
msg += f"{factory} dose not support this model({factory}/{mdl_nm})"
|
try:
|
||||||
except Exception as e:
|
mdl = RerankModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
arr, tc = mdl.similarity("Hello~ RAGFlower!", ["Hi, there!", "Ohh, my friend!"])
|
||||||
elif llm["model_type"] == LLMType.IMAGE2TEXT.value:
|
if len(arr) == 0:
|
||||||
assert factory in CvModel, f"Image to text model from {factory} is not supported yet."
|
raise Exception("Not known.")
|
||||||
mdl = CvModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
except KeyError:
|
||||||
try:
|
msg += f"{factory} dose not support this model({factory}/{mdl_nm})"
|
||||||
image_data = test_image
|
except Exception as e:
|
||||||
m, tc = mdl.describe(image_data)
|
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||||
if not tc and m.find("**ERROR**:") >= 0:
|
|
||||||
raise Exception(m)
|
case LLMType.IMAGE2TEXT.value:
|
||||||
except Exception as e:
|
assert factory in CvModel, f"Image to text model from {factory} is not supported yet."
|
||||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
mdl = CvModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||||
elif llm["model_type"] == LLMType.TTS:
|
try:
|
||||||
assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
|
image_data = test_image
|
||||||
mdl = TTSModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm["api_base"])
|
m, tc = mdl.describe(image_data)
|
||||||
try:
|
if not tc and m.find("**ERROR**:") >= 0:
|
||||||
for resp in mdl.tts("Hello~ RAGFlower!"):
|
raise Exception(m)
|
||||||
pass
|
except Exception as e:
|
||||||
except RuntimeError as e:
|
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
case LLMType.TTS.value:
|
||||||
elif llm["model_type"] == LLMType.OCR.value:
|
assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
|
||||||
assert factory in OcrModel, f"OCR model from {factory} is not supported yet."
|
mdl = TTSModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||||
try:
|
try:
|
||||||
mdl = OcrModel[factory](key=llm["api_key"], model_name=mdl_nm, base_url=llm.get("api_base", ""))
|
for resp in mdl.tts("Hello~ RAGFlower!"):
|
||||||
ok, reason = mdl.check_available()
|
pass
|
||||||
if not ok:
|
except RuntimeError as e:
|
||||||
raise RuntimeError(reason or "Model not available")
|
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
||||||
except Exception as e:
|
case LLMType.OCR.value:
|
||||||
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
|
assert factory in OcrModel, f"OCR model from {factory} is not supported yet."
|
||||||
else:
|
try:
|
||||||
# TODO: check other type of models
|
mdl = OcrModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||||
pass
|
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:
|
if msg:
|
||||||
return get_data_error_result(message=msg)
|
return get_data_error_result(message=msg)
|
||||||
|
|||||||
@ -326,7 +326,6 @@ async def list_tools() -> Response:
|
|||||||
try:
|
try:
|
||||||
tools = await asyncio.to_thread(tool_call_session.get_tools, timeout)
|
tools = await asyncio.to_thread(tool_call_session.get_tools, timeout)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tools = []
|
|
||||||
return get_data_error_result(message=f"MCP list tools error: {e}")
|
return get_data_error_result(message=f"MCP list tools error: {e}")
|
||||||
|
|
||||||
results[server_key] = []
|
results[server_key] = []
|
||||||
@ -428,7 +427,6 @@ async def test_mcp() -> Response:
|
|||||||
try:
|
try:
|
||||||
tools = await asyncio.to_thread(tool_call_session.get_tools, timeout)
|
tools = await asyncio.to_thread(tool_call_session.get_tools, timeout)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tools = []
|
|
||||||
return get_data_error_result(message=f"Test MCP error: {e}")
|
return get_data_error_result(message=f"Test MCP error: {e}")
|
||||||
finally:
|
finally:
|
||||||
# PERF: blocking call to close sessions — consider moving to background thread or task queue
|
# 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)
|
chats = DialogService.get_list(tenant_id, page_number, items_per_page, orderby, desc, id, name)
|
||||||
if not chats:
|
if not chats:
|
||||||
return get_result(data=[])
|
return get_result(data=[])
|
||||||
list_assts = []
|
list_assistants = []
|
||||||
key_mapping = {
|
key_mapping = {
|
||||||
"parameters": "variables",
|
"parameters": "variables",
|
||||||
"prologue": "opener",
|
"prologue": "opener",
|
||||||
@ -321,5 +321,5 @@ def list_chat(tenant_id):
|
|||||||
del res["kb_ids"]
|
del res["kb_ids"]
|
||||||
res["datasets"] = kb_list
|
res["datasets"] = kb_list
|
||||||
res["avatar"] = res.pop("icon")
|
res["avatar"] = res.pop("icon")
|
||||||
list_assts.append(res)
|
list_assistants.append(res)
|
||||||
return get_result(data=list_assts)
|
return get_result(data=list_assistants)
|
||||||
|
|||||||
@ -205,7 +205,8 @@ async def create(tenant_id):
|
|||||||
if not FileService.is_parent_folder_exist(pf_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)
|
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):
|
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:
|
if input_file_type == FileType.FOLDER.value:
|
||||||
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(
|
if file.type != FileType.FOLDER.value and pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
|
||||||
file.name.lower()).suffix:
|
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):
|
for existing_file in FileService.query(name=req["name"], pf_id=file.parent_id):
|
||||||
if existing_file.name == req["name"]:
|
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"]}):
|
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)
|
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:
|
except Exception as e:
|
||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/file/download/<attachment_id>", methods=["GET"]) # noqa: F821
|
@manager.route("/file/download/<attachment_id>", methods=["GET"]) # noqa: F821
|
||||||
@token_required
|
@token_required
|
||||||
async def download_attachment(tenant_id,attachment_id):
|
async def download_attachment(tenant_id, attachment_id):
|
||||||
try:
|
try:
|
||||||
ext = request.args.get("ext", "markdown")
|
ext = request.args.get("ext", "markdown")
|
||||||
data = await asyncio.to_thread(settings.STORAGE_IMPL.get, tenant_id, attachment_id)
|
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:
|
except Exception as e:
|
||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route('/file/mv', methods=['POST']) # noqa: F821
|
@manager.route('/file/mv', methods=['POST']) # noqa: F821
|
||||||
@token_required
|
@token_required
|
||||||
async def move(tenant_id):
|
async def move(tenant_id):
|
||||||
|
|||||||
@ -448,7 +448,7 @@ async def chat_completion_openai_like(tenant_id, chat_id):
|
|||||||
@token_required
|
@token_required
|
||||||
async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
||||||
req = await get_request_json()
|
req = await get_request_json()
|
||||||
tiktokenenc = tiktoken.get_encoding("cl100k_base")
|
tiktoken_encode = tiktoken.get_encoding("cl100k_base")
|
||||||
messages = req.get("messages", [])
|
messages = req.get("messages", [])
|
||||||
if not messages:
|
if not messages:
|
||||||
return get_error_data_result("You must provide at least one message.")
|
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}")
|
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"]]
|
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:
|
if not filtered_messages:
|
||||||
return jsonify(
|
return jsonify(
|
||||||
get_data_openai(
|
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).",
|
content="No valid messages found (user or assistant).",
|
||||||
finish_reason="stop",
|
finish_reason="stop",
|
||||||
model=req.get("model", ""),
|
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,
|
prompt_tokens=prompt_tokens,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -501,6 +501,8 @@ async def agents_completion_openai_compatibility(tenant_id, agent_id):
|
|||||||
):
|
):
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/agents/<agent_id>/completions", methods=["POST"]) # noqa: F821
|
@manager.route("/agents/<agent_id>/completions", methods=["POST"]) # noqa: F821
|
||||||
@token_required
|
@token_required
|
||||||
@ -920,6 +922,7 @@ async def chatbot_completions(dialog_id):
|
|||||||
async for answer in iframe_completion(dialog_id, **req):
|
async for answer in iframe_completion(dialog_id, **req):
|
||||||
return get_result(data=answer)
|
return get_result(data=answer)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@manager.route("/chatbots/<dialog_id>/info", methods=["GET"]) # noqa: F821
|
@manager.route("/chatbots/<dialog_id>/info", methods=["GET"]) # noqa: F821
|
||||||
async def chatbots_inputs(dialog_id):
|
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):
|
async for answer in agent_completion(objs[0].tenant_id, agent_id, **req):
|
||||||
return get_result(data=answer)
|
return get_result(data=answer)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@manager.route("/agentbots/<agent_id>/inputs", methods=["GET"]) # noqa: F821
|
@manager.route("/agentbots/<agent_id>/inputs", methods=["GET"]) # noqa: F821
|
||||||
async def begin_inputs(agent_id):
|
async def begin_inputs(agent_id):
|
||||||
|
|||||||
@ -660,7 +660,7 @@ def user_register(user_id, user):
|
|||||||
tenant_llm = get_init_tenant_llm(user_id)
|
tenant_llm = get_init_tenant_llm(user_id)
|
||||||
|
|
||||||
if not UserService.save(**user):
|
if not UserService.save(**user):
|
||||||
return
|
return None
|
||||||
TenantService.insert(**tenant)
|
TenantService.insert(**tenant)
|
||||||
UserTenantService.insert(**usr_tenant)
|
UserTenantService.insert(**usr_tenant)
|
||||||
TenantLLMService.insert_many(tenant_llm)
|
TenantLLMService.insert_many(tenant_llm)
|
||||||
|
|||||||
@ -54,6 +54,7 @@ class RetCode(IntEnum, CustomEnum):
|
|||||||
SERVER_ERROR = 500
|
SERVER_ERROR = 500
|
||||||
FORBIDDEN = 403
|
FORBIDDEN = 403
|
||||||
NOT_FOUND = 404
|
NOT_FOUND = 404
|
||||||
|
CONFLICT = 409
|
||||||
|
|
||||||
|
|
||||||
class StatusEnum(Enum):
|
class StatusEnum(Enum):
|
||||||
|
|||||||
@ -64,15 +64,23 @@ class BlobStorageConnector(LoadConnector, PollConnector):
|
|||||||
|
|
||||||
elif self.bucket_type == BlobType.S3:
|
elif self.bucket_type == BlobType.S3:
|
||||||
authentication_method = credentials.get("authentication_method", "access_key")
|
authentication_method = credentials.get("authentication_method", "access_key")
|
||||||
|
|
||||||
if authentication_method == "access_key":
|
if authentication_method == "access_key":
|
||||||
if not all(
|
if not all(
|
||||||
credentials.get(key)
|
credentials.get(key)
|
||||||
for key in ["aws_access_key_id", "aws_secret_access_key"]
|
for key in ["aws_access_key_id", "aws_secret_access_key"]
|
||||||
):
|
):
|
||||||
raise ConnectorMissingCredentialError("Amazon S3")
|
raise ConnectorMissingCredentialError("Amazon S3")
|
||||||
|
|
||||||
elif authentication_method == "iam_role":
|
elif authentication_method == "iam_role":
|
||||||
if not credentials.get("aws_role_arn"):
|
if not credentials.get("aws_role_arn"):
|
||||||
raise ConnectorMissingCredentialError("Amazon S3 IAM role ARN is required")
|
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:
|
elif self.bucket_type == BlobType.GOOGLE_CLOUD_STORAGE:
|
||||||
if not all(
|
if not all(
|
||||||
@ -293,4 +301,4 @@ if __name__ == "__main__":
|
|||||||
except ConnectorMissingCredentialError as e:
|
except ConnectorMissingCredentialError as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
except Exception as 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:
|
elif bucket_type == BlobType.S3:
|
||||||
authentication_method = credentials.get("authentication_method", "access_key")
|
authentication_method = credentials.get("authentication_method", "access_key")
|
||||||
|
|
||||||
|
region_name = credentials.get("region") or None
|
||||||
|
|
||||||
if authentication_method == "access_key":
|
if authentication_method == "access_key":
|
||||||
session = boto3.Session(
|
session = boto3.Session(
|
||||||
aws_access_key_id=credentials["aws_access_key_id"],
|
aws_access_key_id=credentials["aws_access_key_id"],
|
||||||
aws_secret_access_key=credentials["aws_secret_access_key"],
|
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":
|
elif authentication_method == "iam_role":
|
||||||
role_arn = credentials["aws_role_arn"]
|
role_arn = credentials["aws_role_arn"]
|
||||||
|
|
||||||
def _refresh_credentials() -> dict[str, str]:
|
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(
|
assumed_role_object = sts_client.assume_role(
|
||||||
RoleArn=role_arn,
|
RoleArn=role_arn,
|
||||||
RoleSessionName=f"onyx_blob_storage_{int(datetime.now().timestamp())}",
|
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 = get_session()
|
||||||
botocore_session._credentials = refreshable
|
botocore_session._credentials = refreshable
|
||||||
session = boto3.Session(botocore_session=botocore_session)
|
session = boto3.Session(botocore_session=botocore_session, region_name=region_name)
|
||||||
return session.client("s3")
|
return session.client("s3", region_name=region_name)
|
||||||
|
|
||||||
elif authentication_method == "assume_role":
|
elif authentication_method == "assume_role":
|
||||||
return boto3.client("s3")
|
return boto3.client("s3", region_name=region_name)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid authentication method for S3.")
|
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):
|
def log_exception(e, *args):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
for a in args:
|
for a in args:
|
||||||
if hasattr(a, "text"):
|
try:
|
||||||
logging.error(a.text)
|
text = getattr(a, "text")
|
||||||
raise Exception(a.text)
|
except Exception:
|
||||||
else:
|
text = None
|
||||||
logging.error(str(a))
|
if text is not None:
|
||||||
|
logging.error(text)
|
||||||
|
raise Exception(text)
|
||||||
|
logging.error(str(a))
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@ -44,23 +44,23 @@ def total_token_count_from_response(resp):
|
|||||||
if resp is None:
|
if resp is None:
|
||||||
return 0
|
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
|
return resp.usage.total_tokens
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return resp.usage_metadata.total_tokens
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if hasattr(resp, "meta") and hasattr(resp.meta, "billed_units") and hasattr(resp.meta.billed_units, "input_tokens"):
|
try:
|
||||||
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
|
return resp.meta.billed_units.input_tokens
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if isinstance(resp, dict) and 'usage' in resp and 'total_tokens' in resp['usage']:
|
if isinstance(resp, dict) and 'usage' in resp and 'total_tokens' in resp['usage']:
|
||||||
try:
|
try:
|
||||||
@ -85,4 +85,3 @@ def total_token_count_from_response(resp):
|
|||||||
def truncate(string: str, max_len: int) -> str:
|
def truncate(string: str, max_len: int) -> str:
|
||||||
"""Returns truncated text if the length of text exceed max_len."""
|
"""Returns truncated text if the length of text exceed max_len."""
|
||||||
return encoder.decode(encoder.encode(string)[: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 {
|
import {
|
||||||
ChunkMethodItem,
|
ChunkMethodItem,
|
||||||
EnableTocToggle,
|
EnableTocToggle,
|
||||||
|
ImageContextWindow,
|
||||||
ParseTypeItem,
|
ParseTypeItem,
|
||||||
} from '@/pages/dataset/dataset-setting/configuration/common-item';
|
} from '@/pages/dataset/dataset-setting/configuration/common-item';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@ -119,6 +120,7 @@ export function ChunkMethodDialog({
|
|||||||
auto_questions: z.coerce.number().optional(),
|
auto_questions: z.coerce.number().optional(),
|
||||||
html4excel: z.boolean().optional(),
|
html4excel: z.boolean().optional(),
|
||||||
toc_extraction: 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_parse_method: z.enum(['auto', 'txt', 'ocr']).optional(),
|
||||||
mineru_formula_enable: z.boolean().optional(),
|
mineru_formula_enable: z.boolean().optional(),
|
||||||
mineru_table_enable: z.boolean().optional(),
|
mineru_table_enable: z.boolean().optional(),
|
||||||
@ -364,7 +366,10 @@ export function ChunkMethodDialog({
|
|||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
>
|
>
|
||||||
{selectedTag === DocumentParserType.Naive && (
|
{selectedTag === DocumentParserType.Naive && (
|
||||||
<EnableTocToggle />
|
<>
|
||||||
|
<EnableTocToggle />
|
||||||
|
<ImageContextWindow />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{showAutoKeywords(selectedTag) && (
|
{showAutoKeywords(selectedTag) && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export function useDefaultParserValues() {
|
|||||||
auto_questions: 0,
|
auto_questions: 0,
|
||||||
html4excel: false,
|
html4excel: false,
|
||||||
toc_extraction: false,
|
toc_extraction: false,
|
||||||
|
image_context_window: 0,
|
||||||
mineru_parse_method: 'auto',
|
mineru_parse_method: 'auto',
|
||||||
mineru_formula_enable: true,
|
mineru_formula_enable: true,
|
||||||
mineru_table_enable: true,
|
mineru_table_enable: true,
|
||||||
|
|||||||
@ -145,6 +145,8 @@ interface FileUploaderProps
|
|||||||
*/
|
*/
|
||||||
maxFileCount?: DropzoneProps['maxFiles'];
|
maxFileCount?: DropzoneProps['maxFiles'];
|
||||||
|
|
||||||
|
hideDropzoneOnMaxFileCount?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the uploader should accept multiple files.
|
* Whether the uploader should accept multiple files.
|
||||||
* @type boolean
|
* @type boolean
|
||||||
@ -178,6 +180,7 @@ export function FileUploader(props: FileUploaderProps) {
|
|||||||
maxFileCount = 100000000000,
|
maxFileCount = 100000000000,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
hideDropzoneOnMaxFileCount = false,
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@ -189,6 +192,8 @@ export function FileUploader(props: FileUploaderProps) {
|
|||||||
onChange: onValueChange,
|
onChange: onValueChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reachesMaxFileCount = (files?.length ?? 0) >= maxFileCount;
|
||||||
|
|
||||||
const onDrop = React.useCallback(
|
const onDrop = React.useCallback(
|
||||||
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||||
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
|
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
|
||||||
@ -263,65 +268,68 @@ export function FileUploader(props: FileUploaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col gap-6 overflow-hidden">
|
<div className="relative flex flex-col gap-6 overflow-hidden">
|
||||||
<Dropzone
|
{!(hideDropzoneOnMaxFileCount && reachesMaxFileCount) && (
|
||||||
onDrop={onDrop}
|
<Dropzone
|
||||||
accept={accept}
|
onDrop={onDrop}
|
||||||
maxSize={maxSize}
|
accept={accept}
|
||||||
maxFiles={maxFileCount}
|
maxSize={maxSize}
|
||||||
multiple={maxFileCount > 1 || multiple}
|
maxFiles={maxFileCount}
|
||||||
disabled={isDisabled}
|
multiple={maxFileCount > 1 || multiple}
|
||||||
>
|
disabled={isDisabled}
|
||||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
>
|
||||||
<div
|
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||||
{...getRootProps()}
|
<div
|
||||||
className={cn(
|
{...getRootProps()}
|
||||||
'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',
|
className={cn(
|
||||||
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
'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',
|
||||||
isDragActive && 'border-border-button',
|
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
isDisabled && 'pointer-events-none opacity-60',
|
isDragActive && 'border-border-button',
|
||||||
className,
|
isDisabled && 'pointer-events-none opacity-60',
|
||||||
)}
|
className,
|
||||||
{...dropzoneProps}
|
)}
|
||||||
>
|
{...dropzoneProps}
|
||||||
<input {...getInputProps()} />
|
>
|
||||||
{isDragActive ? (
|
<input {...getInputProps()} />
|
||||||
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
{isDragActive ? (
|
||||||
<div className="rounded-full border border-dashed p-3">
|
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
||||||
<Upload
|
<div className="rounded-full border border-dashed p-3">
|
||||||
className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
|
<Upload
|
||||||
aria-hidden="true"
|
className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
|
||||||
/>
|
aria-hidden="true"
|
||||||
</div>
|
/>
|
||||||
<p className="font-medium text-text-secondary">
|
</div>
|
||||||
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">
|
|
||||||
<p className="font-medium text-text-secondary">
|
<p className="font-medium text-text-secondary">
|
||||||
{title || t('knowledgeDetails.uploadTitle')}
|
Drop the files here
|
||||||
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
||||||
</div>
|
<div className="rounded-full border border-dashed p-3">
|
||||||
)}
|
<Upload
|
||||||
</Dropzone>
|
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 ? (
|
{files?.length ? (
|
||||||
<div className="h-fit w-full px-3">
|
<div className="h-fit w-full px-3">
|
||||||
<div className="flex max-h-48 flex-col gap-4 overflow-auto scrollbar-auto">
|
<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 { api_host } from '@/utils/api';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||||
|
|
||||||
interface IImage {
|
interface IImage extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
id: string;
|
id: string;
|
||||||
className?: string;
|
|
||||||
onClick?(): void;
|
|
||||||
t?: string | number;
|
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,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover';
|
} from '@/components/ui/popover';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
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 { useForm } from 'react-hook-form';
|
||||||
import { ZodArray, ZodString, z } from 'zod';
|
import { ZodArray, ZodString, z } from 'zod';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
import { FilterField } from './filter-field';
|
||||||
import { FilterChange, FilterCollection, FilterValue } from './interface';
|
import { FilterChange, FilterCollection, FilterValue } from './interface';
|
||||||
|
|
||||||
export type CheckboxFormMultipleProps = {
|
export type CheckboxFormMultipleProps = {
|
||||||
@ -35,29 +41,71 @@ function CheckboxFormMultiple({
|
|||||||
onChange,
|
onChange,
|
||||||
setOpen,
|
setOpen,
|
||||||
}: CheckboxFormMultipleProps) {
|
}: CheckboxFormMultipleProps) {
|
||||||
const fieldsDict = filters?.reduce<Record<string, Array<any>>>((pre, cur) => {
|
const [resolvedFilters, setResolvedFilters] =
|
||||||
pre[cur.field] = [];
|
useState<FilterCollection[]>(filters);
|
||||||
return pre;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const FormSchema = z.object(
|
useEffect(() => {
|
||||||
filters.reduce<Record<string, ZodArray<ZodString, 'many'>>>((pre, cur) => {
|
if (filters && filters.length > 0) {
|
||||||
pre[cur.field] = z.array(z.string());
|
setResolvedFilters(filters);
|
||||||
|
}
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
// .refine((value) => value.some((item) => item), {
|
const fieldsDict = useMemo(() => {
|
||||||
// message: 'You have to select at least one item.',
|
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;
|
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>>({
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
resolver: zodResolver(FormSchema),
|
resolver: resolvedFilters.length > 0 ? zodResolver(FormSchema) : undefined,
|
||||||
defaultValues: fieldsDict,
|
defaultValues: fieldsDict,
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
function onSubmit() {
|
||||||
onChange?.(data);
|
const formValues = form.getValues();
|
||||||
|
onChange?.({ ...formValues });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,8 +115,10 @@ function CheckboxFormMultiple({
|
|||||||
}, [fieldsDict, onChange, setOpen]);
|
}, [fieldsDict, onChange, setOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset(value);
|
if (resolvedFilters.length > 0) {
|
||||||
}, [form, value]);
|
form.reset(value || fieldsDict);
|
||||||
|
}
|
||||||
|
}, [form, value, resolvedFilters, fieldsDict]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@ -85,44 +135,21 @@ function CheckboxFormMultiple({
|
|||||||
render={() => (
|
render={() => (
|
||||||
<FormItem className="space-y-4">
|
<FormItem className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<FormLabel className="text-base text-text-sub-title-invert">
|
<FormLabel className="text-text-primary">{x.label}</FormLabel>
|
||||||
{x.label}
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
</div>
|
||||||
{x.list.map((item) => (
|
{x.list.map((item) => {
|
||||||
<FormField
|
return (
|
||||||
key={item.id}
|
<FilterField
|
||||||
control={form.control}
|
key={item.id}
|
||||||
name={x.field}
|
item={{ ...item }}
|
||||||
render={({ field }) => {
|
parent={{
|
||||||
return (
|
...x,
|
||||||
<div className="flex items-center justify-between text-text-primary text-xs">
|
id: x.field,
|
||||||
<FormItem
|
// field: `${x.field}${item.field ? '.' + item.field : ''}`,
|
||||||
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>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -137,7 +164,13 @@ function CheckboxFormMultiple({
|
|||||||
>
|
>
|
||||||
{t('common.clear')}
|
{t('common.clear')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" size={'sm'}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
console.log(form.formState.errors, form.getValues());
|
||||||
|
}}
|
||||||
|
size={'sm'}
|
||||||
|
>
|
||||||
{t('common.submit')}
|
{t('common.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ interface IProps {
|
|||||||
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
|
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
showFilter?: boolean;
|
showFilter?: boolean;
|
||||||
leftPanel?: ReactNode;
|
leftPanel?: ReactNode;
|
||||||
|
preChildren?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterButton = React.forwardRef<
|
export const FilterButton = React.forwardRef<
|
||||||
@ -46,6 +47,7 @@ export const FilterButton = React.forwardRef<
|
|||||||
export default function ListFilterBar({
|
export default function ListFilterBar({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
|
preChildren,
|
||||||
searchString,
|
searchString,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
showFilter = true,
|
showFilter = true,
|
||||||
@ -63,7 +65,18 @@ export default function ListFilterBar({
|
|||||||
const filterCount = useMemo(() => {
|
const filterCount = useMemo(() => {
|
||||||
return typeof value === 'object' && value !== null
|
return typeof value === 'object' && value !== null
|
||||||
? Object.values(value).reduce((pre, cur) => {
|
? 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)
|
||||||
: 0;
|
: 0;
|
||||||
}, [value]);
|
}, [value]);
|
||||||
@ -80,6 +93,7 @@ export default function ListFilterBar({
|
|||||||
{leftPanel || title}
|
{leftPanel || title}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-5 items-center">
|
<div className="flex gap-5 items-center">
|
||||||
|
{preChildren}
|
||||||
{showFilter && (
|
{showFilter && (
|
||||||
<FilterPopover
|
<FilterPopover
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
export type FilterType = {
|
export type FilterType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
field?: string;
|
||||||
label: string | JSX.Element;
|
label: string | JSX.Element;
|
||||||
|
list?: FilterType[];
|
||||||
|
value?: string | string[];
|
||||||
count?: number;
|
count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -10,6 +13,9 @@ export type FilterCollection = {
|
|||||||
list: FilterType[];
|
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;
|
export type FilterChange = (value: FilterValue) => void;
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { getExtension } from '@/utils/document-util';
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||||
import Markdown from 'react-markdown';
|
import Markdown from 'react-markdown';
|
||||||
import reactStringReplace from 'react-string-replace';
|
|
||||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
import rehypeRaw from 'rehype-raw';
|
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 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentReg,
|
|
||||||
preprocessLaTeX,
|
preprocessLaTeX,
|
||||||
replaceTextByOldReg,
|
replaceTextByOldReg,
|
||||||
replaceThinkToSection,
|
replaceThinkToSection,
|
||||||
@ -31,6 +29,11 @@ import classNames from 'classnames';
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { pipe } from 'lodash/fp';
|
import { pipe } from 'lodash/fp';
|
||||||
import { CircleAlert } from 'lucide-react';
|
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 { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
@ -39,6 +42,19 @@ import {
|
|||||||
} from '../ui/hover-card';
|
} from '../ui/hover-card';
|
||||||
import styles from './index.less';
|
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);
|
const getChunkIndex = (match: string) => Number(match);
|
||||||
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
||||||
function MarkdownContent({
|
function MarkdownContent({
|
||||||
@ -211,47 +227,95 @@ function MarkdownContent({
|
|||||||
|
|
||||||
const renderReference = useCallback(
|
const renderReference = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
const groups = groupConsecutiveReferences(text);
|
||||||
const chunkIndex = getChunkIndex(match);
|
const elements = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
const { documentUrl, fileExtension, imageId, chunkItem, documentId } =
|
const convertedReference = reference
|
||||||
getReferenceInfo(chunkIndex);
|
? 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) ? (
|
if (
|
||||||
<section>
|
convertedReference &&
|
||||||
<Image
|
shouldShowCarousel(group, convertedReference)
|
||||||
id={imageId}
|
) {
|
||||||
className={styles.referenceInnerChunkImage}
|
elements.push(
|
||||||
onClick={
|
<ImageCarousel
|
||||||
documentId
|
key={`carousel-${groupIndex}`}
|
||||||
? handleDocumentButtonClick(
|
group={group}
|
||||||
documentId,
|
reference={convertedReference}
|
||||||
chunkItem,
|
fileThumbnails={fileThumbnails}
|
||||||
fileExtension === 'pdf',
|
onImageClick={handleDocumentButtonClick}
|
||||||
documentUrl,
|
/>,
|
||||||
)
|
);
|
||||||
: () => {}
|
} else {
|
||||||
}
|
group.forEach((ref) => {
|
||||||
></Image>
|
const chunkIndex = getChunkIndex(ref.id);
|
||||||
<span className="text-accent-primary">{imageId}</span>
|
const {
|
||||||
</section>
|
documentUrl,
|
||||||
) : (
|
fileExtension,
|
||||||
<HoverCard key={i}>
|
imageId,
|
||||||
<HoverCardTrigger>
|
chunkItem,
|
||||||
<CircleAlert className="size-4 inline-block" />
|
documentId,
|
||||||
</HoverCardTrigger>
|
} = getReferenceInfo(chunkIndex);
|
||||||
<HoverCardContent className="max-w-3xl">
|
const docType = chunkItem?.doc_type;
|
||||||
{renderPopoverContent(chunkIndex)}
|
|
||||||
</HoverCardContent>
|
if (showImage(docType)) {
|
||||||
</HoverCard>
|
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 (
|
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,
|
suffix: filterValue.type,
|
||||||
run_status: filterValue.run,
|
run_status: filterValue.run,
|
||||||
|
metadata: filterValue.metadata,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (ret.data.code === 0) {
|
if (ret.data.code === 0) {
|
||||||
@ -196,6 +197,7 @@ export const useGetDocumentFilter = (): {
|
|||||||
filter: data?.filter || {
|
filter: data?.filter || {
|
||||||
run_status: {},
|
run_status: {},
|
||||||
suffix: {},
|
suffix: {},
|
||||||
|
metadata: {},
|
||||||
},
|
},
|
||||||
onOpenChange: handleOnpenChange,
|
onOpenChange: handleOnpenChange,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -60,4 +60,5 @@ interface GraphRag {
|
|||||||
export type IDocumentInfoFilter = {
|
export type IDocumentInfoFilter = {
|
||||||
run_status: Record<number, number>;
|
run_status: Record<number, number>;
|
||||||
suffix: Record<string, number>;
|
suffix: Record<string, number>;
|
||||||
|
metadata: Record<string, Record<string, number>>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -116,12 +116,15 @@ export interface ITenantInfo {
|
|||||||
tts_id: string;
|
tts_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChunkDocType = 'image' | 'table';
|
||||||
|
|
||||||
export interface IChunk {
|
export interface IChunk {
|
||||||
available_int: number; // Whether to enable, 0: not enabled, 1: enabled
|
available_int: number; // Whether to enable, 0: not enabled, 1: enabled
|
||||||
chunk_id: string;
|
chunk_id: string;
|
||||||
content_with_weight: string;
|
content_with_weight: string;
|
||||||
doc_id: string;
|
doc_id: string;
|
||||||
doc_name: string;
|
doc_name: string;
|
||||||
|
doc_type_kwd?: ChunkDocType;
|
||||||
image_id: string;
|
image_id: string;
|
||||||
important_kwd?: string[];
|
important_kwd?: string[];
|
||||||
question_kwd?: string[]; // keywords
|
question_kwd?: string[]; // keywords
|
||||||
|
|||||||
@ -364,6 +364,9 @@ Procedural Memory: Learned skills, habits, and automated procedures.`,
|
|||||||
},
|
},
|
||||||
knowledgeConfiguration: {
|
knowledgeConfiguration: {
|
||||||
settings: 'Settings',
|
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',
|
autoMetadata: 'Auto metadata',
|
||||||
mineruOptions: 'MinerU Options',
|
mineruOptions: 'MinerU Options',
|
||||||
mineruParseMethod: 'Parse Method',
|
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',
|
'The document being parsed cannot be deleted',
|
||||||
},
|
},
|
||||||
chunk: {
|
chunk: {
|
||||||
|
type: 'Type',
|
||||||
|
docType: {
|
||||||
|
image: 'Image',
|
||||||
|
table: 'Table',
|
||||||
|
text: 'Text',
|
||||||
|
},
|
||||||
chunk: 'Chunk',
|
chunk: 'Chunk',
|
||||||
bulk: 'Bulk',
|
bulk: 'Bulk',
|
||||||
selectAll: 'Select all',
|
selectAll: 'Select all',
|
||||||
@ -2084,12 +2093,14 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
schema: 'Schema',
|
schema: 'Schema',
|
||||||
response: 'Response',
|
response: 'Response',
|
||||||
executionMode: 'Execution mode',
|
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',
|
authMethods: 'Authentication methods',
|
||||||
authType: 'Authentication type',
|
authType: 'Authentication type',
|
||||||
limit: 'Request limit',
|
limit: 'Request limit',
|
||||||
per: 'Time period',
|
per: 'Time period',
|
||||||
maxBodySize: 'Maximum body size',
|
maxBodySize: 'Maximum body size',
|
||||||
ipWhitelist: 'Ip whitelist',
|
ipWhitelist: 'IP whitelist',
|
||||||
tokenHeader: 'Token header',
|
tokenHeader: 'Token header',
|
||||||
tokenValue: 'Token value',
|
tokenValue: 'Token value',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
@ -2109,6 +2120,8 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
queryParameters: 'Query parameters',
|
queryParameters: 'Query parameters',
|
||||||
headerParameters: 'Header parameters',
|
headerParameters: 'Header parameters',
|
||||||
requestBodyParameters: 'Request body parameters',
|
requestBodyParameters: 'Request body parameters',
|
||||||
|
streaming: 'Accepted response',
|
||||||
|
immediately: 'Final response',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
llmTools: {
|
llmTools: {
|
||||||
|
|||||||
@ -1835,6 +1835,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
|||||||
schema: '模式',
|
schema: '模式',
|
||||||
response: '响应',
|
response: '响应',
|
||||||
executionMode: '执行模式',
|
executionMode: '执行模式',
|
||||||
|
executionModeTip:
|
||||||
|
'Accepted Response:请求校验通过后立即返回接收成功响应,工作流在后台异步执行;Final Response:系统在工作流执行完成后返回最终处理结果',
|
||||||
authMethods: '认证方法',
|
authMethods: '认证方法',
|
||||||
authType: '认证类型',
|
authType: '认证类型',
|
||||||
limit: '请求限制',
|
limit: '请求限制',
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Collapse } from '@/components/collapse';
|
import { Collapse } from '@/components/collapse';
|
||||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { WebHookResponseStatusFormField } from '@/components/webhook-response-status';
|
||||||
import { WebhookExecutionMode } from '@/pages/agent/constant';
|
import { WebhookExecutionMode } from '@/pages/agent/constant';
|
||||||
import { buildOptions } from '@/utils/form';
|
import { buildOptions } from '@/utils/form';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
@ -24,19 +24,17 @@ export function WebhookResponse() {
|
|||||||
<RAGFlowFormItem
|
<RAGFlowFormItem
|
||||||
name="execution_mode"
|
name="execution_mode"
|
||||||
label={t('flow.webhook.executionMode')}
|
label={t('flow.webhook.executionMode')}
|
||||||
|
tooltip={t('flow.webhook.executionModeTip')}
|
||||||
>
|
>
|
||||||
<SelectWithSearch
|
<SelectWithSearch
|
||||||
options={buildOptions(WebhookExecutionMode)}
|
options={buildOptions(WebhookExecutionMode, t, 'flow.webhook')}
|
||||||
></SelectWithSearch>
|
></SelectWithSearch>
|
||||||
</RAGFlowFormItem>
|
</RAGFlowFormItem>
|
||||||
{executionMode === WebhookExecutionMode.Immediately && (
|
{executionMode === WebhookExecutionMode.Immediately && (
|
||||||
<>
|
<>
|
||||||
<RAGFlowFormItem
|
<WebHookResponseStatusFormField
|
||||||
name={'response.status'}
|
name={'response.status'}
|
||||||
label={t('flow.webhook.status')}
|
></WebHookResponseStatusFormField>
|
||||||
>
|
|
||||||
<Input type="number"></Input>
|
|
||||||
</RAGFlowFormItem>
|
|
||||||
{/* <DynamicResponse
|
{/* <DynamicResponse
|
||||||
name="response.headers_template"
|
name="response.headers_template"
|
||||||
label={t('flow.webhook.headersTemplate')}
|
label={t('flow.webhook.headersTemplate')}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { RAGFlowSelect } from '@/components/ui/select';
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { WebHookResponseStatusFormField } from '@/components/webhook-response-status';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -20,6 +21,7 @@ import { ExportFileType } from '../../constant';
|
|||||||
import { INextOperatorForm } from '../../interface';
|
import { INextOperatorForm } from '../../interface';
|
||||||
import { FormWrapper } from '../components/form-wrapper';
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
import { PromptEditor } from '../components/prompt-editor';
|
import { PromptEditor } from '../components/prompt-editor';
|
||||||
|
import { useShowWebhookResponseStatus } from './use-show-response-status';
|
||||||
import { useValues } from './use-values';
|
import { useValues } from './use-values';
|
||||||
import { useWatchFormChange } from './use-watch-change';
|
import { useWatchFormChange } from './use-watch-change';
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ function MessageForm({ node }: INextOperatorForm) {
|
|||||||
.optional(),
|
.optional(),
|
||||||
output_format: z.string().optional(),
|
output_format: z.string().optional(),
|
||||||
auto_play: z.boolean().optional(),
|
auto_play: z.boolean().optional(),
|
||||||
|
status: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -56,9 +59,14 @@ function MessageForm({ node }: INextOperatorForm) {
|
|||||||
control: form.control,
|
control: form.control,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showWebhookResponseStatus = useShowWebhookResponseStatus(form);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
|
{showWebhookResponseStatus && (
|
||||||
|
<WebHookResponseStatusFormField name="status"></WebHookResponseStatusFormField>
|
||||||
|
)}
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel>
|
<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 {
|
.imagePreview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
max-width: 50vw;
|
max-width: 50vw;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import Image from '@/components/image';
|
|||||||
import { useTheme } from '@/components/theme-provider';
|
import { useTheme } from '@/components/theme-provider';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@/components/ui/popover';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
import { IChunk } from '@/interfaces/database/knowledge';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { CheckedState } from '@radix-ui/react-checkbox';
|
import { CheckedState } from '@radix-ui/react-checkbox';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChunkTextMode } from '../../constant';
|
import { ChunkTextMode } from '../../constant';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ const ChunkCard = ({
|
|||||||
textMode,
|
textMode,
|
||||||
t: imageCacheKey,
|
t: imageCacheKey,
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const available = Number(item.available_int);
|
const available = Number(item.available_int);
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@ -63,51 +66,59 @@ const ChunkCard = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEnabled(available === 1);
|
setEnabled(available === 1);
|
||||||
}, [available]);
|
}, [available]);
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={classNames(styles.chunkCard, {
|
className={classNames('relative flex-none', styles.chunkCard, {
|
||||||
[`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]:
|
[`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]:
|
||||||
selected,
|
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">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<Checkbox onCheckedChange={handleCheck} checked={checked}></Checkbox>
|
<Checkbox onCheckedChange={handleCheck} checked={checked}></Checkbox>
|
||||||
|
|
||||||
|
{/* Using <Tooltip> instead of <Popover> to avoid flickering when hovering over the image */}
|
||||||
{item.image_id && (
|
{item.image_id && (
|
||||||
<Popover open={open}>
|
<Tooltip>
|
||||||
<PopoverTrigger
|
<TooltipTrigger>
|
||||||
asChild
|
<Image
|
||||||
onMouseEnter={() => setOpen(true)}
|
t={imageCacheKey}
|
||||||
onMouseLeave={() => setOpen(false)}
|
id={item.image_id}
|
||||||
>
|
className={styles.image}
|
||||||
<div>
|
/>
|
||||||
<Image
|
</TooltipTrigger>
|
||||||
t={imageCacheKey}
|
<TooltipContent
|
||||||
id={item.image_id}
|
|
||||||
className={styles.image}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="p-0"
|
className="p-0"
|
||||||
align={'start'}
|
align={'start'}
|
||||||
side={'right'}
|
side={'right'}
|
||||||
sideOffset={-20}
|
sideOffset={-20}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div>
|
<Image
|
||||||
<Image
|
t={imageCacheKey}
|
||||||
t={imageCacheKey}
|
id={item.image_id}
|
||||||
id={item.image_id}
|
className={styles.imagePreview}
|
||||||
className={styles.imagePreview}
|
/>
|
||||||
></Image>
|
</TooltipContent>
|
||||||
</div>
|
</Tooltip>
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section
|
<section
|
||||||
onDoubleClick={handleContentDoubleClick}
|
onDoubleClick={handleContentDoubleClick}
|
||||||
onClick={handleContentClick}
|
onClick={handleContentClick}
|
||||||
className={styles.content}
|
className={cn(styles.content, 'mt-2')}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
@ -118,7 +129,8 @@ const ChunkCard = ({
|
|||||||
})}
|
})}
|
||||||
></div>
|
></div>
|
||||||
</section>
|
</section>
|
||||||
<div>
|
|
||||||
|
<div className="mt-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '@/components/ui/hover-card';
|
} from '@/components/ui/hover-card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Modal } from '@/components/ui/modal/modal';
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
import Space from '@/components/ui/space';
|
import Space from '@/components/ui/space';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
@ -76,6 +77,7 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
|||||||
const { removeChunk } = useDeleteChunkByIds();
|
const { removeChunk } = useDeleteChunkByIds();
|
||||||
const { data } = useFetchChunk(chunkId);
|
const { data } = useFetchChunk(chunkId);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isEditMode = !!chunkId;
|
||||||
|
|
||||||
const isTagParser = parserId === 'tag';
|
const isTagParser = parserId === 'tag';
|
||||||
const onSubmit = useCallback(
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="image"
|
name="image"
|
||||||
@ -151,18 +175,18 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="gap-1">{t('chunk.image')}</FormLabel>
|
<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 && (
|
{data?.data?.img_id && (
|
||||||
<Image
|
<Image
|
||||||
id={data?.data?.img_id}
|
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">
|
<div className="col-start-2 col-end-3 only:col-span-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
className="h-48"
|
className="h-auto p-6"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
accept={{
|
accept={{
|
||||||
@ -171,6 +195,7 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
|||||||
'image/webp': [],
|
'image/webp': [],
|
||||||
}}
|
}}
|
||||||
maxFileCount={1}
|
maxFileCount={1}
|
||||||
|
hideDropzoneOnMaxFileCount
|
||||||
title={t('chunk.imageUploaderTitle')}
|
title={t('chunk.imageUploaderTitle')}
|
||||||
description={<></>}
|
description={<></>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default (props: ICheckboxSetProps) => {
|
|||||||
}, [selectedChunkIds]);
|
}, [selectedChunkIds]);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-3 cursor-pointer text-muted-foreground hover:text-text-primary">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="all_chunks_checkbox"
|
id="all_chunks_checkbox"
|
||||||
|
|||||||
@ -204,7 +204,8 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{metaData.restrictDefinedValues && (
|
{((metaData.restrictDefinedValues && isShowValueSwitch) ||
|
||||||
|
!isShowValueSwitch) && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>{t('knowledgeDetails.metadata.values')}</div>
|
<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() {
|
export function OverlappedPercent() {
|
||||||
return (
|
return (
|
||||||
<SliderInputFormField
|
<SliderInputFormField
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
AutoMetadata,
|
AutoMetadata,
|
||||||
EnableTocToggle,
|
EnableTocToggle,
|
||||||
|
ImageContextWindow,
|
||||||
OverlappedPercent,
|
OverlappedPercent,
|
||||||
} from './common-item';
|
} from './common-item';
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export function NaiveConfiguration() {
|
|||||||
<DelimiterFormField></DelimiterFormField>
|
<DelimiterFormField></DelimiterFormField>
|
||||||
<ChildrenDelimiterForm />
|
<ChildrenDelimiterForm />
|
||||||
<EnableTocToggle />
|
<EnableTocToggle />
|
||||||
|
<ImageContextWindow />
|
||||||
<AutoMetadata />
|
<AutoMetadata />
|
||||||
<OverlappedPercent />
|
<OverlappedPercent />
|
||||||
</ConfigurationFormContainer>
|
</ConfigurationFormContainer>
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export const formSchema = z
|
|||||||
tag_kb_ids: z.array(z.string()).nullish(),
|
tag_kb_ids: z.array(z.string()).nullish(),
|
||||||
topn_tags: z.number().optional(),
|
topn_tags: z.number().optional(),
|
||||||
toc_extraction: z.boolean().optional(),
|
toc_extraction: z.boolean().optional(),
|
||||||
|
image_context_window: z.number().optional(),
|
||||||
overlapped_percent: z.number().optional(),
|
overlapped_percent: z.number().optional(),
|
||||||
// MinerU-specific options
|
// MinerU-specific options
|
||||||
mineru_parse_method: z.enum(['auto', 'txt', 'ocr']).optional(),
|
mineru_parse_method: z.enum(['auto', 'txt', 'ocr']).optional(),
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export default function DatasetSettings() {
|
|||||||
html4excel: false,
|
html4excel: false,
|
||||||
topn_tags: 3,
|
topn_tags: 3,
|
||||||
toc_extraction: false,
|
toc_extraction: false,
|
||||||
|
image_context_window: 0,
|
||||||
overlapped_percent: 0,
|
overlapped_percent: 0,
|
||||||
// MinerU-specific defaults
|
// MinerU-specific defaults
|
||||||
mineru_parse_method: 'auto',
|
mineru_parse_method: 'auto',
|
||||||
|
|||||||
@ -216,12 +216,6 @@ const Generate: React.FC<GenerateProps> = (props) => {
|
|||||||
? graphRunData
|
? graphRunData
|
||||||
: raptorRunData
|
: raptorRunData
|
||||||
) as ITraceInfo;
|
) as ITraceInfo;
|
||||||
console.log(
|
|
||||||
name,
|
|
||||||
'data',
|
|
||||||
data,
|
|
||||||
!data || (!data.progress && data.progress !== 0),
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div key={name}>
|
<div key={name}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@ -25,12 +25,33 @@ export function useSelectDatasetFilters() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [filter.run_status, t]);
|
}, [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(() => {
|
const filters: FilterCollection[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ field: 'type', label: 'File Type', list: fileTypes },
|
{ field: 'type', label: 'File Type', list: fileTypes },
|
||||||
{ field: 'run', label: 'Status', list: fileStatus },
|
{ field: 'run', label: 'Status', list: fileStatus },
|
||||||
|
{ field: 'metadata', label: 'metadata', list: metaDataList },
|
||||||
] as FilterCollection[];
|
] as FilterCollection[];
|
||||||
}, [fileStatus, fileTypes]);
|
}, [fileStatus, fileTypes, metaDataList]);
|
||||||
|
|
||||||
return { filters, onOpenChange };
|
return { filters, onOpenChange };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
|||||||
import { getExtension } from '@/utils/document-util';
|
import { getExtension } from '@/utils/document-util';
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import DOMPurify from 'dompurify';
|
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 Markdown from 'react-markdown';
|
||||||
import reactStringReplace from 'react-string-replace';
|
|
||||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
import rehypeRaw from 'rehype-raw';
|
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 '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 {
|
import {
|
||||||
currentReg,
|
|
||||||
preprocessLaTeX,
|
preprocessLaTeX,
|
||||||
replaceTextByOldReg,
|
replaceTextByOldReg,
|
||||||
replaceThinkToSection,
|
replaceThinkToSection,
|
||||||
@ -37,8 +41,6 @@ import classNames from 'classnames';
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { pipe } from 'lodash/fp';
|
import { pipe } from 'lodash/fp';
|
||||||
|
|
||||||
const getChunkIndex = (match: string) => Number(match);
|
|
||||||
|
|
||||||
// Defining Tailwind CSS class name constants
|
// Defining Tailwind CSS class name constants
|
||||||
const styles = {
|
const styles = {
|
||||||
referenceChunkImage: 'w-[10vw] object-contain',
|
referenceChunkImage: 'w-[10vw] object-contain',
|
||||||
@ -86,18 +88,11 @@ const MarkdownContent = ({
|
|||||||
(
|
(
|
||||||
documentId: string,
|
documentId: string,
|
||||||
chunk: IReferenceChunk,
|
chunk: IReferenceChunk,
|
||||||
// isPdf: boolean,
|
isPdf: boolean = false,
|
||||||
// documentUrl?: string,
|
documentUrl?: string,
|
||||||
) =>
|
) =>
|
||||||
() => {
|
() => {
|
||||||
// if (!isPdf) {
|
|
||||||
// if (!documentUrl) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// window.open(documentUrl, '_blank');
|
|
||||||
// } else {
|
|
||||||
clickDocumentButton?.(documentId, chunk);
|
clickDocumentButton?.(documentId, chunk);
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
[clickDocumentButton],
|
[clickDocumentButton],
|
||||||
);
|
);
|
||||||
@ -218,43 +213,83 @@ const MarkdownContent = ({
|
|||||||
|
|
||||||
const renderReference = useCallback(
|
const renderReference = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
let replacedText = reactStringReplace(text, currentReg, (match, i) => {
|
const groups = groupConsecutiveReferences(text);
|
||||||
const chunkIndex = getChunkIndex(match);
|
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) ? (
|
if (showImage(docType)) {
|
||||||
<Image
|
elements.push(
|
||||||
id={imageId}
|
<Image
|
||||||
className={styles.referenceInnerChunkImage}
|
key={ref.id}
|
||||||
onClick={
|
id={imageId}
|
||||||
documentId
|
className={styles.referenceInnerChunkImage}
|
||||||
? handleDocumentButtonClick(
|
onClick={
|
||||||
documentId,
|
documentId
|
||||||
chunkItem,
|
? handleDocumentButtonClick(documentId, chunkItem)
|
||||||
// fileExtension === 'pdf',
|
: () => {}
|
||||||
// documentUrl,
|
}
|
||||||
)
|
/>,
|
||||||
: () => {}
|
);
|
||||||
|
} else {
|
||||||
|
elements.push(
|
||||||
|
<Popover key={ref.id}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<InfoCircleOutlined className={styles.referenceIcon} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="!w-fit">
|
||||||
|
{getPopoverContent(chunkIndex)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
></Image>
|
// Add the original reference text
|
||||||
) : (
|
elements.push(ref.fullMatch);
|
||||||
<Popover>
|
});
|
||||||
<PopoverTrigger>
|
}
|
||||||
<InfoCircleOutlined className={styles.referenceIcon} />
|
|
||||||
</PopoverTrigger>
|
lastIndex = group[group.length - 1].end;
|
||||||
<PopoverContent className="!w-fit">
|
|
||||||
{getPopoverContent(chunkIndex)}
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
@ -265,7 +300,11 @@ const MarkdownContent = ({
|
|||||||
components={
|
components={
|
||||||
{
|
{
|
||||||
'custom-typography': ({ children }: { children: string }) =>
|
'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) {
|
code(props: any) {
|
||||||
const { children, className, ...rest } = props;
|
const { children, className, ...rest } = props;
|
||||||
const restProps = omit(rest, 'node');
|
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 { t, TFunction } from 'i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 BoxTokenField from './component/box-token-field';
|
||||||
import { ConfluenceIndexingModeField } from './component/confluence-token-field';
|
import { ConfluenceIndexingModeField } from './component/confluence-token-field';
|
||||||
import GmailTokenField from './component/gmail-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 = () => {
|
export const useDataSourceInfo = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dataSourceInfo, setDataSourceInfo] = useState<IDataSourceInfoMap>(
|
const [dataSourceInfo, setDataSourceInfo] = useState<IDataSourceInfoMap>(
|
||||||
@ -222,18 +229,6 @@ export const DataSourceFormFields = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[DataSourceKey.S3]: [
|
[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',
|
label: 'Bucket Name',
|
||||||
name: 'config.bucket_name',
|
name: 'config.bucket_name',
|
||||||
@ -241,39 +236,21 @@ export const DataSourceFormFields = {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Bucket Type',
|
label: 'Region',
|
||||||
name: 'config.bucket_type',
|
name: 'config.credentials.region',
|
||||||
type: FormFieldType.Select,
|
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,
|
required: false,
|
||||||
placeholder: 'Virtual Hosted Style',
|
options: awsRegionOptions,
|
||||||
tooltip: t('setting.S3CompatibleAddressingStyleTip'),
|
customValidate: (val: string, formValues: any) => {
|
||||||
shouldRender: (formValues: any) => {
|
const credentials = formValues?.config?.credentials || {};
|
||||||
return formValues?.config?.bucket_type === 's3_compatible';
|
const bucketType = formValues?.config?.bucket_type || 's3';
|
||||||
},
|
const hasAccessKey = Boolean(
|
||||||
},
|
credentials.aws_access_key_id || credentials.aws_secret_access_key,
|
||||||
{
|
);
|
||||||
label: 'Endpoint URL',
|
if (bucketType === 's3' && hasAccessKey) {
|
||||||
name: 'config.credentials.endpoint_url',
|
return Boolean(val) || 'Region is required when using access key';
|
||||||
type: FormFieldType.Text,
|
}
|
||||||
required: false,
|
return true;
|
||||||
placeholder: 'https://fsn1.your-objectstorage.com',
|
|
||||||
tooltip: t('setting.S3CompatibleEndpointUrlTip'),
|
|
||||||
shouldRender: (formValues: any) => {
|
|
||||||
return formValues?.config?.bucket_type === 's3_compatible';
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -283,6 +260,14 @@ export const DataSourceFormFields = {
|
|||||||
required: false,
|
required: false,
|
||||||
tooltip: t('setting.s3PrefixTip'),
|
tooltip: t('setting.s3PrefixTip'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Credentials',
|
||||||
|
name: 'config.credentials.__blob_token',
|
||||||
|
type: FormFieldType.Custom,
|
||||||
|
hideLabel: true,
|
||||||
|
required: false,
|
||||||
|
render: () => <BlobTokenField />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[DataSourceKey.NOTION]: [
|
[DataSourceKey.NOTION]: [
|
||||||
{
|
{
|
||||||
@ -700,6 +685,9 @@ export const DataSourceFormDefaultValues = {
|
|||||||
credentials: {
|
credentials: {
|
||||||
aws_access_key_id: '',
|
aws_access_key_id: '',
|
||||||
aws_secret_access_key: '',
|
aws_secret_access_key: '',
|
||||||
|
region: '',
|
||||||
|
authentication_method: 'access_key',
|
||||||
|
aws_role_arn: '',
|
||||||
endpoint_url: '',
|
endpoint_url: '',
|
||||||
addressing_style: 'virtual',
|
addressing_style: 'virtual',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { RunningStatus } from '@/constants/knowledge';
|
import { RunningStatus } from '@/constants/knowledge';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { debounce } from 'lodash';
|
import { CirclePause, Loader2, Repeat } from 'lucide-react';
|
||||||
import { CirclePause, Repeat } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FieldValues } from 'react-hook-form';
|
import { FieldValues } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
@ -120,11 +119,11 @@ const SourceDetailPage = () => {
|
|||||||
];
|
];
|
||||||
}, [detail, runSchedule]);
|
}, [detail, runSchedule]);
|
||||||
|
|
||||||
const { handleAddOk } = useAddDataSource();
|
const { addLoading, handleAddOk } = useAddDataSource();
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
const onSubmit = useCallback(() => {
|
||||||
formRef?.current?.submit();
|
formRef?.current?.submit();
|
||||||
}, [formRef]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (detail) {
|
if (detail) {
|
||||||
@ -140,9 +139,7 @@ const SourceDetailPage = () => {
|
|||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
horizontal: true,
|
horizontal: true,
|
||||||
onChange: () => {
|
onChange: undefined,
|
||||||
onSubmit();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setFields(newFields);
|
setFields(newFields);
|
||||||
@ -175,12 +172,23 @@ const SourceDetailPage = () => {
|
|||||||
<DynamicForm.Root
|
<DynamicForm.Root
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={debounce((data) => {
|
onSubmit={(data) => handleAddOk(data)}
|
||||||
handleAddOk(data);
|
|
||||||
}, 500)}
|
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<section className="flex flex-col gap-2">
|
||||||
<div className="text-2xl text-text-primary mb-2">
|
<div className="text-2xl text-text-primary mb-2">
|
||||||
{t('setting.log')}
|
{t('setting.log')}
|
||||||
|
|||||||
Reference in New Issue
Block a user