diff --git a/api/.env.example b/api/.env.example index 6dcbae5db2..c793d650d2 100644 --- a/api/.env.example +++ b/api/.env.example @@ -446,3 +446,5 @@ CREATE_TIDB_SERVICE_JOB_ENABLED=false # Maximum number of submitted thread count in a ThreadPool for parallel node execution MAX_SUBMIT_COUNT=100 +# Lockout duration in seconds +LOGIN_LOCKOUT_DURATION=86400 \ No newline at end of file diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index f1a1b92e98..06928c9c74 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -543,6 +543,11 @@ class AuthConfig(BaseSettings): default=60, ) + LOGIN_LOCKOUT_DURATION: PositiveInt = Field( + description="Time (in seconds) a user must wait before retrying login after exceeding the rate limit.", + default=86400, + ) + class ModerationConfig(BaseSettings): """ diff --git a/api/core/model_runtime/model_providers/gitee_ai/llm/InternVL2-8B.yaml b/api/core/model_runtime/model_providers/gitee_ai/llm/InternVL2-8B.yaml new file mode 100644 index 0000000000..d288c3dd39 --- /dev/null +++ b/api/core/model_runtime/model_providers/gitee_ai/llm/InternVL2-8B.yaml @@ -0,0 +1,93 @@ +model: InternVL2-8B +label: + en_US: InternVL2-8B +model_type: llm +features: + - vision + - agent-thought +model_properties: + mode: chat + context_size: 32000 +parameter_rules: + - name: max_tokens + use_template: max_tokens + label: + en_US: "Max Tokens" + zh_Hans: "最大Token数" + type: int + default: 512 + min: 1 + required: true + help: + en_US: "The maximum number of tokens that can be generated by the model varies depending on the model." + zh_Hans: "模型可生成的最大 token 个数,不同模型上限不同。" + + - name: temperature + use_template: temperature + label: + en_US: "Temperature" + zh_Hans: "采样温度" + type: float + default: 0.7 + min: 0.0 + max: 1.0 + precision: 1 + required: true + help: + en_US: "The randomness of the sampling temperature control output. The temperature value is within the range of [0.0, 1.0]. The higher the value, the more random and creative the output; the lower the value, the more stable it is. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样温度控制输出的随机性。温度值在 [0.0, 1.0] 范围内,值越高,输出越随机和创造性;值越低,输出越稳定。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: top_p + use_template: top_p + label: + en_US: "Top P" + zh_Hans: "Top P" + type: float + default: 0.7 + min: 0.0 + max: 1.0 + precision: 1 + required: true + help: + en_US: "The value range of the sampling method is [0.0, 1.0]. The top_p value determines that the model selects tokens from the top p% of candidate words with the highest probability; when top_p is 0, this parameter is invalid. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样方法的取值范围为 [0.0,1.0]。top_p 值确定模型从概率最高的前p%的候选词中选取 tokens;当 top_p 为 0 时,此参数无效。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: top_k + use_template: top_k + label: + en_US: "Top K" + zh_Hans: "Top K" + type: int + default: 50 + min: 0 + max: 100 + required: true + help: + en_US: "The value range is [0,100], which limits the model to only select from the top k words with the highest probability when choosing the next word at each step. The larger the value, the more diverse text generation will be." + zh_Hans: "取值范围为 [0,100],限制模型在每一步选择下一个词时,只从概率最高的前 k 个词中选取。数值越大,文本生成越多样。" + + - name: frequency_penalty + use_template: frequency_penalty + label: + en_US: "Frequency Penalty" + zh_Hans: "频率惩罚" + type: float + default: 0 + min: -1.0 + max: 1.0 + precision: 1 + required: false + help: + en_US: "Used to adjust the frequency of repeated content in automatically generated text. Positive numbers reduce repetition, while negative numbers increase repetition. After setting this parameter, if a word has already appeared in the text, the model will decrease the probability of choosing that word for subsequent generation." + zh_Hans: "用于调整自动生成文本中重复内容的频率。正数减少重复,负数增加重复。设置此参数后,如果一个词在文本中已经出现过,模型在后续生成中选择该词的概率会降低。" + + - name: user + use_template: text + label: + en_US: "User" + zh_Hans: "用户" + type: string + required: false + help: + en_US: "Used to track and differentiate conversation requests from different users." + zh_Hans: "用于追踪和区分不同用户的对话请求。" diff --git a/api/core/model_runtime/model_providers/gitee_ai/llm/InternVL2.5-26B.yaml b/api/core/model_runtime/model_providers/gitee_ai/llm/InternVL2.5-26B.yaml new file mode 100644 index 0000000000..b2dee88c02 --- /dev/null +++ b/api/core/model_runtime/model_providers/gitee_ai/llm/InternVL2.5-26B.yaml @@ -0,0 +1,93 @@ +model: InternVL2.5-26B +label: + en_US: InternVL2.5-26B +model_type: llm +features: + - vision + - agent-thought +model_properties: + mode: chat + context_size: 32000 +parameter_rules: + - name: max_tokens + use_template: max_tokens + label: + en_US: "Max Tokens" + zh_Hans: "最大Token数" + type: int + default: 512 + min: 1 + required: true + help: + en_US: "The maximum number of tokens that can be generated by the model varies depending on the model." + zh_Hans: "模型可生成的最大 token 个数,不同模型上限不同。" + + - name: temperature + use_template: temperature + label: + en_US: "Temperature" + zh_Hans: "采样温度" + type: float + default: 0.7 + min: 0.0 + max: 1.0 + precision: 1 + required: true + help: + en_US: "The randomness of the sampling temperature control output. The temperature value is within the range of [0.0, 1.0]. The higher the value, the more random and creative the output; the lower the value, the more stable it is. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样温度控制输出的随机性。温度值在 [0.0, 1.0] 范围内,值越高,输出越随机和创造性;值越低,输出越稳定。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: top_p + use_template: top_p + label: + en_US: "Top P" + zh_Hans: "Top P" + type: float + default: 0.7 + min: 0.0 + max: 1.0 + precision: 1 + required: true + help: + en_US: "The value range of the sampling method is [0.0, 1.0]. The top_p value determines that the model selects tokens from the top p% of candidate words with the highest probability; when top_p is 0, this parameter is invalid. It is recommended to adjust either top_p or temperature parameters according to your needs to avoid adjusting both at the same time." + zh_Hans: "采样方法的取值范围为 [0.0,1.0]。top_p 值确定模型从概率最高的前p%的候选词中选取 tokens;当 top_p 为 0 时,此参数无效。建议根据需求调整 top_p 或 temperature 参数,避免同时调整两者。" + + - name: top_k + use_template: top_k + label: + en_US: "Top K" + zh_Hans: "Top K" + type: int + default: 50 + min: 0 + max: 100 + required: true + help: + en_US: "The value range is [0,100], which limits the model to only select from the top k words with the highest probability when choosing the next word at each step. The larger the value, the more diverse text generation will be." + zh_Hans: "取值范围为 [0,100],限制模型在每一步选择下一个词时,只从概率最高的前 k 个词中选取。数值越大,文本生成越多样。" + + - name: frequency_penalty + use_template: frequency_penalty + label: + en_US: "Frequency Penalty" + zh_Hans: "频率惩罚" + type: float + default: 0 + min: -1.0 + max: 1.0 + precision: 1 + required: false + help: + en_US: "Used to adjust the frequency of repeated content in automatically generated text. Positive numbers reduce repetition, while negative numbers increase repetition. After setting this parameter, if a word has already appeared in the text, the model will decrease the probability of choosing that word for subsequent generation." + zh_Hans: "用于调整自动生成文本中重复内容的频率。正数减少重复,负数增加重复。设置此参数后,如果一个词在文本中已经出现过,模型在后续生成中选择该词的概率会降低。" + + - name: user + use_template: text + label: + en_US: "User" + zh_Hans: "用户" + type: string + required: false + help: + en_US: "Used to track and differentiate conversation requests from different users." + zh_Hans: "用于追踪和区分不同用户的对话请求。" diff --git a/api/core/model_runtime/model_providers/gitee_ai/llm/_position.yaml b/api/core/model_runtime/model_providers/gitee_ai/llm/_position.yaml new file mode 100644 index 0000000000..c942cda3b2 --- /dev/null +++ b/api/core/model_runtime/model_providers/gitee_ai/llm/_position.yaml @@ -0,0 +1,10 @@ +- Qwen2.5-72B-Instruct +- Qwen2-7B-Instruct +- Qwen2-72B-Instruct +- Yi-1.5-34B-Chat +- glm-4-9b-chat +- deepseek-coder-33B-instruct-chat +- deepseek-coder-33B-instruct-completions +- codegeex4-all-9b +- InternVL2.5-26B +- InternVL2-8B diff --git a/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py b/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py new file mode 100644 index 0000000000..68aaad2e3f --- /dev/null +++ b/api/core/model_runtime/model_providers/gitee_ai/llm/llm.py @@ -0,0 +1,61 @@ +from collections.abc import Generator +from typing import Optional, Union + +from core.model_runtime.entities.llm_entities import LLMMode, LLMResult +from core.model_runtime.entities.message_entities import ( + PromptMessage, + PromptMessageTool, +) +from core.model_runtime.entities.model_entities import ModelFeature +from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel + + +class GiteeAILargeLanguageModel(OAIAPICompatLargeLanguageModel): + MODEL_TO_IDENTITY: dict[str, str] = { + "Yi-1.5-34B-Chat": "Yi-34B-Chat", + "deepseek-coder-33B-instruct-completions": "deepseek-coder-33B-instruct", + "deepseek-coder-33B-instruct-chat": "deepseek-coder-33B-instruct", + } + + def _invoke( + self, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + ) -> Union[LLMResult, Generator]: + self._add_custom_parameters(credentials, model, model_parameters) + return super()._invoke( + GiteeAILargeLanguageModel.MODEL_TO_IDENTITY.get(model, model), + credentials, + prompt_messages, + model_parameters, + tools, + stop, + stream, + user, + ) + + def validate_credentials(self, model: str, credentials: dict) -> None: + self._add_custom_parameters(credentials, model, None) + super().validate_credentials(GiteeAILargeLanguageModel.MODEL_TO_IDENTITY.get(model, model), credentials) + + def _add_custom_parameters(self, credentials: dict, model: Optional[str], model_parameters: dict) -> None: + if model is None: + model = "Qwen2-72B-Instruct" + + credentials["endpoint_url"] = "https://ai.gitee.com/v1" + if model.endswith("completions"): + credentials["mode"] = LLMMode.COMPLETION.value + else: + credentials["mode"] = LLMMode.CHAT.value + + schema = self.get_model_schema(model, credentials) + assert schema is not None, f"Model schema not found for model {model}" + assert schema.features is not None, f"Model features not found for model {model}" + if ModelFeature.TOOL_CALL in schema.features or ModelFeature.MULTI_TOOL_CALL in schema.features: + credentials["function_calling_type"] = "tool_call" diff --git a/api/poetry.lock b/api/poetry.lock index 2cdd07202c..3a0d5ea842 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -11,6 +11,17 @@ files = [ {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + [[package]] name = "aiohappyeyeballs" version = "2.4.4" @@ -955,6 +966,10 @@ files = [ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, @@ -967,8 +982,14 @@ files = [ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, @@ -979,8 +1000,24 @@ files = [ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, @@ -990,6 +1027,10 @@ files = [ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, @@ -1001,6 +1042,10 @@ files = [ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, @@ -1013,6 +1058,10 @@ files = [ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, @@ -1025,6 +1074,10 @@ files = [ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, @@ -7482,23 +7535,24 @@ image = ["Pillow (>=8.0.0)"] [[package]] name = "pypdfium2" -version = "4.17.0" +version = "4.30.0" description = "Python bindings to PDFium" optional = false python-versions = ">=3.6" files = [ - {file = "pypdfium2-4.17.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:e9ed42d5a5065ae41ae3ead3cd642e1f21b6039e69ccc204e260e218e91cd7e1"}, - {file = "pypdfium2-4.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0a3b5a8eca53a1e68434969821b70bd2bc9ac2b70e58daf516c6ff0b6b5779e7"}, - {file = "pypdfium2-4.17.0-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:854e04b51205466ec415b86588fe5dc593e9ca3e8e15b5aa05978c5352bd57d2"}, - {file = "pypdfium2-4.17.0-py3-none-manylinux_2_17_armv7l.whl", hash = "sha256:9ff8707b28568e9585bdf9a96b7a8a9f91c0b5ad05af119b49381dad89983364"}, - {file = "pypdfium2-4.17.0-py3-none-manylinux_2_17_i686.whl", hash = "sha256:09ecbef6212993db0b5460cfd46d6b157a921ff45c97b0764e6fe8ea2e8cdebf"}, - {file = "pypdfium2-4.17.0-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:f680e469b79c71c3fb086d7ced8361fbd66f4cd7b0ad08ff888289fe6743ab32"}, - {file = "pypdfium2-4.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ba7a7da48fbf0f1aaa903dac7d0e62186d6e8ae9a78b7b7b836d3f1b3d1be5d"}, - {file = "pypdfium2-4.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:451752170caf59d4b4572b527c2858dfff96eb1da35f2822c66cdce006dd4eae"}, - {file = "pypdfium2-4.17.0-py3-none-win32.whl", hash = "sha256:4930cfa793298214fa644c6986f6466e21f98eba3f338b4577614ebd8aa34af5"}, - {file = "pypdfium2-4.17.0-py3-none-win_amd64.whl", hash = "sha256:99de7f336e967dea4d324484f581fff55db1eb3c8e90baa845567dd9a3cc84f3"}, - {file = "pypdfium2-4.17.0-py3-none-win_arm64.whl", hash = "sha256:9381677b489c13d64ea4f8cbf6ebfc858216b052883e01e40fa993c2818a078e"}, - {file = "pypdfium2-4.17.0.tar.gz", hash = "sha256:2a2b3273c4614ee2004df60ace5f387645f843418ae29f379408ee11560241c0"}, + {file = "pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab"}, + {file = "pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de"}, + {file = "pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854"}, + {file = "pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2"}, + {file = "pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad"}, + {file = "pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f"}, + {file = "pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163"}, + {file = "pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e"}, + {file = "pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be"}, + {file = "pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e"}, + {file = "pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c"}, + {file = "pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29"}, + {file = "pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16"}, ] [[package]] @@ -11052,4 +11106,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "1aa6a44bc9270d50c9c0ea09f55a304b5148bf4dbbbb068ff1b1ea8da6fa60cc" +content-hash = "14476bf95504a4df4b8d5a5c6608c6aa3dae7499d27d1e41ef39d761cc7c693d" diff --git a/api/pyproject.toml b/api/pyproject.toml index a20c129e9c..da9eabecf5 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -66,7 +66,7 @@ pydantic = "~2.9.2" pydantic-settings = "~2.6.0" pydantic_extra_types = "~2.9.0" pyjwt = "~2.8.0" -pypdfium2 = "~4.17.0" +pypdfium2 = "~4.30.0" python = ">=3.11,<3.13" python-docx = "~1.1.0" python-dotenv = "1.0.0" diff --git a/api/services/account_service.py b/api/services/account_service.py index 57eabd5b6a..0b636ae779 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -440,7 +440,7 @@ class AccountService: if count is None: count = 0 count = int(count) + 1 - redis_client.setex(key, 60 * 60 * 24, count) + redis_client.setex(key, dify_config.LOGIN_LOCKOUT_DURATION, count) @staticmethod def is_login_error_rate_limit(email: str) -> bool: diff --git a/docker/.env.example b/docker/.env.example index db85e5d511..5ffab952c7 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -944,3 +944,6 @@ CSP_WHITELIST= # Enable or disable create tidb service job CREATE_TIDB_SERVICE_JOB_ENABLED=false + +# Maximum number of submitted thread count in a ThreadPool for parallel node execution +MAX_SUBMIT_COUNT=100 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 669f6eb4dd..65a337bc03 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -287,7 +287,10 @@ x-shared-env: &shared-api-worker-env OCEANBASE_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OCEANBASE_MEMORY_LIMIT: ${OCEANBASE_MEMORY_LIMIT:-6G} CREATE_TIDB_SERVICE_JOB_ENABLED: ${CREATE_TIDB_SERVICE_JOB_ENABLED:-false} + MAX_SUBMIT_COUNT: ${MAX_SUBMIT_COUNT:-100} RETRIEVAL_TOP_N: ${RETRIEVAL_TOP_N:-0} + HTTP_PROXY: ${HTTP_PROXY:-} + HTTPS_PROXY: ${HTTPS_PROXY:-} services: # API service diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx index e5d35059a6..1b327185e5 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx @@ -7,84 +7,36 @@ import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import { Cog8ToothIcon, - // CommandLineIcon, DocumentTextIcon, PaperClipIcon, - PuzzlePieceIcon, - QuestionMarkCircleIcon, - Squares2X2Icon, } from '@heroicons/react/24/outline' import { Cog8ToothIcon as Cog8ToothSolidIcon, // CommandLineIcon as CommandLineSolidIcon, DocumentTextIcon as DocumentTextSolidIcon, } from '@heroicons/react/24/solid' -import Link from 'next/link' +import { RiApps2AddLine, RiInformation2Line } from '@remixicon/react' import s from './style.module.css' import classNames from '@/utils/classnames' import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets' -import type { RelatedApp, RelatedAppResponse } from '@/models/datasets' +import type { RelatedAppResponse } from '@/models/datasets' import AppSideBar from '@/app/components/app-sidebar' -import Divider from '@/app/components/base/divider' -import AppIcon from '@/app/components/base/app-icon' import Loading from '@/app/components/base/loading' -import FloatPopoverContainer from '@/app/components/base/float-popover-container' import DatasetDetailContext from '@/context/dataset-detail' import { DataSourceType } from '@/models/datasets' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { LanguagesSupported } from '@/i18n/language' import { useStore } from '@/app/components/app/store' -import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication' -import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' import { getLocaleOnClient } from '@/i18n' import { useAppContext } from '@/context/app-context' +import Tooltip from '@/app/components/base/tooltip' +import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' export type IAppDetailLayoutProps = { children: React.ReactNode params: { datasetId: string } } -type ILikedItemProps = { - type?: 'plugin' | 'app' - appStatus?: boolean - detail: RelatedApp - isMobile: boolean -} - -const LikedItem = ({ - type = 'app', - detail, - isMobile, -}: ILikedItemProps) => { - return ( - -
- - {type === 'app' && ( - - {detail.mode === 'advanced-chat' && ( - - )} - {detail.mode === 'agent-chat' && ( - - )} - {detail.mode === 'chat' && ( - - )} - {detail.mode === 'completion' && ( - - )} - {detail.mode === 'workflow' && ( - - )} - - )} -
- {!isMobile &&
{detail?.name || '--'}
} - - ) -} - const TargetIcon = ({ className }: SVGProps) => { return @@ -116,65 +68,80 @@ const BookOpenIcon = ({ className }: SVGProps) => { type IExtraInfoProps = { isMobile: boolean relatedApps?: RelatedAppResponse + expand: boolean } -const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => { +const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { const locale = getLocaleOnClient() const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile) const { t } = useTranslation() + const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 + const relatedAppsTotal = relatedApps?.data?.length || 0 + useEffect(() => { setShowTips(!isMobile) }, [isMobile, setShowTips]) - return
- - {(relatedApps?.data && relatedApps?.data?.length > 0) && ( + return
+ {hasRelatedApps && ( <> - {!isMobile &&
{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}
} + {!isMobile && ( + + } + > +
+ {relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')} + +
+
+ )} + {isMobile &&
- {relatedApps?.total || '--'} + {relatedAppsTotal || '--'}
} - {relatedApps?.data?.map((item, index) => ())} )} - {!relatedApps?.data?.length && ( - - + {!hasRelatedApps && !expand && ( + +
+ +
+
{t('common.datasetMenus.emptyTip')}
+ + + {t('common.datasetMenus.viewDoc')} +
} > -
-
-
- -
-
- -
-
-
{t('common.datasetMenus.emptyTip')}
- - - {t('common.datasetMenus.viewDoc')} - +
+ {t('common.datasetMenus.noRelatedApp')} +
- + )}
} @@ -245,7 +212,7 @@ const DatasetDetailLayout: FC = (props) => { desc={datasetRes?.description || '--'} isExternal={datasetRes?.provider === 'external'} navigation={navigation} - extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => : undefined} + extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => : undefined} iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'} />} + + + +type Props = { + isExternal?: boolean + name: string + description: string + expand: boolean + extraInfo?: React.ReactNode +} + +const DatasetInfo: FC = ({ + name, + description, + isExternal, + expand, + extraInfo, +}) => { + const { t } = useTranslation() + return ( +
+
+ +
+ {expand && ( +
+
+ {name} +
+
{isExternal ? t('dataset.externalTag') : t('dataset.localDocs')}
+
{description}
+
+ )} + {extraInfo} +
+ ) +} +export default React.memo(DatasetInfo) diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index 5ee063ad64..dc5bf5b17a 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -4,12 +4,14 @@ import NavLink from './navLink' import type { NavIcon } from './navLink' import AppBasic from './basic' import AppInfo from './app-info' +import DatasetInfo from './dataset-info' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { AlignLeft01, AlignRight01, } from '@/app/components/base/icons/src/vender/line/layout' import { useStore as useAppStore } from '@/app/components/app/store' +import cn from '@/utils/classnames' export type IAppDetailNavProps = { iconType?: 'app' | 'dataset' | 'notion' @@ -63,7 +65,16 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati {iconType === 'app' && ( )} - {iconType !== 'app' && ( + {iconType === 'dataset' && ( + + )} + {!['app', 'dataset'].includes(iconType) && ( )}
- {!expand && ( -
- )} +
+
+
{ !isMobile && ( diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index c195b7253d..1938c42d3e 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import { init } from 'emoji-mart' import data from '@emoji-mart/data' -import Image from 'next/image' import { cva } from 'class-variance-authority' import type { AppIconType } from '@/types/app' import classNames from '@/utils/classnames' @@ -62,7 +61,8 @@ const AppIcon: FC = ({ onClick={onClick} > {isValidImageIcon - ? app icon + // eslint-disable-next-line @next/next/no-img-element + ? app icon : (innerIcon || ((icon && icon !== '') ? : )) } diff --git a/web/app/components/base/badge.tsx b/web/app/components/base/badge.tsx index fb1ceca1e6..7b5a0fc873 100644 --- a/web/app/components/base/badge.tsx +++ b/web/app/components/base/badge.tsx @@ -3,7 +3,8 @@ import cn from '@/utils/classnames' type BadgeProps = { className?: string - text: string | React.ReactNode + text?: string + children?: React.ReactNode uppercase?: boolean hasRedCornerMark?: boolean } @@ -11,6 +12,7 @@ type BadgeProps = { const Badge = ({ className, text, + children, uppercase = true, hasRedCornerMark, }: BadgeProps) => { @@ -22,7 +24,7 @@ const Badge = ({ className, )} > - {text} + {children || text} {hasRedCornerMark && (
diff --git a/web/app/components/base/checkbox/assets/mixed.svg b/web/app/components/base/checkbox/assets/mixed.svg new file mode 100644 index 0000000000..e16b8fc975 --- /dev/null +++ b/web/app/components/base/checkbox/assets/mixed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/checkbox/index.module.css b/web/app/components/base/checkbox/index.module.css new file mode 100644 index 0000000000..d675607b46 --- /dev/null +++ b/web/app/components/base/checkbox/index.module.css @@ -0,0 +1,10 @@ +.mixed { + background: var(--color-components-checkbox-bg) url(./assets/mixed.svg) center center no-repeat; + background-size: 12px 12px; + border: none; +} + +.checked.disabled { + background-color: #d0d5dd; + border-color: #d0d5dd; +} \ No newline at end of file diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx index c2b43ba4b2..df2c368cc0 100644 --- a/web/app/components/base/checkbox/index.tsx +++ b/web/app/components/base/checkbox/index.tsx @@ -1,4 +1,5 @@ import { RiCheckLine } from '@remixicon/react' +import s from './index.module.css' import cn from '@/utils/classnames' type CheckboxProps = { @@ -6,15 +7,17 @@ type CheckboxProps = { onCheck?: () => void className?: string disabled?: boolean + mixed?: boolean } -const Checkbox = ({ checked, onCheck, className, disabled }: CheckboxProps) => { +const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProps) => { if (!checked) { return (
{ diff --git a/web/app/components/base/divider/index.tsx b/web/app/components/base/divider/index.tsx index fcee10a9e2..6fe16b95a2 100644 --- a/web/app/components/base/divider/index.tsx +++ b/web/app/components/base/divider/index.tsx @@ -22,7 +22,7 @@ const dividerVariants = cva('', }, ) -type DividerProps = { +export type DividerProps = { className?: string style?: CSSProperties } & VariantProps diff --git a/web/app/components/base/divider/with-label.tsx b/web/app/components/base/divider/with-label.tsx new file mode 100644 index 0000000000..608bc79998 --- /dev/null +++ b/web/app/components/base/divider/with-label.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react' +import type { DividerProps } from '.' +import Divider from '.' +import classNames from '@/utils/classnames' + +export type DividerWithLabelProps = DividerProps & { + label: string +} + +export const DividerWithLabel: FC = (props) => { + const { label, className, ...rest } = props + return
+ + + {label} + + +
+} + +export default DividerWithLabel diff --git a/web/app/components/base/drawer/index.tsx b/web/app/components/base/drawer/index.tsx index c1d7565882..e34dc7697a 100644 --- a/web/app/components/base/drawer/index.tsx +++ b/web/app/components/base/drawer/index.tsx @@ -19,6 +19,7 @@ export type IDrawerProps = { onClose: () => void onCancel?: () => void onOk?: () => void + unmount?: boolean } export default function Drawer({ @@ -35,11 +36,12 @@ export default function Drawer({ onClose, onCancel, onOk, + unmount = false, }: IDrawerProps) { const { t } = useTranslation() return ( !clickOutsideNotOpen && onClose()} className="fixed z-30 inset-0 overflow-y-auto" diff --git a/web/app/components/base/file-uploader/file-type-icon.tsx b/web/app/components/base/file-uploader/file-type-icon.tsx index ed4cdde7e7..de9166d2ae 100644 --- a/web/app/components/base/file-uploader/file-type-icon.tsx +++ b/web/app/components/base/file-uploader/file-type-icon.tsx @@ -82,11 +82,8 @@ const FileTypeIcon = ({ size = 'sm', className, }: FileTypeIconProps) => { - const Icon = FILE_TYPE_ICON_MAP[type].component - const color = FILE_TYPE_ICON_MAP[type].color - - if (!Icon) - return null + const Icon = FILE_TYPE_ICON_MAP[type]?.component || FileAppearanceTypeEnum.document + const color = FILE_TYPE_ICON_MAP[type]?.color || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].color return } diff --git a/web/app/components/base/icons/assets/public/knowledge/chunk.svg b/web/app/components/base/icons/assets/public/knowledge/chunk.svg new file mode 100644 index 0000000000..1dc04943fc --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/chunk.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/app/components/base/icons/assets/public/knowledge/collapse.svg b/web/app/components/base/icons/assets/public/knowledge/collapse.svg new file mode 100644 index 0000000000..b54e046085 --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/collapse.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/app/components/base/icons/assets/public/knowledge/general-type.svg b/web/app/components/base/icons/assets/public/knowledge/general-type.svg new file mode 100644 index 0000000000..779df5f31c --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/general-type.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/public/knowledge/layout-right-2-line-mod.svg b/web/app/components/base/icons/assets/public/knowledge/layout-right-2-line-mod.svg new file mode 100644 index 0000000000..188f9b55a3 --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/layout-right-2-line-mod.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/public/knowledge/parent-child-type.svg b/web/app/components/base/icons/assets/public/knowledge/parent-child-type.svg new file mode 100644 index 0000000000..bc596b672a --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/parent-child-type.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/app/components/base/icons/assets/public/knowledge/selection-mod.svg b/web/app/components/base/icons/assets/public/knowledge/selection-mod.svg new file mode 100644 index 0000000000..ae3c9c5c75 --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/selection-mod.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/app/components/base/icons/src/public/knowledge/Chunk.json b/web/app/components/base/icons/src/public/knowledge/Chunk.json new file mode 100644 index 0000000000..469d85d1a7 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Chunk.json @@ -0,0 +1,116 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "10", + "height": "10", + "viewBox": "0 0 10 10", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M2.5 10H0V7.5H2.5V10Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M6.25 6.25H3.75V3.75H6.25V6.25Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M2.5 6.25H0V3.75H2.5V6.25Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_4", + "d": "M6.25 2.5H3.75V0H6.25V2.5Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_5", + "d": "M2.5 2.5H0V0H2.5V2.5Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_6", + "d": "M10 2.5H7.5V0H10V2.5Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_7", + "d": "M9.58342 7.91663H7.91675V9.58329H9.58342V7.91663Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_8", + "d": "M9.58342 4.16663H7.91675V5.83329H9.58342V4.16663Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_9", + "d": "M5.83341 7.91663H4.16675V9.58329H5.83341V7.91663Z", + "fill": "#676F83" + }, + "children": [] + } + ] + } + ] + }, + "name": "Chunk" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/Chunk.tsx b/web/app/components/base/icons/src/public/knowledge/Chunk.tsx new file mode 100644 index 0000000000..87ff635811 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Chunk.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Chunk.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Chunk' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/Collapse.json b/web/app/components/base/icons/src/public/knowledge/Collapse.json new file mode 100644 index 0000000000..66d457155d --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Collapse.json @@ -0,0 +1,62 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon L" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Vector" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M2.66602 11.3333H0.666016L3.33268 8.66667L5.99935 11.3333H3.99935L3.99935 14H2.66602L2.66602 11.3333Z", + "fill": "#354052" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M2.66602 4.66667L2.66602 2L3.99935 2L3.99935 4.66667L5.99935 4.66667L3.33268 7.33333L0.666016 4.66667L2.66602 4.66667Z", + "fill": "#354052" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M7.33268 2.66667H13.9993V4H7.33268V2.66667ZM7.33268 12H13.9993V13.3333H7.33268V12ZM5.99935 7.33333H13.9993V8.66667H5.99935V7.33333Z", + "fill": "#354052" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "Collapse" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/Collapse.tsx b/web/app/components/base/icons/src/public/knowledge/Collapse.tsx new file mode 100644 index 0000000000..48206c4d0c --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Collapse.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Collapse.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Collapse' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/GeneralType.json b/web/app/components/base/icons/src/public/knowledge/GeneralType.json new file mode 100644 index 0000000000..9a87d00a60 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/GeneralType.json @@ -0,0 +1,38 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "12", + "height": "12", + "viewBox": "0 0 12 12", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon L" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M6 0.5C6.27615 0.5 6.5 0.72386 6.5 1V1.52755C6.95855 1.57831 7.3967 1.69804 7.80355 1.87619L8.067 1.41997C8.20505 1.18083 8.51085 1.09889 8.75 1.23696C8.98915 1.37503 9.07105 1.68082 8.933 1.91998L8.6692 2.37685C9.033 2.64523 9.3548 2.96707 9.6232 3.33084L10.0801 3.06703C10.3193 2.92896 10.6251 3.0109 10.7632 3.25005C10.9012 3.4892 10.8193 3.79499 10.5801 3.93306L10.1238 4.19649C10.302 4.60333 10.4218 5.0415 10.4725 5.50005H11C11.2761 5.50005 11.5 5.7239 11.5 6.00005C11.5 6.2762 11.2761 6.50005 11 6.50005H10.4725C10.4218 6.9586 10.302 7.3968 10.1238 7.80365L10.5801 8.0671C10.8193 8.20515 10.9012 8.51095 10.7632 8.7501C10.6251 8.98925 10.3193 9.0712 10.0801 8.9331L9.6232 8.6693C9.3548 9.03305 9.03295 9.3549 8.6692 9.62325L8.933 10.0802C9.07105 10.3193 8.98915 10.6251 8.75 10.7632C8.51085 10.9012 8.20505 10.8193 8.067 10.5802L7.80355 10.1239C7.3967 10.3021 6.95855 10.4218 6.5 10.4726V11C6.5 11.2761 6.27615 11.5 6 11.5C5.72385 11.5 5.5 11.2761 5.5 11V10.4726C5.04145 10.4218 4.60328 10.3021 4.19644 10.1239L3.933 10.5802C3.79493 10.8194 3.48914 10.9013 3.24999 10.7633C3.01084 10.6252 2.92891 10.3194 3.06698 10.0802L3.3308 9.62325C2.96702 9.3549 2.64517 9.03305 2.37678 8.66925L1.91986 8.93305C1.68071 9.07115 1.37492 8.9892 1.23685 8.75005C1.09878 8.5109 1.18072 8.2051 1.41986 8.06705L1.87612 7.8036C1.69797 7.39675 1.57824 6.9586 1.52749 6.50005L0.999975 6.5C0.723835 6.5 0.499987 6.2761 0.5 6C0.500015 5.72385 0.72388 5.5 1.00003 5.5L1.5275 5.50005C1.57825 5.0415 1.69796 4.60335 1.87611 4.19652L1.41987 3.93312C1.18072 3.79504 1.09878 3.48925 1.23685 3.2501C1.37492 3.01095 1.68071 2.92901 1.91985 3.06709L2.37675 3.33086C2.64514 2.96708 2.967 2.64524 3.33078 2.37684L3.06698 1.91992C2.92891 1.68077 3.01084 1.37498 3.24999 1.23691C3.48914 1.09884 3.79493 1.18077 3.933 1.41992L4.19642 1.87619C4.60327 1.69803 5.04145 1.57831 5.5 1.52755V1C5.5 0.72386 5.72385 0.5 6 0.5ZM3.83484 3.24991C3.48643 3.52463 3.19141 3.86415 2.96808 4.25014C2.67048 4.7645 2.49999 5.3616 2.49999 6.00005C2.49999 6.6385 2.67048 7.2356 2.96809 7.75C3.19142 8.13595 3.48645 8.4755 3.83486 8.7502L4.8599 6.97475C4.63581 6.71285 4.49999 6.37245 4.49999 6.00005C4.49999 5.62765 4.63581 5.28725 4.8599 5.02535L3.83484 3.24991ZM5.7258 4.52514L4.70041 2.74911C5.10185 2.58847 5.5402 2.50005 6 2.50005C6.63845 2.50005 7.23555 2.67054 7.74995 2.96816C8.28125 3.27557 8.7245 3.71882 9.0319 4.25012C9.2503 4.62764 9.4003 5.04975 9.4646 5.50005H7.41465C7.2087 4.91745 6.6531 4.50005 6 4.50005C5.9065 4.50005 5.8148 4.50865 5.7258 4.52514ZM7.41465 6.50005C7.2087 7.08265 6.6531 7.50005 6 7.50005C5.9065 7.50005 5.8148 7.49145 5.7258 7.47495L4.70043 9.251C5.10185 9.41165 5.5402 9.50005 6 9.50005C6.63845 9.50005 7.23555 9.32955 7.7499 9.03195C8.2812 8.72455 8.72445 8.2813 9.03185 7.75C9.2503 7.3725 9.4003 6.95035 9.4646 6.50005H7.41465Z", + "fill": "#676F83" + }, + "children": [] + } + ] + } + ] + }, + "name": "GeneralType" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/GeneralType.tsx b/web/app/components/base/icons/src/public/knowledge/GeneralType.tsx new file mode 100644 index 0000000000..7fce78a61d --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/GeneralType.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './GeneralType.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'GeneralType' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/LayoutRight2LineMod.json b/web/app/components/base/icons/src/public/knowledge/LayoutRight2LineMod.json new file mode 100644 index 0000000000..26c5cf1d4f --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/LayoutRight2LineMod.json @@ -0,0 +1,36 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon L" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M14.0002 2C14.3684 2 14.6668 2.29848 14.6668 2.66667V13.3333C14.6668 13.7015 14.3684 14 14.0002 14H2.00016C1.63198 14 1.3335 13.7015 1.3335 13.3333V2.66667C1.3335 2.29848 1.63198 2 2.00016 2H14.0002ZM13.3335 3.33333H2.66683V12.6667H13.3335V3.33333ZM14.0002 2.66667V13.3333H10.0002V2.66667H14.0002Z", + "fill": "#354052" + }, + "children": [] + } + ] + } + ] + }, + "name": "LayoutRight2LineMod" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/LayoutRight2LineMod.tsx b/web/app/components/base/icons/src/public/knowledge/LayoutRight2LineMod.tsx new file mode 100644 index 0000000000..2189893eb3 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/LayoutRight2LineMod.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './LayoutRight2LineMod.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'LayoutRight2LineMod' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/ParentChildType.json b/web/app/components/base/icons/src/public/knowledge/ParentChildType.json new file mode 100644 index 0000000000..250da77fc8 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/ParentChildType.json @@ -0,0 +1,56 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "10", + "height": "11", + "viewBox": "0 0 10 11", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M2.70833 3.87501C3.51375 3.87501 4.16666 3.22209 4.16666 2.41668C4.16666 1.61126 3.51375 0.958344 2.70833 0.958344C1.90292 0.958344 1.25 1.61126 1.25 2.41668C1.25 3.22209 1.90292 3.87501 2.70833 3.87501Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M7.29158 3.87501C8.097 3.87501 8.74992 3.22209 8.74992 2.41668C8.74992 1.61126 8.097 0.958344 7.29158 0.958344C6.48617 0.958344 5.83325 1.61126 5.83325 2.41668C5.83325 3.22209 6.48617 3.87501 7.29158 3.87501Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M7.29167 4.70835C6.83771 4.70886 6.39118 4.82363 5.99324 5.04208C5.59529 5.26053 5.25874 5.57563 5.01459 5.95835C5.34482 5.9622 5.66011 6.09658 5.89159 6.33215C6.12306 6.56771 6.25191 6.8853 6.24998 7.21555C6.24805 7.5458 6.11551 7.86187 5.8813 8.09472C5.6471 8.32756 5.33026 8.45826 5 8.45826C4.66975 8.45826 4.35291 8.32756 4.1187 8.09472C3.8845 7.86187 3.75195 7.5458 3.75003 7.21555C3.7481 6.8853 3.87695 6.56771 4.10842 6.33215C4.3399 6.09658 4.65519 5.9622 4.98542 5.95835C4.67086 5.46415 4.20432 5.08546 3.656 4.87926C3.10767 4.67306 2.50721 4.6505 1.94496 4.81497C1.3827 4.97944 0.889064 5.32205 0.538306 5.79125C0.187547 6.26045 -0.00135882 6.83086 7.35834e-06 7.41668V10.125C7.35834e-06 10.2355 0.043906 10.3415 0.122046 10.4196C0.200186 10.4978 0.306167 10.5417 0.416674 10.5417H3.33334V9.50001L1.83334 8.37501C1.78957 8.34218 1.75269 8.30105 1.72481 8.25397C1.69693 8.20688 1.6786 8.15477 1.67086 8.1006C1.65523 7.99121 1.6837 7.88008 1.75001 7.79168C1.81631 7.70327 1.91502 7.64483 2.02441 7.6292C2.13381 7.61357 2.24493 7.64204 2.33334 7.70835L3.88875 8.87501H6.11125L7.66667 7.70835C7.75507 7.64204 7.8662 7.61357 7.97559 7.6292C8.08499 7.64483 8.1837 7.70327 8.25 7.79168C8.31631 7.88008 8.34478 7.99121 8.32915 8.1006C8.31352 8.21 8.25507 8.30871 8.16667 8.37501L6.66667 9.50001V10.5417H9.58333C9.69384 10.5417 9.79982 10.4978 9.87796 10.4196C9.9561 10.3415 10 10.2355 10 10.125V7.41668C9.99912 6.69866 9.71349 6.01029 9.20577 5.50257C8.69805 4.99485 8.00969 4.70923 7.29167 4.70835Z", + "fill": "#676F83" + }, + "children": [] + } + ] + } + ] + }, + "name": "ParentChildType" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/ParentChildType.tsx b/web/app/components/base/icons/src/public/knowledge/ParentChildType.tsx new file mode 100644 index 0000000000..2b13aa939c --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/ParentChildType.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './ParentChildType.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'ParentChildType' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/SelectionMod.json b/web/app/components/base/icons/src/public/knowledge/SelectionMod.json new file mode 100644 index 0000000000..ff8174a572 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/SelectionMod.json @@ -0,0 +1,116 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "10", + "height": "10", + "viewBox": "0 0 10 10", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Group" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "d": "M2.5 10H0V7.5H2.5V10Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_2", + "d": "M6.25 6.25H3.75V3.75H6.25V6.25Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_3", + "d": "M2.5 6.25H0V3.75H2.5V6.25Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_4", + "d": "M6.25 2.5H3.75V0H6.25V2.5Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_5", + "d": "M2.5 2.5H0V0H2.5V2.5Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_6", + "d": "M10 2.5H7.5V0H10V2.5Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_7", + "d": "M9.58332 7.91663H7.91666V9.58329H9.58332V7.91663Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_8", + "d": "M9.58332 4.16663H7.91666V5.83329H9.58332V4.16663Z", + "fill": "#676F83" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector_9", + "d": "M5.83332 7.91663H4.16666V9.58329H5.83332V7.91663Z", + "fill": "#676F83" + }, + "children": [] + } + ] + } + ] + }, + "name": "SelectionMod" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/SelectionMod.tsx b/web/app/components/base/icons/src/public/knowledge/SelectionMod.tsx new file mode 100644 index 0000000000..19125b7063 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/SelectionMod.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './SelectionMod.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'SelectionMod' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/index.ts b/web/app/components/base/icons/src/public/knowledge/index.ts new file mode 100644 index 0000000000..0af2cf341d --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/index.ts @@ -0,0 +1,6 @@ +export { default as Chunk } from './Chunk' +export { default as Collapse } from './Collapse' +export { default as GeneralType } from './GeneralType' +export { default as LayoutRight2LineMod } from './LayoutRight2LineMod' +export { default as ParentChildType } from './ParentChildType' +export { default as SelectionMod } from './SelectionMod' diff --git a/web/app/components/base/icons/src/vender/features/index.ts b/web/app/components/base/icons/src/vender/features/index.ts index f246732226..853cad8f9f 100644 --- a/web/app/components/base/icons/src/vender/features/index.ts +++ b/web/app/components/base/icons/src/vender/features/index.ts @@ -1,5 +1,6 @@ export { default as Citations } from './Citations' export { default as ContentModeration } from './ContentModeration' +export { default as Document } from './Document' export { default as FolderUpload } from './FolderUpload' export { default as LoveMessage } from './LoveMessage' export { default as MessageFast } from './MessageFast' @@ -7,4 +8,3 @@ export { default as Microphone01 } from './Microphone01' export { default as TextToAudio } from './TextToAudio' export { default as VirtualAssistant } from './VirtualAssistant' export { default as Vision } from './Vision' -export { default as Document } from './Document' diff --git a/web/app/components/base/input-number/index.tsx b/web/app/components/base/input-number/index.tsx new file mode 100644 index 0000000000..7492e0814c --- /dev/null +++ b/web/app/components/base/input-number/index.tsx @@ -0,0 +1,57 @@ +import type { FC, SetStateAction } from 'react' +import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react' +import Input, { type InputProps } from '../input' +import classNames from '@/utils/classnames' + +export type InputNumberProps = { + unit?: string + value: number + onChange: (value: number) => void + amount?: number + size?: 'sm' | 'md' +} & Omit + +export const InputNumber: FC = (props) => { + const { unit, className, onChange, amount = 1, value, size = 'md', max, min, ...rest } = props + const update = (input: SetStateAction) => { + const current = typeof input === 'function' ? input(value) : input as number + if (max && current >= (max as number)) + return + if (min && current <= (min as number)) + return + onChange(current) + } + const inc = () => update(val => val + amount) + const dec = () => update(val => val - amount) + return
+ { + const parsed = Number(e.target.value) + if (Number.isNaN(parsed)) + return + onChange(parsed) + }} + /> + {unit &&
{unit}
} +
+ + +
+
+} diff --git a/web/app/components/base/linked-apps-panel/index.tsx b/web/app/components/base/linked-apps-panel/index.tsx new file mode 100644 index 0000000000..4320cb0fc6 --- /dev/null +++ b/web/app/components/base/linked-apps-panel/index.tsx @@ -0,0 +1,62 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import Link from 'next/link' +import { useTranslation } from 'react-i18next' +import { RiArrowRightUpLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import AppIcon from '@/app/components/base/app-icon' +import type { RelatedApp } from '@/models/datasets' + +type ILikedItemProps = { + appStatus?: boolean + detail: RelatedApp + isMobile: boolean +} + +const appTypeMap = { + 'chat': 'Chatbot', + 'completion': 'Completion', + 'agent-chat': 'Agent', + 'advanced-chat': 'Chatflow', + 'workflow': 'Workflow', +} + +const LikedItem = ({ + detail, + isMobile, +}: ILikedItemProps) => { + return ( + +
+
+ +
+ {!isMobile &&
{detail?.name || '--'}
} +
+
{appTypeMap[detail.mode]}
+ + + ) +} + +type Props = { + relatedApps: RelatedApp[] + isMobile: boolean +} + +const LinkedAppsPanel: FC = ({ + relatedApps, + isMobile, +}) => { + const { t } = useTranslation() + return ( +
+
{relatedApps.length || '--'} {t('common.datasetMenus.relatedApp')}
+ {relatedApps.map((item, index) => ( + + ))} +
+ ) +} +export default React.memo(LinkedAppsPanel) diff --git a/web/app/components/base/pagination/index.tsx b/web/app/components/base/pagination/index.tsx index 2285db9230..ec67040c01 100644 --- a/web/app/components/base/pagination/index.tsx +++ b/web/app/components/base/pagination/index.tsx @@ -8,7 +8,7 @@ import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import cn from '@/utils/classnames' -type Props = { +export type Props = { className?: string current: number onChange: (cur: number) => void diff --git a/web/app/components/base/param-item/index.tsx b/web/app/components/base/param-item/index.tsx index 49acc81484..2252e99233 100644 --- a/web/app/components/base/param-item/index.tsx +++ b/web/app/components/base/param-item/index.tsx @@ -1,5 +1,6 @@ 'use client' import type { FC } from 'react' +import { InputNumber } from '../input-number' import Tooltip from '@/app/components/base/tooltip' import Slider from '@/app/components/base/slider' import Switch from '@/app/components/base/switch' @@ -23,39 +24,44 @@ type Props = { const ParamItem: FC = ({ className, id, name, noTooltip, tip, step = 0.1, min = 0, max, value, enable, onChange, hasSwitch, onSwitchChange }) => { return (
-
+
{hasSwitch && ( { onSwitchChange?.(id, val) }} /> )} - {name} + {name} {!noTooltip && ( {tip}
} /> )} -
-
-
-
- { - const value = parseFloat(e.target.value) - if (value < min || value > max) - return - - onChange(id, value) - }} /> +
+
+ { + onChange(id, value) + }} + className='w-[72px]' + />
-
+
= ({ onChosen = () => { }, chosenConfig, chosenConfigWrapClassName, + className, }) => { return (
-
-
+
+
{icon}
-
{title}
-
{description}
+
{title}
+
{description}
{!noRadio && ( -
+
= ({ )}
{((isChosen && chosenConfig) || noRadio) && ( -
- {chosenConfig} +
+
+
+ {chosenConfig} +
)}
diff --git a/web/app/components/base/retry-button/index.tsx b/web/app/components/base/retry-button/index.tsx deleted file mode 100644 index 689827af7b..0000000000 --- a/web/app/components/base/retry-button/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useEffect, useReducer } from 'react' -import { useTranslation } from 'react-i18next' -import useSWR from 'swr' -import s from './style.module.css' -import classNames from '@/utils/classnames' -import Divider from '@/app/components/base/divider' -import { getErrorDocs, retryErrorDocs } from '@/service/datasets' -import type { IndexingStatusResponse } from '@/models/datasets' - -const WarningIcon = () => - - - - -type Props = { - datasetId: string -} -type IIndexState = { - value: string -} -type ActionType = 'retry' | 'success' | 'error' - -type IAction = { - type: ActionType -} -const indexStateReducer = (state: IIndexState, action: IAction) => { - const actionMap = { - retry: 'retry', - success: 'success', - error: 'error', - } - - return { - ...state, - value: actionMap[action.type] || state.value, - } -} - -const RetryButton: FC = ({ datasetId }) => { - const { t } = useTranslation() - const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' }) - const { data: errorDocs } = useSWR({ datasetId }, getErrorDocs) - - const onRetryErrorDocs = async () => { - dispatch({ type: 'retry' }) - const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || [] - const res = await retryErrorDocs({ datasetId, document_ids }) - if (res.result === 'success') - dispatch({ type: 'success' }) - else - dispatch({ type: 'error' }) - } - - useEffect(() => { - if (errorDocs?.total === 0) - dispatch({ type: 'success' }) - else - dispatch({ type: 'error' }) - }, [errorDocs?.total]) - - if (indexState.value === 'success') - return null - - return ( -
- - - {errorDocs?.total} {t('dataset.docsFailedNotice')} - - - - {t('dataset.retry')} - -
- ) -} -export default RetryButton diff --git a/web/app/components/base/retry-button/style.module.css b/web/app/components/base/retry-button/style.module.css deleted file mode 100644 index 99a0947576..0000000000 --- a/web/app/components/base/retry-button/style.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.retryBtn { - @apply inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base; - @apply border-solid border border-gray-200 text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300; -} diff --git a/web/app/components/base/simple-pie-chart/index.tsx b/web/app/components/base/simple-pie-chart/index.tsx index 7de539cbb1..4b987ab42d 100644 --- a/web/app/components/base/simple-pie-chart/index.tsx +++ b/web/app/components/base/simple-pie-chart/index.tsx @@ -10,10 +10,11 @@ export type SimplePieChartProps = { fill?: string stroke?: string size?: number + animationDuration?: number className?: string } -const SimplePieChart = ({ percentage = 80, fill = '#fdb022', stroke = '#f79009', size = 12, className }: SimplePieChartProps) => { +const SimplePieChart = ({ percentage = 80, fill = '#fdb022', stroke = '#f79009', size = 12, animationDuration, className }: SimplePieChartProps) => { const option: EChartsOption = useMemo(() => ({ series: [ { @@ -34,7 +35,7 @@ const SimplePieChart = ({ percentage = 80, fill = '#fdb022', stroke = '#f79009', { type: 'pie', radius: '83%', - animationDuration: 600, + animationDuration: animationDuration ?? 600, data: [ { value: percentage, itemStyle: { color: fill } }, { value: 100 - percentage, itemStyle: { color: '#fff' } }, @@ -48,7 +49,7 @@ const SimplePieChart = ({ percentage = 80, fill = '#fdb022', stroke = '#f79009', cursor: 'default', }, ], - }), [stroke, fill, percentage]) + }), [stroke, fill, percentage, animationDuration]) return ( = (props) => { ) } -export const SkeletonPoint: FC = () => -
·
- +export const SkeletonPoint: FC = (props) => { + const { className, ...rest } = props + return ( +
·
+ ) +} /** Usage * * diff --git a/web/app/components/base/switch/index.tsx b/web/app/components/base/switch/index.tsx index f61c6f46ff..3c3a4e6ca3 100644 --- a/web/app/components/base/switch/index.tsx +++ b/web/app/components/base/switch/index.tsx @@ -11,7 +11,7 @@ type SwitchProps = { className?: string } -const Switch = ({ onChange, size = 'md', defaultValue = false, disabled = false, className }: SwitchProps) => { +const Switch = React.forwardRef(({ onChange, size = 'md', defaultValue = false, disabled = false, className }: SwitchProps, ref) => { const [enabled, setEnabled] = useState(defaultValue) useEffect(() => { setEnabled(defaultValue) @@ -38,6 +38,7 @@ const Switch = ({ onChange, size = 'md', defaultValue = false, disabled = false, } return ( { if (disabled) @@ -63,5 +64,8 @@ const Switch = ({ onChange, size = 'md', defaultValue = false, disabled = false, /> ) -} +}) + +Switch.displayName = 'Switch' + export default React.memo(Switch) diff --git a/web/app/components/base/tag-input/index.tsx b/web/app/components/base/tag-input/index.tsx index b26d0c6438..ec6c1cee34 100644 --- a/web/app/components/base/tag-input/index.tsx +++ b/web/app/components/base/tag-input/index.tsx @@ -3,8 +3,8 @@ import type { ChangeEvent, FC, KeyboardEvent } from 'react' import { } from 'use-context-selector' import { useTranslation } from 'react-i18next' import AutosizeInput from 'react-18-input-autosize' +import { RiAddLine, RiCloseLine } from '@remixicon/react' import cn from '@/utils/classnames' -import { X } from '@/app/components/base/icons/src/vender/line/general' import { useToastContext } from '@/app/components/base/toast' type TagInputProps = { @@ -75,14 +75,14 @@ const TagInput: FC = ({ (items || []).map((item, index) => (
+ className={cn('flex items-center mr-1 mt-1 pl-1.5 pr-1 py-1 system-xs-regular text-text-secondary border border-divider-deep bg-components-badge-white-to-dark rounded-md')} + > {item} { !disableRemove && ( - handleRemove(index)} - /> +
handleRemove(index)}> + +
) }
@@ -90,24 +90,27 @@ const TagInput: FC = ({ } { !disableAdd && ( - setFocused(true)} - onBlur={handleBlur} - value={value} - onChange={(e: ChangeEvent) => { - setValue(e.target.value) - }} - onKeyDown={handleKeyDown} - placeholder={t(placeholder || (isSpecialMode ? 'common.model.params.stop_sequencesPlaceholder' : 'datasetDocuments.segment.addKeyWord'))} - /> +
+ {!isSpecialMode && !focused && } + setFocused(true)} + onBlur={handleBlur} + value={value} + onChange={(e: ChangeEvent) => { + setValue(e.target.value) + }} + onKeyDown={handleKeyDown} + placeholder={t(placeholder || (isSpecialMode ? 'common.model.params.stop_sequencesPlaceholder' : 'datasetDocuments.segment.addKeyWord'))} + /> +
) }
diff --git a/web/app/components/base/toast/index.tsx b/web/app/components/base/toast/index.tsx index 9156595b1e..daad66074b 100644 --- a/web/app/components/base/toast/index.tsx +++ b/web/app/components/base/toast/index.tsx @@ -21,6 +21,7 @@ export type IToastProps = { children?: ReactNode onClose?: () => void className?: string + customComponent?: ReactNode } type IToastContext = { notify: (props: IToastProps) => void @@ -35,6 +36,7 @@ const Toast = ({ message, children, className, + customComponent, }: IToastProps) => { const { close } = useToastContext() // sometimes message is react node array. Not handle it. @@ -49,8 +51,7 @@ const Toast = ({ 'top-0', 'right-0', )}> - -
-
{message}
+
+
+
{message}
+ {customComponent} +
{children &&
{children}
@@ -119,7 +123,8 @@ Toast.notify = ({ message, duration, className, -}: Pick) => { + customComponent, +}: Pick) => { const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000 if (typeof window === 'object') { const holder = document.createElement('div') @@ -127,7 +132,7 @@ Toast.notify = ({ root.render( {}, + notify: () => { }, close: () => { if (holder) { root.unmount() @@ -135,7 +140,7 @@ Toast.notify = ({ } }, }}> - + , ) document.body.appendChild(holder) diff --git a/web/app/components/base/tooltip/index.tsx b/web/app/components/base/tooltip/index.tsx index 8ec3cd8c7a..65b5a99077 100644 --- a/web/app/components/base/tooltip/index.tsx +++ b/web/app/components/base/tooltip/index.tsx @@ -14,6 +14,7 @@ export type TooltipProps = { popupContent?: React.ReactNode children?: React.ReactNode popupClassName?: string + noDecoration?: boolean offset?: OffsetOptions needsDelay?: boolean asChild?: boolean @@ -27,6 +28,7 @@ const Tooltip: FC = ({ popupContent, children, popupClassName, + noDecoration, offset, asChild = true, needsDelay = false, @@ -96,7 +98,7 @@ const Tooltip: FC = ({ > {popupContent && (
triggerMethod === 'hover' && setHoverPopup()} diff --git a/web/app/components/billing/priority-label/index.tsx b/web/app/components/billing/priority-label/index.tsx index 36338cf4a8..6ecac4a79e 100644 --- a/web/app/components/billing/priority-label/index.tsx +++ b/web/app/components/billing/priority-label/index.tsx @@ -4,6 +4,7 @@ import { DocumentProcessingPriority, Plan, } from '../type' +import cn from '@/utils/classnames' import { useProviderContext } from '@/context/provider-context' import { ZapFast, @@ -11,7 +12,11 @@ import { } from '@/app/components/base/icons/src/vender/solid/general' import Tooltip from '@/app/components/base/tooltip' -const PriorityLabel = () => { +type PriorityLabelProps = { + className?: string +} + +const PriorityLabel = ({ className }: PriorityLabelProps) => { const { t } = useTranslation() const { plan } = useProviderContext() @@ -37,18 +42,18 @@ const PriorityLabel = () => { }
}> - + { plan.type === Plan.professional && ( - + ) } { (plan.type === Plan.team || plan.type === Plan.enterprise) && ( - + ) } {t(`billing.plansCommon.priority.${priority}`)} diff --git a/web/app/components/datasets/chunk.tsx b/web/app/components/datasets/chunk.tsx new file mode 100644 index 0000000000..2d0e45e2c0 --- /dev/null +++ b/web/app/components/datasets/chunk.tsx @@ -0,0 +1,54 @@ +import type { FC, PropsWithChildren } from 'react' +import { SelectionMod } from '../base/icons/src/public/knowledge' +import type { QA } from '@/models/datasets' + +export type ChunkLabelProps = { + label: string + characterCount: number +} + +export const ChunkLabel: FC = (props) => { + const { label, characterCount } = props + return
+ +

+ {label} + + + · + + + {`${characterCount} characters`} +

+
+} + +export type ChunkContainerProps = ChunkLabelProps & PropsWithChildren + +export const ChunkContainer: FC = (props) => { + const { label, characterCount, children } = props + return
+ +

+ {children} +

+
+} + +export type QAPreviewProps = { + qa: QA +} + +export const QAPreview: FC = (props) => { + const { qa } = props + return
+
+ +

{qa.question}

+
+
+ +

{qa.answer}

+
+
+} diff --git a/web/app/components/datasets/common/chunking-mode-label.tsx b/web/app/components/datasets/common/chunking-mode-label.tsx new file mode 100644 index 0000000000..7c6e924009 --- /dev/null +++ b/web/app/components/datasets/common/chunking-mode-label.tsx @@ -0,0 +1,29 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import Badge from '@/app/components/base/badge' +import { GeneralType, ParentChildType } from '@/app/components/base/icons/src/public/knowledge' + +type Props = { + isGeneralMode: boolean + isQAMode: boolean +} + +const ChunkingModeLabel: FC = ({ + isGeneralMode, + isQAMode, +}) => { + const { t } = useTranslation() + const TypeIcon = isGeneralMode ? GeneralType : ParentChildType + + return ( + +
+ + {isGeneralMode ? `${t('dataset.chunkingMode.general')}${isQAMode ? ' · QA' : ''}` : t('dataset.chunkingMode.parentChild')} +
+
+ ) +} +export default React.memo(ChunkingModeLabel) diff --git a/web/app/components/datasets/common/document-file-icon.tsx b/web/app/components/datasets/common/document-file-icon.tsx new file mode 100644 index 0000000000..5842cbbc7c --- /dev/null +++ b/web/app/components/datasets/common/document-file-icon.tsx @@ -0,0 +1,40 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import FileTypeIcon from '../../base/file-uploader/file-type-icon' +import type { FileAppearanceType } from '@/app/components/base/file-uploader/types' +import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' + +const extendToFileTypeMap: { [key: string]: FileAppearanceType } = { + pdf: FileAppearanceTypeEnum.pdf, + json: FileAppearanceTypeEnum.document, + html: FileAppearanceTypeEnum.document, + txt: FileAppearanceTypeEnum.document, + markdown: FileAppearanceTypeEnum.markdown, + md: FileAppearanceTypeEnum.markdown, + xlsx: FileAppearanceTypeEnum.excel, + xls: FileAppearanceTypeEnum.excel, + csv: FileAppearanceTypeEnum.excel, + doc: FileAppearanceTypeEnum.word, + docx: FileAppearanceTypeEnum.word, +} + +type Props = { + extension?: string + name?: string + size?: 'sm' | 'lg' | 'md' + className?: string +} + +const DocumentFileIcon: FC = ({ + extension, + name, + size = 'md', + className, +}) => { + const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase() + return ( + + ) +} +export default React.memo(DocumentFileIcon) diff --git a/web/app/components/datasets/common/document-picker/document-list.tsx b/web/app/components/datasets/common/document-picker/document-list.tsx new file mode 100644 index 0000000000..3e320d7507 --- /dev/null +++ b/web/app/components/datasets/common/document-picker/document-list.tsx @@ -0,0 +1,42 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import FileIcon from '../document-file-icon' +import cn from '@/utils/classnames' +import type { DocumentItem } from '@/models/datasets' + +type Props = { + className?: string + list: DocumentItem[] + onChange: (value: DocumentItem) => void +} + +const DocumentList: FC = ({ + className, + list, + onChange, +}) => { + const handleChange = useCallback((item: DocumentItem) => { + return () => onChange(item) + }, [onChange]) + + return ( +
+ {list.map((item) => { + const { id, name, extension } = item + return ( +
+ +
{name}
+
+ ) + })} +
+ ) +} + +export default React.memo(DocumentList) diff --git a/web/app/components/datasets/common/document-picker/index.tsx b/web/app/components/datasets/common/document-picker/index.tsx new file mode 100644 index 0000000000..2f816fe5ea --- /dev/null +++ b/web/app/components/datasets/common/document-picker/index.tsx @@ -0,0 +1,118 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import { useBoolean } from 'ahooks' +import { RiArrowDownSLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import FileIcon from '../document-file-icon' +import DocumentList from './document-list' +import type { DocumentItem, ParentMode, SimpleDocumentDetail } from '@/models/datasets' +import { ProcessMode } from '@/models/datasets' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import cn from '@/utils/classnames' +import SearchInput from '@/app/components/base/search-input' +import { GeneralType, ParentChildType } from '@/app/components/base/icons/src/public/knowledge' +import { useDocumentList } from '@/service/knowledge/use-document' +import Loading from '@/app/components/base/loading' + +type Props = { + datasetId: string + value: { + name?: string + extension?: string + processMode?: ProcessMode + parentMode?: ParentMode + } + onChange: (value: SimpleDocumentDetail) => void +} + +const DocumentPicker: FC = ({ + datasetId, + value, + onChange, +}) => { + const { t } = useTranslation() + const { + name, + extension, + processMode, + parentMode, + } = value + const [query, setQuery] = useState('') + + const { data } = useDocumentList({ + datasetId, + query: { + keyword: query, + page: 1, + limit: 20, + }, + }) + const documentsList = data?.data + const isParentChild = processMode === ProcessMode.parentChild + const TypeIcon = isParentChild ? ParentChildType : GeneralType + + const [open, { + set: setOpen, + toggle: togglePopup, + }] = useBoolean(false) + const ArrowIcon = RiArrowDownSLine + + const handleChange = useCallback(({ id }: DocumentItem) => { + onChange(documentsList?.find(item => item.id === id) as SimpleDocumentDetail) + setOpen(false) + }, [documentsList, onChange, setOpen]) + + return ( + + +
+ +
+
+ {name || '--'} + +
+
+ + + {isParentChild ? t('dataset.chunkingMode.parentChild') : t('dataset.chunkingMode.general')} + {isParentChild && ` · ${parentMode || '--'}`} + +
+
+
+
+ +
+ + {documentsList + ? ( + ({ + id: d.id, + name: d.name, + extension: d.data_source_detail_dict?.upload_file.extension || '', + }))} + onChange={handleChange} + /> + ) + : (
+ +
)} +
+ +
+
+ ) +} +export default React.memo(DocumentPicker) diff --git a/web/app/components/datasets/common/document-picker/preview-document-picker.tsx b/web/app/components/datasets/common/document-picker/preview-document-picker.tsx new file mode 100644 index 0000000000..2a35b75471 --- /dev/null +++ b/web/app/components/datasets/common/document-picker/preview-document-picker.tsx @@ -0,0 +1,82 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useBoolean } from 'ahooks' +import { RiArrowDownSLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import FileIcon from '../document-file-icon' +import DocumentList from './document-list' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import cn from '@/utils/classnames' +import Loading from '@/app/components/base/loading' +import type { DocumentItem } from '@/models/datasets' + +type Props = { + className?: string + value: DocumentItem + files: DocumentItem[] + onChange: (value: DocumentItem) => void +} + +const PreviewDocumentPicker: FC = ({ + className, + value, + files, + onChange, +}) => { + const { t } = useTranslation() + const { name, extension } = value + + const [open, { + set: setOpen, + toggle: togglePopup, + }] = useBoolean(false) + const ArrowIcon = RiArrowDownSLine + + const handleChange = useCallback((item: DocumentItem) => { + onChange(item) + setOpen(false) + }, [onChange, setOpen]) + + return ( + + +
+ +
+
+ {name || '--'} + +
+
+
+
+ +
+ {files?.length > 1 &&
{t('dataset.preprocessDocument', { num: files.length })}
} + {files?.length > 0 + ? ( + + ) + : (
+ +
)} +
+ +
+
+ ) +} +export default React.memo(PreviewDocumentPicker) diff --git a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx new file mode 100644 index 0000000000..b687c004e5 --- /dev/null +++ b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx @@ -0,0 +1,38 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import StatusWithAction from './status-with-action' +import { useAutoDisabledDocuments, useDocumentEnable, useInvalidDisabledDocument } from '@/service/knowledge/use-document' +import Toast from '@/app/components/base/toast' +type Props = { + datasetId: string +} + +const AutoDisabledDocument: FC = ({ + datasetId, +}) => { + const { t } = useTranslation() + const { data, isLoading } = useAutoDisabledDocuments(datasetId) + const invalidDisabledDocument = useInvalidDisabledDocument() + const documentIds = data?.document_ids + const hasDisabledDocument = documentIds && documentIds.length > 0 + const { mutateAsync: enableDocument } = useDocumentEnable() + const handleEnableDocuments = useCallback(async () => { + await enableDocument({ datasetId, documentIds }) + invalidDisabledDocument() + Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + }, []) + if (!hasDisabledDocument || isLoading) + return null + + return ( + + ) +} +export default React.memo(AutoDisabledDocument) diff --git a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx new file mode 100644 index 0000000000..37311768b9 --- /dev/null +++ b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx @@ -0,0 +1,69 @@ +'use client' +import type { FC } from 'react' +import React, { useEffect, useReducer } from 'react' +import { useTranslation } from 'react-i18next' +import useSWR from 'swr' +import StatusWithAction from './status-with-action' +import { getErrorDocs, retryErrorDocs } from '@/service/datasets' +import type { IndexingStatusResponse } from '@/models/datasets' + +type Props = { + datasetId: string +} +type IIndexState = { + value: string +} +type ActionType = 'retry' | 'success' | 'error' + +type IAction = { + type: ActionType +} +const indexStateReducer = (state: IIndexState, action: IAction) => { + const actionMap = { + retry: 'retry', + success: 'success', + error: 'error', + } + + return { + ...state, + value: actionMap[action.type] || state.value, + } +} + +const RetryButton: FC = ({ datasetId }) => { + const { t } = useTranslation() + const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' }) + const { data: errorDocs, isLoading } = useSWR({ datasetId }, getErrorDocs) + + const onRetryErrorDocs = async () => { + dispatch({ type: 'retry' }) + const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || [] + const res = await retryErrorDocs({ datasetId, document_ids }) + if (res.result === 'success') + dispatch({ type: 'success' }) + else + dispatch({ type: 'error' }) + } + + useEffect(() => { + if (errorDocs?.total === 0) + dispatch({ type: 'success' }) + else + dispatch({ type: 'error' }) + }, [errorDocs?.total]) + + if (isLoading || indexState.value === 'success') + return null + + return ( + { }} + /> + ) +} +export default RetryButton diff --git a/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx b/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx new file mode 100644 index 0000000000..a8da9bf6cc --- /dev/null +++ b/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx @@ -0,0 +1,65 @@ +'use client' +import { RiAlertFill, RiCheckboxCircleFill, RiErrorWarningFill, RiInformation2Fill } from '@remixicon/react' +import type { FC } from 'react' +import React from 'react' +import cn from '@/utils/classnames' +import Divider from '@/app/components/base/divider' + +type Status = 'success' | 'error' | 'warning' | 'info' +type Props = { + type?: Status + description: string + actionText: string + onAction: () => void + disabled?: boolean +} + +const IconMap = { + success: { + Icon: RiCheckboxCircleFill, + color: 'text-text-success', + }, + error: { + Icon: RiErrorWarningFill, + color: 'text-text-destructive', + }, + warning: { + Icon: RiAlertFill, + color: 'text-text-warning-secondary', + }, + info: { + Icon: RiInformation2Fill, + color: 'text-text-accent', + }, +} + +const getIcon = (type: Status) => { + return IconMap[type] +} + +const StatusAction: FC = ({ + type = 'info', + description, + actionText, + onAction, + disabled, +}) => { + const { Icon, color } = getIcon(type) + return ( +
+
+
+ +
{description}
+ +
{actionText}
+
+
+ ) +} +export default React.memo(StatusAction) diff --git a/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx b/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx index f3da67b92c..9236858ae4 100644 --- a/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx +++ b/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx @@ -2,10 +2,11 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' +import Image from 'next/image' import RetrievalParamConfig from '../retrieval-param-config' +import { OptionCard } from '../../create/step-two/option-card' +import { retrievalIcon } from '../../create/icons' import { RETRIEVE_METHOD } from '@/types/app' -import RadioCard from '@/app/components/base/radio-card' -import { HighPriority } from '@/app/components/base/icons/src/vender/solid/arrows' import type { RetrievalConfig } from '@/types/app' type Props = { @@ -21,19 +22,17 @@ const EconomicalRetrievalMethodConfig: FC = ({ return (
- } + } title={t('dataset.retrieval.invertedIndex.title')} - description={t('dataset.retrieval.invertedIndex.description')} - noRadio - chosenConfig={ - - } - /> + description={t('dataset.retrieval.invertedIndex.description')} isActive + activeHeaderClassName='bg-dataset-option-card-purple-gradient' + > + +
) } diff --git a/web/app/components/datasets/common/retrieval-method-config/index.tsx b/web/app/components/datasets/common/retrieval-method-config/index.tsx index 20d93568ad..3a60bd8818 100644 --- a/web/app/components/datasets/common/retrieval-method-config/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-config/index.tsx @@ -2,12 +2,13 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' +import Image from 'next/image' import RetrievalParamConfig from '../retrieval-param-config' +import { OptionCard } from '../../create/step-two/option-card' +import Effect from '../../create/assets/option-card-effect-purple.svg' +import { retrievalIcon } from '../../create/icons' import type { RetrievalConfig } from '@/types/app' import { RETRIEVE_METHOD } from '@/types/app' -import RadioCard from '@/app/components/base/radio-card' -import { PatternRecognition, Semantic } from '@/app/components/base/icons/src/vender/solid/development' -import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files' import { useProviderContext } from '@/context/provider-context' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -16,6 +17,7 @@ import { RerankingModeEnum, WeightedScoreEnum, } from '@/models/datasets' +import Badge from '@/app/components/base/badge' type Props = { value: RetrievalConfig @@ -56,67 +58,72 @@ const RetrievalMethodConfig: FC = ({ return (
{supportRetrievalMethods.includes(RETRIEVE_METHOD.semantic) && ( - } + } title={t('dataset.retrieval.semantic_search.title')} description={t('dataset.retrieval.semantic_search.description')} - isChosen={value.search_method === RETRIEVE_METHOD.semantic} - onChosen={() => onChange({ + isActive={ + value.search_method === RETRIEVE_METHOD.semantic + } + onSwitched={() => onChange({ ...value, search_method: RETRIEVE_METHOD.semantic, })} - chosenConfig={ - - } - /> + effectImg={Effect.src} + activeHeaderClassName='bg-dataset-option-card-purple-gradient' + > + + )} {supportRetrievalMethods.includes(RETRIEVE_METHOD.semantic) && ( - } + } title={t('dataset.retrieval.full_text_search.title')} description={t('dataset.retrieval.full_text_search.description')} - isChosen={value.search_method === RETRIEVE_METHOD.fullText} - onChosen={() => onChange({ + isActive={ + value.search_method === RETRIEVE_METHOD.fullText + } + onSwitched={() => onChange({ ...value, search_method: RETRIEVE_METHOD.fullText, })} - chosenConfig={ - - } - /> + effectImg={Effect.src} + activeHeaderClassName='bg-dataset-option-card-purple-gradient' + > + + )} {supportRetrievalMethods.includes(RETRIEVE_METHOD.semantic) && ( - } + } title={
{t('dataset.retrieval.hybrid_search.title')}
-
{t('dataset.retrieval.hybrid_search.recommend')}
+
} - description={t('dataset.retrieval.hybrid_search.description')} - isChosen={value.search_method === RETRIEVE_METHOD.hybrid} - onChosen={() => onChange({ + description={t('dataset.retrieval.hybrid_search.description')} isActive={ + value.search_method === RETRIEVE_METHOD.hybrid + } + onClick={() => onChange({ ...value, search_method: RETRIEVE_METHOD.hybrid, reranking_enable: true, })} - chosenConfig={ - - } - /> + effectImg={Effect.src} + activeHeaderClassName='bg-dataset-option-card-purple-gradient' + > + +
)}
) diff --git a/web/app/components/datasets/common/retrieval-method-info/index.tsx b/web/app/components/datasets/common/retrieval-method-info/index.tsx index 7d9b999c53..fc3020d4a9 100644 --- a/web/app/components/datasets/common/retrieval-method-info/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-info/index.tsx @@ -2,12 +2,11 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' +import Image from 'next/image' +import { retrievalIcon } from '../../create/icons' import type { RetrievalConfig } from '@/types/app' import { RETRIEVE_METHOD } from '@/types/app' import RadioCard from '@/app/components/base/radio-card' -import { HighPriority } from '@/app/components/base/icons/src/vender/solid/arrows' -import { PatternRecognition, Semantic } from '@/app/components/base/icons/src/vender/solid/development' -import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files' type Props = { value: RetrievalConfig @@ -15,11 +14,12 @@ type Props = { export const getIcon = (type: RETRIEVE_METHOD) => { return ({ - [RETRIEVE_METHOD.semantic]: Semantic, - [RETRIEVE_METHOD.fullText]: FileSearch02, - [RETRIEVE_METHOD.hybrid]: PatternRecognition, - [RETRIEVE_METHOD.invertedIndex]: HighPriority, - })[type] || FileSearch02 + [RETRIEVE_METHOD.semantic]: retrievalIcon.vector, + [RETRIEVE_METHOD.fullText]: retrievalIcon.fullText, + [RETRIEVE_METHOD.hybrid]: retrievalIcon.hybrid, + [RETRIEVE_METHOD.invertedIndex]: retrievalIcon.vector, + [RETRIEVE_METHOD.keywordSearch]: retrievalIcon.vector, + })[type] || retrievalIcon.vector } const EconomicalRetrievalMethodConfig: FC = ({ @@ -28,11 +28,11 @@ const EconomicalRetrievalMethodConfig: FC = ({ }) => { const { t } = useTranslation() const type = value.search_method - const Icon = getIcon(type) + const icon = return (
} + icon={icon} title={t(`dataset.retrieval.${type}.title`)} description={t(`dataset.retrieval.${type}.description`)} noRadio diff --git a/web/app/components/datasets/common/retrieval-param-config/index.tsx b/web/app/components/datasets/common/retrieval-param-config/index.tsx index 9d48d56a8d..5136ac1659 100644 --- a/web/app/components/datasets/common/retrieval-param-config/index.tsx +++ b/web/app/components/datasets/common/retrieval-param-config/index.tsx @@ -3,6 +3,9 @@ import type { FC } from 'react' import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import Image from 'next/image' +import ProgressIndicator from '../../create/assets/progress-indicator.svg' +import Reranking from '../../create/assets/rerank.svg' import cn from '@/utils/classnames' import TopKItem from '@/app/components/base/param-item/top-k-item' import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item' @@ -20,6 +23,7 @@ import { } from '@/models/datasets' import WeightedScore from '@/app/components/app/configuration/dataset-config/params-config/weighted-score' import Toast from '@/app/components/base/toast' +import RadioCard from '@/app/components/base/radio-card' type Props = { type: RETRIEVE_METHOD @@ -116,7 +120,7 @@ const RetrievalParamConfig: FC = ({
{!isEconomical && !isHybridSearch && (
-
+
{canToggleRerankModalEnable && (
= ({
)}
- {t('common.modelProvider.rerankModel.key')} + {t('common.modelProvider.rerankModel.key')} {t('common.modelProvider.rerankModel.tip')}
@@ -163,7 +167,7 @@ const RetrievalParamConfig: FC = ({ )} { !isHybridSearch && ( -
+
= ({ { isHybridSearch && ( <> -
+
{ rerankingModeOptions.map(option => ( -
handleChangeRerankMode(option.value)} - > -
{option.label}
- {option.tips}
} - triggerClassName='ml-0.5 w-3.5 h-3.5' - /> -
+ isChosen={value.reranking_mode === option.value} + onChosen={() => handleChangeRerankMode(option.value)} + icon={} + title={option.label} + description={option.tips} + className='flex-1' + /> )) }
diff --git a/web/app/components/datasets/create/assets/family-mod.svg b/web/app/components/datasets/create/assets/family-mod.svg new file mode 100644 index 0000000000..b1c4e6f566 --- /dev/null +++ b/web/app/components/datasets/create/assets/family-mod.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/assets/file-list-3-fill.svg b/web/app/components/datasets/create/assets/file-list-3-fill.svg new file mode 100644 index 0000000000..a4e6c4da97 --- /dev/null +++ b/web/app/components/datasets/create/assets/file-list-3-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/datasets/create/assets/gold.svg b/web/app/components/datasets/create/assets/gold.svg new file mode 100644 index 0000000000..b48ac0eae5 --- /dev/null +++ b/web/app/components/datasets/create/assets/gold.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/assets/note-mod.svg b/web/app/components/datasets/create/assets/note-mod.svg new file mode 100644 index 0000000000..b9e81f6bd5 --- /dev/null +++ b/web/app/components/datasets/create/assets/note-mod.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/datasets/create/assets/option-card-effect-blue.svg b/web/app/components/datasets/create/assets/option-card-effect-blue.svg new file mode 100644 index 0000000000..00a8afad8b --- /dev/null +++ b/web/app/components/datasets/create/assets/option-card-effect-blue.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web/app/components/datasets/create/assets/option-card-effect-orange.svg b/web/app/components/datasets/create/assets/option-card-effect-orange.svg new file mode 100644 index 0000000000..d833764f0c --- /dev/null +++ b/web/app/components/datasets/create/assets/option-card-effect-orange.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web/app/components/datasets/create/assets/option-card-effect-purple.svg b/web/app/components/datasets/create/assets/option-card-effect-purple.svg new file mode 100644 index 0000000000..a7857f8e57 --- /dev/null +++ b/web/app/components/datasets/create/assets/option-card-effect-purple.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web/app/components/datasets/create/assets/pattern-recognition-mod.svg b/web/app/components/datasets/create/assets/pattern-recognition-mod.svg new file mode 100644 index 0000000000..1083e888ed --- /dev/null +++ b/web/app/components/datasets/create/assets/pattern-recognition-mod.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/assets/piggy-bank-mod.svg b/web/app/components/datasets/create/assets/piggy-bank-mod.svg new file mode 100644 index 0000000000..b1120ad9a9 --- /dev/null +++ b/web/app/components/datasets/create/assets/piggy-bank-mod.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/assets/progress-indicator.svg b/web/app/components/datasets/create/assets/progress-indicator.svg new file mode 100644 index 0000000000..3c99713636 --- /dev/null +++ b/web/app/components/datasets/create/assets/progress-indicator.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/app/components/datasets/create/assets/rerank.svg b/web/app/components/datasets/create/assets/rerank.svg new file mode 100644 index 0000000000..409b52e6e2 --- /dev/null +++ b/web/app/components/datasets/create/assets/rerank.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/app/components/datasets/create/assets/research-mod.svg b/web/app/components/datasets/create/assets/research-mod.svg new file mode 100644 index 0000000000..1f0bb34233 --- /dev/null +++ b/web/app/components/datasets/create/assets/research-mod.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/assets/selection-mod.svg b/web/app/components/datasets/create/assets/selection-mod.svg new file mode 100644 index 0000000000..2d0dd3b5f7 --- /dev/null +++ b/web/app/components/datasets/create/assets/selection-mod.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/assets/setting-gear-mod.svg b/web/app/components/datasets/create/assets/setting-gear-mod.svg new file mode 100644 index 0000000000..c782caade8 --- /dev/null +++ b/web/app/components/datasets/create/assets/setting-gear-mod.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/app/components/datasets/create/embedding-process/index.module.css b/web/app/components/datasets/create/embedding-process/index.module.css index 1ebb006b54..f2ab4d85a2 100644 --- a/web/app/components/datasets/create/embedding-process/index.module.css +++ b/web/app/components/datasets/create/embedding-process/index.module.css @@ -14,24 +14,7 @@ border-radius: 6px; overflow: hidden; } -.sourceItem.error { - background: #FEE4E2; -} -.sourceItem.success { - background: #D1FADF; -} -.progressbar { - position: absolute; - top: 0; - left: 0; - height: 100%; - background-color: #B2CCFF; -} -.sourceItem .info { - display: flex; - align-items: center; - z-index: 1; -} + .sourceItem .info .name { font-weight: 500; font-size: 12px; @@ -55,13 +38,6 @@ color: #05603A; } - -.cost { - @apply flex justify-between items-center text-xs text-gray-700; -} -.embeddingStatus { - @apply flex items-center justify-between text-gray-900 font-medium text-sm mr-2; -} .commonIcon { @apply w-3 h-3 mr-1 inline-block align-middle; } @@ -81,35 +57,33 @@ @apply text-xs font-medium; } -.fileIcon { - @apply w-4 h-4 mr-1 bg-center bg-no-repeat; +.unknownFileIcon { background-image: url(../assets/unknown.svg); - background-size: 16px; } -.fileIcon.csv { +.csv { background-image: url(../assets/csv.svg); } -.fileIcon.docx { +.docx { background-image: url(../assets/docx.svg); } -.fileIcon.xlsx, -.fileIcon.xls { +.xlsx, +.xls { background-image: url(../assets/xlsx.svg); } -.fileIcon.pdf { +.pdf { background-image: url(../assets/pdf.svg); } -.fileIcon.html, -.fileIcon.htm { +.html, +.htm { background-image: url(../assets/html.svg); } -.fileIcon.md, -.fileIcon.markdown { +.md, +.markdown { background-image: url(../assets/md.svg); } -.fileIcon.txt { +.txt { background-image: url(../assets/txt.svg); } -.fileIcon.json { +.json { background-image: url(../assets/json.svg); } diff --git a/web/app/components/datasets/create/embedding-process/index.tsx b/web/app/components/datasets/create/embedding-process/index.tsx index 7786582085..98f0ebc6fd 100644 --- a/web/app/components/datasets/create/embedding-process/index.tsx +++ b/web/app/components/datasets/create/embedding-process/index.tsx @@ -6,9 +6,15 @@ import { useTranslation } from 'react-i18next' import { omit } from 'lodash-es' import { ArrowRightIcon } from '@heroicons/react/24/solid' import { + RiCheckboxCircleFill, RiErrorWarningFill, + RiLoader2Fill, + RiTerminalBoxLine, } from '@remixicon/react' -import s from './index.module.css' +import Image from 'next/image' +import { indexMethodIcon, retrievalIcon } from '../icons' +import { IndexingType } from '../step-two' +import DocumentFileIcon from '../../common/document-file-icon' import cn from '@/utils/classnames' import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata' import Button from '@/app/components/base/button' @@ -21,17 +27,23 @@ import { Plan } from '@/app/components/billing/type' import { ZapFast } from '@/app/components/base/icons/src/vender/solid/general' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import { useProviderContext } from '@/context/provider-context' -import Tooltip from '@/app/components/base/tooltip' import { sleep } from '@/utils' +import { RETRIEVE_METHOD } from '@/types/app' +import Tooltip from '@/app/components/base/tooltip' type Props = { datasetId: string batchId: string documents?: FullDocumentDetail[] indexingType?: string + retrievalMethod?: string } -const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) => { +const RuleDetail: FC<{ + sourceData?: ProcessRuleResponse + indexingType?: string + retrievalMethod?: string +}> = ({ sourceData, indexingType, retrievalMethod }) => { const { t } = useTranslation() const segmentationRuleMap = { @@ -73,7 +85,7 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) => return value }, [sourceData]) - return
+ return
{Object.keys(segmentationRuleMap).map((field) => { return = ({ sourceData }) => displayedValue={String(getValue(field))} /> })} + + } + /> + + } + />
} -const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], indexingType }) => { +const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], indexingType, retrievalMethod }) => { const { t } = useTranslation() const { enableBilling, plan } = useProviderContext() @@ -127,6 +172,7 @@ const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], index } useEffect(() => { + setIsStopQuery(false) startQueryStatus() return () => { stopQueryStatus() @@ -146,6 +192,9 @@ const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], index const navToDocumentList = () => { router.push(`/datasets/${datasetId}/documents`) } + const navToApiDocs = () => { + router.push('/datasets?category=api') + } const isEmbedding = useMemo(() => { return indexingStatusBatchDetail.some(indexingStatusDetail => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || '')) @@ -177,13 +226,17 @@ const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], index return doc?.data_source_info.notion_page_icon } - const isSourceEmbedding = (detail: IndexingStatusResponse) => ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes(detail.indexing_status || '') + const isSourceEmbedding = (detail: IndexingStatusResponse) => + ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes(detail.indexing_status || '') return ( <> -
-
- {isEmbedding && t('datasetDocuments.embedding.processing')} +
+
+ {isEmbedding &&
+ + {t('datasetDocuments.embedding.processing')} +
} {isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
@@ -200,69 +253,80 @@ const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], index
) } -
+
{indexingStatusBatchDetail.map(indexingStatusDetail => (
{isSourceEmbedding(indexingStatusDetail) && ( -
+
)} -
+
{getSourceType(indexingStatusDetail.id) === DataSourceType.FILE && ( -
+ //
+ )} {getSourceType(indexingStatusDetail.id) === DataSourceType.NOTION && ( )} -
{getSourceName(indexingStatusDetail.id)}
- { - enableBilling && ( - - ) - } -
-
+
+
+ {getSourceName(indexingStatusDetail.id)} +
+ { + enableBilling && ( + + ) + } +
{isSourceEmbedding(indexingStatusDetail) && ( -
{`${getSourcePercent(indexingStatusDetail)}%`}
+
{`${getSourcePercent(indexingStatusDetail)}%`}
)} - {indexingStatusDetail.indexing_status === 'error' && indexingStatusDetail.error && ( + {indexingStatusDetail.indexing_status === 'error' && ( - {indexingStatusDetail.error} -
- )} + popupClassName='px-4 py-[14px] max-w-60 text-sm leading-4 text-text-secondary border-[0.5px] border-components-panel-border rounded-xl' + offset={4} + popupContent={indexingStatusDetail.error} > -
- Error - -
+ + + )} - {indexingStatusDetail.indexing_status === 'error' && !indexingStatusDetail.error && ( -
- Error -
- )} {indexingStatusDetail.indexing_status === 'completed' && ( -
100%
+ )}
))}
- -
+
+ +
+
diff --git a/web/app/components/datasets/create/file-preview/index.module.css b/web/app/components/datasets/create/file-preview/index.module.css index d87522e6d0..468c93a7d7 100644 --- a/web/app/components/datasets/create/file-preview/index.module.css +++ b/web/app/components/datasets/create/file-preview/index.module.css @@ -1,6 +1,6 @@ .filePreview { @apply flex flex-col border-l border-gray-200 shrink-0; - width: 528px; + width: 100%; background-color: #fcfcfd; } diff --git a/web/app/components/datasets/create/file-preview/index.tsx b/web/app/components/datasets/create/file-preview/index.tsx index e20af64386..af9b63d824 100644 --- a/web/app/components/datasets/create/file-preview/index.tsx +++ b/web/app/components/datasets/create/file-preview/index.tsx @@ -44,7 +44,7 @@ const FilePreview = ({ }, [file]) return ( -
+
{t('datasetCreation.stepOne.filePreview')} diff --git a/web/app/components/datasets/create/file-uploader/index.module.css b/web/app/components/datasets/create/file-uploader/index.module.css index bf5b7dcaf5..7d29f2ef9c 100644 --- a/web/app/components/datasets/create/file-uploader/index.module.css +++ b/web/app/components/datasets/create/file-uploader/index.module.css @@ -1,68 +1,3 @@ -.fileUploader { - @apply mb-6; -} - -.fileUploader .title { - @apply mb-2; - font-weight: 500; - font-size: 16px; - line-height: 24px; - color: #344054; -} - -.fileUploader .tip { - font-weight: 400; - font-size: 12px; - line-height: 18px; - color: #667085; -} - -.uploader { - @apply relative box-border flex justify-center items-center mb-2 p-3; - flex-direction: column; - max-width: 640px; - min-height: 80px; - background: #F9FAFB; - border: 1px dashed #EAECF0; - border-radius: 12px; - font-weight: 400; - font-size: 14px; - line-height: 20px; - color: #667085; -} - -.uploader.dragging { - background: #F5F8FF; - border: 1px dashed #B2CCFF; -} - -.uploader .draggingCover { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.uploader .uploadIcon { - content: ''; - display: block; - margin-right: 8px; - width: 24px; - height: 24px; - background: center no-repeat url(../assets/upload-cloud-01.svg); - background-size: contain; -} - -.uploader .browse { - @apply pl-1 cursor-pointer; - color: #155eef; -} - -.fileList { - @apply space-y-2; -} - .file { @apply box-border relative flex items-center justify-between; padding: 8px 12px 8px 8px; @@ -193,4 +128,4 @@ .file:hover .actionWrapper .remove { display: block; -} \ No newline at end of file +} diff --git a/web/app/components/datasets/create/file-uploader/index.tsx b/web/app/components/datasets/create/file-uploader/index.tsx index adb4bed0d1..fe1bb73ad7 100644 --- a/web/app/components/datasets/create/file-uploader/index.tsx +++ b/web/app/components/datasets/create/file-uploader/index.tsx @@ -3,10 +3,12 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import useSWR from 'swr' -import s from './index.module.css' +import { RiDeleteBinLine, RiUploadCloud2Line } from '@remixicon/react' +import DocumentFileIcon from '../../common/document-file-icon' import cn from '@/utils/classnames' import type { CustomFile as File, FileItem } from '@/models/datasets' import { ToastContext } from '@/app/components/base/toast' +import SimplePieChart from '@/app/components/base/simple-pie-chart' import { upload } from '@/service/base' import { fetchFileUploadConfig } from '@/service/common' @@ -14,6 +16,8 @@ import { fetchSupportFileTypes } from '@/service/datasets' import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n/language' import { IS_CE_EDITION } from '@/config' +import { useAppContext } from '@/context/app-context' +import { Theme } from '@/types/app' const FILES_NUMBER_LIMIT = 20 @@ -222,6 +226,9 @@ const FileUploader = ({ initialUpload(files.filter(isValid)) }, [isValid, initialUpload]) + const { theme } = useAppContext() + const chartColor = useMemo(() => theme === Theme.dark ? '#5289ff' : '#296dff', [theme]) + useEffect(() => { dropRef.current?.addEventListener('dragenter', handleDragEnter) dropRef.current?.addEventListener('dragover', handleDragOver) @@ -236,12 +243,12 @@ const FileUploader = ({ }, [handleDrop]) return ( -
+
{!hideUpload && ( )} -
{t('datasetCreation.stepOne.uploader.title')}
- {!hideUpload && ( +
{t('datasetCreation.stepOne.uploader.title')}
+ + {!hideUpload && ( +
+
+ -
-
- {t('datasetCreation.stepOne.uploader.button')} - + {supportTypes.length > 0 && ( + + )}
-
{t('datasetCreation.stepOne.uploader.tip', { +
{t('datasetCreation.stepOne.uploader.tip', { size: fileUploadConfig.file_size_limit, supportTypes: supportTypesShowNames, })}
- {dragging &&
} + {dragging &&
}
)} -
+
+ {fileList.map((fileItem, index) => (
fileItem.file?.id && onPreview(fileItem.file)} className={cn( - s.file, - fileItem.progress < 100 && s.uploading, + 'flex items-center h-12 bg-components-panel-on-panel-item-bg text-xs leading-3 text-text-tertiary border border-components-panel-border rounded-lg shadow-xs', + // 'border-state-destructive-border bg-state-destructive-hover', )} > - {fileItem.progress < 100 && ( -
- )} -
-
-
{fileItem.file.name}
-
{getFileSize(fileItem.file.size)}
+
+
-
+
+
+
{fileItem.file.name}
+
+
+ {getFileType(fileItem.file)} + · + {getFileSize(fileItem.file.size)} + {/* · + 10k characters */} +
+
+
+ {/* + + */} {(fileItem.progress < 100 && fileItem.progress >= 0) && ( -
{`${fileItem.progress}%`}
- )} - {fileItem.progress === 100 && ( -
{ - e.stopPropagation() - removeFile(fileItem.fileID) - }} /> + //
{`${fileItem.progress}%`}
+ )} + { + e.stopPropagation() + removeFile(fileItem.fileID) + }}> + +
))} diff --git a/web/app/components/datasets/create/icons.ts b/web/app/components/datasets/create/icons.ts new file mode 100644 index 0000000000..80c4b6c944 --- /dev/null +++ b/web/app/components/datasets/create/icons.ts @@ -0,0 +1,16 @@ +import GoldIcon from './assets/gold.svg' +import Piggybank from './assets/piggy-bank-mod.svg' +import Selection from './assets/selection-mod.svg' +import Research from './assets/research-mod.svg' +import PatternRecognition from './assets/pattern-recognition-mod.svg' + +export const indexMethodIcon = { + high_quality: GoldIcon, + economical: Piggybank, +} + +export const retrievalIcon = { + vector: Selection, + fullText: Research, + hybrid: PatternRecognition, +} diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index 98098445c7..9d8d0b1945 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -3,10 +3,10 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import AppUnavailable from '../../base/app-unavailable' import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations' -import StepsNavBar from './steps-nav-bar' import StepOne from './step-one' import StepTwo from './step-two' import StepThree from './step-three' +import { Topbar } from './top-bar' import { DataSourceType } from '@/models/datasets' import type { CrawlOptions, CrawlResultItem, DataSet, FileItem, createDocumentResponse } from '@/models/datasets' import { fetchDataSource } from '@/service/common' @@ -36,6 +36,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { const [dataSourceType, setDataSourceType] = useState(DataSourceType.FILE) const [step, setStep] = useState(1) const [indexingTypeCache, setIndexTypeCache] = useState('') + const [retrievalMethodCache, setRetrievalMethodCache] = useState('') const [fileList, setFiles] = useState([]) const [result, setResult] = useState() const [hasError, setHasError] = useState(false) @@ -80,6 +81,9 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { const updateResultCache = (res?: createDocumentResponse) => { setResult(res) } + const updateRetrievalMethodCache = (method: string) => { + setRetrievalMethodCache(method) + } const nextStep = useCallback(() => { setStep(step + 1) @@ -118,33 +122,29 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { return return ( -
-
- -
-
-
- setShowAccountSettingModal({ payload: 'data-source' })} - datasetId={datasetId} - dataSourceType={dataSourceType} - dataSourceTypeDisable={!!detail?.data_source_type} - changeType={setDataSourceType} - files={fileList} - updateFile={updateFile} - updateFileList={updateFileList} - notionPages={notionPages} - updateNotionPages={updateNotionPages} - onStepChange={nextStep} - websitePages={websitePages} - updateWebsitePages={setWebsitePages} - onWebsiteCrawlProviderChange={setWebsiteCrawlProvider} - onWebsiteCrawlJobIdChange={setWebsiteCrawlJobId} - crawlOptions={crawlOptions} - onCrawlOptionsChange={setCrawlOptions} - /> -
+
+ +
+ {step === 1 && setShowAccountSettingModal({ payload: 'data-source' })} + datasetId={datasetId} + dataSourceType={dataSourceType} + dataSourceTypeDisable={!!detail?.data_source_type} + changeType={setDataSourceType} + files={fileList} + updateFile={updateFile} + updateFileList={updateFileList} + notionPages={notionPages} + updateNotionPages={updateNotionPages} + onStepChange={nextStep} + websitePages={websitePages} + updateWebsitePages={setWebsitePages} + onWebsiteCrawlProviderChange={setWebsiteCrawlProvider} + onWebsiteCrawlJobIdChange={setWebsiteCrawlJobId} + crawlOptions={crawlOptions} + onCrawlOptionsChange={setCrawlOptions} + />} {(step === 2 && (!datasetId || (datasetId && !!detail))) && setShowAccountSettingModal({ payload: 'provider' })} @@ -158,6 +158,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { websiteCrawlJobId={websiteCrawlJobId} onStepChange={changeStep} updateIndexingTypeCache={updateIndexingTypeCache} + updateRetrievalMethodCache={updateRetrievalMethodCache} updateResultCache={updateResultCache} crawlOptions={crawlOptions} />} @@ -165,6 +166,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { datasetId={datasetId} datasetName={detail?.name} indexingType={detail?.indexing_technique || indexingTypeCache} + retrievalMethod={detail?.retrieval_model?.search_method || retrievalMethodCache} creationCache={result} />}
diff --git a/web/app/components/datasets/create/notion-page-preview/index.tsx b/web/app/components/datasets/create/notion-page-preview/index.tsx index 8225e56f04..e13df46481 100644 --- a/web/app/components/datasets/create/notion-page-preview/index.tsx +++ b/web/app/components/datasets/create/notion-page-preview/index.tsx @@ -44,7 +44,7 @@ const NotionPagePreview = ({ }, [currentPage]) return ( -
+
{t('datasetCreation.stepOne.pagePreview')} diff --git a/web/app/components/datasets/create/step-one/index.module.css b/web/app/components/datasets/create/step-one/index.module.css index 4e3cf67cd6..811728315a 100644 --- a/web/app/components/datasets/create/step-one/index.module.css +++ b/web/app/components/datasets/create/step-one/index.module.css @@ -2,21 +2,19 @@ position: sticky; top: 0; left: 0; - padding: 42px 64px 12px; + padding: 42px 64px 12px 0; font-weight: 600; font-size: 18px; line-height: 28px; - color: #101828; } .form { position: relative; padding: 12px 64px; - background-color: #fff; } .dataSourceItem { - @apply box-border relative shrink-0 flex items-center mr-3 p-3 h-14 bg-white rounded-xl cursor-pointer; + @apply box-border relative grow shrink-0 flex items-center p-3 h-14 bg-white rounded-xl cursor-pointer; border: 0.5px solid #EAECF0; box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); font-weight: 500; @@ -24,27 +22,32 @@ line-height: 20px; color: #101828; } + .dataSourceItem:hover { background-color: #f5f8ff; border: 0.5px solid #B2CCFF; box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); } + .dataSourceItem.active { background-color: #f5f8ff; border: 1.5px solid #528BFF; box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06); } + .dataSourceItem.disabled { background-color: #f9fafb; border: 0.5px solid #EAECF0; box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); cursor: default; } + .dataSourceItem.disabled:hover { background-color: #f9fafb; border: 0.5px solid #EAECF0; box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); } + .comingTag { @apply flex justify-center items-center bg-white; position: absolute; @@ -59,6 +62,7 @@ line-height: 18px; color: #444CE7; } + .datasetIcon { @apply flex mr-2 w-8 h-8 rounded-lg bg-center bg-no-repeat; background-color: #F5FAFF; @@ -66,15 +70,18 @@ background-size: 16px; border: 0.5px solid #D1E9FF; } + .dataSourceItem:active .datasetIcon, .dataSourceItem:hover .datasetIcon { background-color: #F5F8FF; border: 0.5px solid #E0EAFF; } + .datasetIcon.notion { background-image: url(../assets/notion.svg); background-size: 20px; } + .datasetIcon.web { background-image: url(../assets/web.svg); } @@ -90,23 +97,6 @@ background-color: #eaecf0; } -.OtherCreationOption { - @apply flex items-center cursor-pointer; - font-weight: 500; - font-size: 13px; - line-height: 18px; - color: #155EEF; -} -.OtherCreationOption::before { - content: ''; - display: block; - margin-right: 4px; - width: 16px; - height: 16px; - background: center no-repeat url(../assets/folder-plus.svg); - background-size: contain; -} - .notionConnectionTip { display: flex; flex-direction: column; @@ -138,6 +128,7 @@ line-height: 24px; color: #374151; } + .notionConnectionTip .title::after { content: ''; position: absolute; @@ -148,6 +139,7 @@ background: center no-repeat url(../assets/Icon-3-dots.svg); background-size: contain; } + .notionConnectionTip .tip { margin-bottom: 20px; font-style: normal; @@ -155,4 +147,4 @@ font-size: 13px; line-height: 18px; color: #6B7280; -} +} \ No newline at end of file diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index 7450e7f618..5351e67269 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -1,6 +1,7 @@ 'use client' import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react' import FilePreview from '../file-preview' import FileUploader from '../file-uploader' import NotionPagePreview from '../notion-page-preview' @@ -17,6 +18,7 @@ import { NotionPageSelector } from '@/app/components/base/notion-page-selector' import { useDatasetDetailContext } from '@/context/dataset-detail' import { useProviderContext } from '@/context/provider-context' import VectorSpaceFull from '@/app/components/billing/vector-space-full' +import classNames from '@/utils/classnames' type IStepOneProps = { datasetId?: string @@ -120,143 +122,174 @@ const StepOne = ({ return true if (isShowVectorSpaceFull) return true - return false - }, [files]) + }, [files, isShowVectorSpaceFull]) + return (
-
- { - shouldShowDataSourceTypeList && ( -
{t('datasetCreation.steps.one')}
- ) - } -
- { - shouldShowDataSourceTypeList && ( -
-
{ - if (dataSourceTypeDisable) - return - changeType(DataSourceType.FILE) - hideFilePreview() - hideNotionPagePreview() - }} - > - - {t('datasetCreation.stepOne.dataSourceType.file')} -
-
{ - if (dataSourceTypeDisable) - return - changeType(DataSourceType.NOTION) - hideFilePreview() - hideNotionPagePreview() - }} - > - - {t('datasetCreation.stepOne.dataSourceType.notion')} -
-
changeType(DataSourceType.WEB)} - > - - {t('datasetCreation.stepOne.dataSourceType.web')} -
-
- ) - } - {dataSourceType === DataSourceType.FILE && ( - <> - - {isShowVectorSpaceFull && ( -
- -
- )} - - - )} - {dataSourceType === DataSourceType.NOTION && ( - <> - {!hasConnection && } - {hasConnection && ( - <> -
- page.page_id)} - onSelect={updateNotionPages} - onPreview={updateCurrentPage} - /> +
+
+
+ { + shouldShowDataSourceTypeList && ( +
{t('datasetCreation.steps.one')}
+ ) + } + { + shouldShowDataSourceTypeList && ( +
+
{ + if (dataSourceTypeDisable) + return + changeType(DataSourceType.FILE) + hideFilePreview() + hideNotionPagePreview() + }} + > + + {t('datasetCreation.stepOne.dataSourceType.file')} +
+
{ + if (dataSourceTypeDisable) + return + changeType(DataSourceType.NOTION) + hideFilePreview() + hideNotionPagePreview() + }} + > + + {t('datasetCreation.stepOne.dataSourceType.notion')} +
+
changeType(DataSourceType.WEB)} + > + + {t('datasetCreation.stepOne.dataSourceType.web')}
- {isShowVectorSpaceFull && ( -
- -
- )} - - - )} - - )} - {dataSourceType === DataSourceType.WEB && ( - <> -
- -
- {isShowVectorSpaceFull && ( -
-
- )} - - - )} - {!datasetId && ( - <> -
-
{t('datasetCreation.stepOne.emptyDatasetCreation')}
- - )} + ) + } + {dataSourceType === DataSourceType.FILE && ( + <> + + {isShowVectorSpaceFull && ( +
+ +
+ )} +
+ {/* */} + +
+ + )} + {dataSourceType === DataSourceType.NOTION && ( + <> + {!hasConnection && } + {hasConnection && ( + <> +
+ page.page_id)} + onSelect={updateNotionPages} + onPreview={updateCurrentPage} + /> +
+ {isShowVectorSpaceFull && ( +
+ +
+ )} +
+ {/* */} + +
+ + )} + + )} + {dataSourceType === DataSourceType.WEB && ( + <> +
+ +
+ {isShowVectorSpaceFull && ( +
+ +
+ )} +
+ {/* */} + +
+ + )} + {!datasetId && ( + <> +
+ + + {t('datasetCreation.stepOne.emptyDatasetCreation')} + + + )} +
+
-
- {currentFile && } - {currentNotionPage && } - {currentWebsite && } +
+ {currentFile && } + {currentNotionPage && } + {currentWebsite && } +
) } diff --git a/web/app/components/datasets/create/step-three/index.tsx b/web/app/components/datasets/create/step-three/index.tsx index 804a196ed5..9be35b2730 100644 --- a/web/app/components/datasets/create/step-three/index.tsx +++ b/web/app/components/datasets/create/step-three/index.tsx @@ -1,45 +1,51 @@ 'use client' import React from 'react' import { useTranslation } from 'react-i18next' +import { RiBookOpenLine } from '@remixicon/react' import EmbeddingProcess from '../embedding-process' -import s from './index.module.css' -import cn from '@/utils/classnames' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import type { FullDocumentDetail, createDocumentResponse } from '@/models/datasets' +import AppIcon from '@/app/components/base/app-icon' type StepThreeProps = { datasetId?: string datasetName?: string indexingType?: string + retrievalMethod?: string creationCache?: createDocumentResponse } -const StepThree = ({ datasetId, datasetName, indexingType, creationCache }: StepThreeProps) => { +const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrievalMethod }: StepThreeProps) => { const { t } = useTranslation() const media = useBreakpoints() const isMobile = media === MediaType.mobile return ( -
-
-
+
+
+
{!datasetId && ( <> -
-
{t('datasetCreation.stepThree.creationTitle')}
-
{t('datasetCreation.stepThree.creationContent')}
-
{t('datasetCreation.stepThree.label')}
-
{datasetName || creationCache?.dataset?.name}
+
+
{t('datasetCreation.stepThree.creationTitle')}
+
{t('datasetCreation.stepThree.creationContent')}
+
+ +
+
{t('datasetCreation.stepThree.label')}
+
{datasetName || creationCache?.dataset?.name}
+
+
-
+
)} {datasetId && ( -
-
{t('datasetCreation.stepThree.additionTitle')}
-
{`${t('datasetCreation.stepThree.additionP1')} ${datasetName || creationCache?.dataset?.name} ${t('datasetCreation.stepThree.additionP2')}`}
+
+
{t('datasetCreation.stepThree.additionTitle')}
+
{`${t('datasetCreation.stepThree.additionP1')} ${datasetName || creationCache?.dataset?.name} ${t('datasetCreation.stepThree.additionP2')}`}
)}
- {!isMobile &&
-
- -
{t('datasetCreation.stepThree.sideTipTitle')}
-
{t('datasetCreation.stepThree.sideTipContent')}
+ {!isMobile && ( +
+
+
+ +
+
{t('datasetCreation.stepThree.sideTipTitle')}
+
{t('datasetCreation.stepThree.sideTipContent')}
+
-
} + )}
) } diff --git a/web/app/components/datasets/create/step-two/index.module.css b/web/app/components/datasets/create/step-two/index.module.css index f89d6d67ea..178cbeba85 100644 --- a/web/app/components/datasets/create/step-two/index.module.css +++ b/web/app/components/datasets/create/step-two/index.module.css @@ -13,18 +13,6 @@ z-index: 10; } -.form { - @apply px-16 pb-8; -} - -.form .label { - @apply pt-6 pb-2 flex items-center; - font-weight: 500; - font-size: 16px; - line-height: 24px; - color: #344054; -} - .segmentationItem { min-height: 68px; } @@ -75,6 +63,10 @@ cursor: pointer; } +.disabled { + cursor: not-allowed !important; +} + .indexItem.disabled:hover { background-color: #fcfcfd; border-color: #f2f4f7; @@ -87,8 +79,7 @@ } .radioItem { - @apply relative mb-2 rounded-xl border border-gray-100 cursor-pointer; - background-color: #fcfcfd; + @apply relative mb-2 rounded-xl border border-components-option-card-option-border cursor-pointer bg-components-option-card-option-bg; } .radioItem.segmentationItem.custom { @@ -146,7 +137,7 @@ } .typeIcon.economical { - background-image: url(../assets/piggy-bank-01.svg); + background-image: url(../assets/piggy-bank-mod.svg); } .radioItem .radio { @@ -247,7 +238,7 @@ } .ruleItem { - @apply flex items-center; + @apply flex items-center py-1.5; } .formFooter { @@ -394,19 +385,6 @@ max-width: 524px; } -.previewHeader { - position: sticky; - top: 0; - left: 0; - padding-top: 42px; - background-color: #fff; - font-weight: 600; - font-size: 18px; - line-height: 28px; - color: #101828; - z-index: 10; -} - /* * `fixed` must under `previewHeader` because of style override would not work */ @@ -432,4 +410,4 @@ font-size: 12px; line-height: 18px; } -} \ No newline at end of file +} diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index af7ec2136f..302544e152 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -1,65 +1,79 @@ 'use client' -import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import type { FC, PropsWithChildren } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' -import { useBoolean } from 'ahooks' -import { XMarkIcon } from '@heroicons/react/20/solid' -import { RocketLaunchIcon } from '@heroicons/react/24/outline' import { - RiCloseLine, + RiAlertFill, + RiArrowLeftLine, + RiSearchEyeLine, } from '@remixicon/react' import Link from 'next/link' -import { groupBy } from 'lodash-es' -import PreviewItem, { PreviewType } from './preview-item' -import LanguageSelect from './language-select' +import Image from 'next/image' +import { useHover } from 'ahooks' +import SettingCog from '../assets/setting-gear-mod.svg' +import OrangeEffect from '../assets/option-card-effect-orange.svg' +import FamilyMod from '../assets/family-mod.svg' +import Note from '../assets/note-mod.svg' +import FileList from '../assets/file-list-3-fill.svg' +import { indexMethodIcon } from '../icons' +import { PreviewContainer } from '../../preview/container' +import { ChunkContainer, QAPreview } from '../../chunk' +import { PreviewHeader } from '../../preview/header' +import { FormattedText } from '../../formatted-text/formatted' +import { PreviewSlice } from '../../formatted-text/flavours/preview-slice' +import PreviewDocumentPicker from '../../common/document-picker/preview-document-picker' import s from './index.module.css' import unescape from './unescape' import escape from './escape' +import { OptionCard } from './option-card' +import LanguageSelect from './language-select' +import { DelimiterInput, MaxLengthInput, OverlapInput } from './inputs' import cn from '@/utils/classnames' -import type { CrawlOptions, CrawlResultItem, CreateDocumentReq, CustomFile, FileIndexingEstimateResponse, FullDocumentDetail, IndexingEstimateParams, NotionInfo, PreProcessingRule, ProcessRule, Rules, createDocumentResponse } from '@/models/datasets' -import { - createDocument, - createFirstDocument, - fetchFileIndexingEstimate as didFetchFileIndexingEstimate, - fetchDefaultProcessRule, -} from '@/service/datasets' +import type { CrawlOptions, CrawlResultItem, CreateDocumentReq, CustomFile, DocumentItem, FullDocumentDetail, ParentMode, PreProcessingRule, ProcessRule, Rules, createDocumentResponse } from '@/models/datasets' + import Button from '@/app/components/base/button' -import Input from '@/app/components/base/input' -import Loading from '@/app/components/base/loading' import FloatRightContainer from '@/app/components/base/float-right-container' import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config' import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config' import type { RetrievalConfig } from '@/types/app' import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import Toast from '@/app/components/base/toast' -import { formatNumber } from '@/utils/format' import type { NotionPage } from '@/models/common' import { DataSourceProvider } from '@/models/common' -import { DataSourceType, DocForm } from '@/models/datasets' -import NotionIcon from '@/app/components/base/notion-icon' -import Switch from '@/app/components/base/switch' -import { MessageChatSquare } from '@/app/components/base/icons/src/public/common' +import { ChuckingMode, DataSourceType } from '@/models/datasets' import { useDatasetDetailContext } from '@/context/dataset-detail' import I18n from '@/context/i18n' -import { IS_CE_EDITION } from '@/config' import { RETRIEVE_METHOD } from '@/types/app' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import Tooltip from '@/app/components/base/tooltip' import { useDefaultModel, useModelList, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { LanguagesSupported } from '@/i18n/language' import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTravel' +import Checkbox from '@/app/components/base/checkbox' +import RadioCard from '@/app/components/base/radio-card' +import { IS_CE_EDITION } from '@/config' +import Divider from '@/app/components/base/divider' +import { getNotionInfo, getWebsiteInfo, useCreateDocument, useCreateFirstDocument, useFetchDefaultProcessRule, useFetchFileIndexingEstimateForFile, useFetchFileIndexingEstimateForNotion, useFetchFileIndexingEstimateForWeb } from '@/service/knowledge/use-create-dataset' +import Badge from '@/app/components/base/badge' +import { SkeletonContanier, SkeletonPoint, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton' +import Tooltip from '@/app/components/base/tooltip' +import CustomDialog from '@/app/components/base/dialog' +import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' + +const TextLabel: FC = (props) => { + return +} -type ValueOf = T[keyof T] type StepTwoProps = { isSetting?: boolean documentDetail?: FullDocumentDetail isAPIKeySet: boolean onSetting: () => void datasetId?: string - indexingType?: ValueOf + indexingType?: IndexingType + retrievalMethod?: string dataSourceType: DataSourceType files: CustomFile[] notionPages?: NotionPage[] @@ -69,21 +83,48 @@ type StepTwoProps = { websiteCrawlJobId?: string onStepChange?: (delta: number) => void updateIndexingTypeCache?: (type: string) => void + updateRetrievalMethodCache?: (method: string) => void updateResultCache?: (res: createDocumentResponse) => void onSave?: () => void onCancel?: () => void } -enum SegmentType { +export enum SegmentType { AUTO = 'automatic', CUSTOM = 'custom', } -enum IndexingType { +export enum IndexingType { QUALIFIED = 'high_quality', ECONOMICAL = 'economy', } const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n' +const DEFAULT_MAXMIMUM_CHUNK_LENGTH = 500 +const DEFAULT_OVERLAP = 50 + +type ParentChildConfig = { + chunkForContext: ParentMode + parent: { + delimiter: string + maxLength: number + } + child: { + delimiter: string + maxLength: number + } +} + +const defaultParentChildConfig: ParentChildConfig = { + chunkForContext: 'paragraph', + parent: { + delimiter: '\\n\\n', + maxLength: 500, + }, + child: { + delimiter: '\\n', + maxLength: 200, + }, +} const StepTwo = ({ isSetting, @@ -104,6 +145,7 @@ const StepTwo = ({ updateResultCache, onSave, onCancel, + updateRetrievalMethodCache, }: StepTwoProps) => { const { t } = useTranslation() const { locale } = useContext(I18n) @@ -113,64 +155,157 @@ const StepTwo = ({ const { dataset: currentDataset, mutateDatasetRes } = useDatasetDetailContext() const isInCreatePage = !datasetId || (datasetId && !currentDataset?.data_source_type) const dataSourceType = isInCreatePage ? inCreatePageDataSourceType : currentDataset?.data_source_type - const scrollRef = useRef(null) - const [scrolled, setScrolled] = useState(false) - const previewScrollRef = useRef(null) - const [previewScrolled, setPreviewScrolled] = useState(false) - const [segmentationType, setSegmentationType] = useState(SegmentType.AUTO) + const [segmentationType, setSegmentationType] = useState(SegmentType.CUSTOM) const [segmentIdentifier, doSetSegmentIdentifier] = useState(DEFAULT_SEGMENT_IDENTIFIER) - const setSegmentIdentifier = useCallback((value: string) => { - doSetSegmentIdentifier(value ? escape(value) : DEFAULT_SEGMENT_IDENTIFIER) + const setSegmentIdentifier = useCallback((value: string, canEmpty?: boolean) => { + doSetSegmentIdentifier(value ? escape(value) : (canEmpty ? '' : DEFAULT_SEGMENT_IDENTIFIER)) }, []) - const [maxChunkLength, setMaxChunkLength] = useState(4000) // default chunk length + const [maxChunkLength, setMaxChunkLength] = useState(DEFAULT_MAXMIMUM_CHUNK_LENGTH) // default chunk length const [limitMaxChunkLength, setLimitMaxChunkLength] = useState(4000) - const [overlap, setOverlap] = useState(50) + const [overlap, setOverlap] = useState(DEFAULT_OVERLAP) const [rules, setRules] = useState([]) const [defaultConfig, setDefaultConfig] = useState() const hasSetIndexType = !!indexingType - const [indexType, setIndexType] = useState>( + const [indexType, setIndexType] = useState( (indexingType || isAPIKeySet) ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL, ) - const [isLanguageSelectDisabled, setIsLanguageSelectDisabled] = useState(false) - const [docForm, setDocForm] = useState( - (datasetId && documentDetail) ? documentDetail.doc_form : DocForm.TEXT, + + const [previewFile, setPreviewFile] = useState( + (datasetId && documentDetail) + ? documentDetail.file + : files[0], ) + const [previewNotionPage, setPreviewNotionPage] = useState( + (datasetId && documentDetail) + ? documentDetail.notion_page + : notionPages[0], + ) + + const [previewWebsitePage, setPreviewWebsitePage] = useState( + (datasetId && documentDetail) + ? documentDetail.website_page + : websitePages[0], + ) + + // QA Related + const [isLanguageSelectDisabled, _setIsLanguageSelectDisabled] = useState(false) + const [isQAConfirmDialogOpen, setIsQAConfirmDialogOpen] = useState(false) + const [docForm, setDocForm] = useState( + (datasetId && documentDetail) ? documentDetail.doc_form as ChuckingMode : ChuckingMode.text, + ) + const handleChangeDocform = (value: ChuckingMode) => { + if (value === ChuckingMode.qa && indexType === IndexingType.ECONOMICAL) { + setIsQAConfirmDialogOpen(true) + return + } + if (value === ChuckingMode.parentChild && indexType === IndexingType.ECONOMICAL) + setIndexType(IndexingType.QUALIFIED) + setDocForm(value) + // eslint-disable-next-line ts/no-use-before-define + currentEstimateMutation.reset() + } + const [docLanguage, setDocLanguage] = useState( (datasetId && documentDetail) ? documentDetail.doc_language : (locale !== LanguagesSupported[1] ? 'English' : 'Chinese'), ) - const [QATipHide, setQATipHide] = useState(false) - const [previewSwitched, setPreviewSwitched] = useState(false) - const [showPreview, { setTrue: setShowPreview, setFalse: hidePreview }] = useBoolean() - const [customFileIndexingEstimate, setCustomFileIndexingEstimate] = useState(null) - const [automaticFileIndexingEstimate, setAutomaticFileIndexingEstimate] = useState(null) - const fileIndexingEstimate = (() => { - return segmentationType === SegmentType.AUTO ? automaticFileIndexingEstimate : customFileIndexingEstimate - })() - const [isCreating, setIsCreating] = useState(false) + const [parentChildConfig, setParentChildConfig] = useState(defaultParentChildConfig) - const scrollHandle = (e: Event) => { - if ((e.target as HTMLDivElement).scrollTop > 0) - setScrolled(true) + const getIndexing_technique = () => indexingType || indexType - else - setScrolled(false) + const getProcessRule = (): ProcessRule => { + if (docForm === ChuckingMode.parentChild) { + return { + rules: { + pre_processing_rules: rules, + segmentation: { + separator: unescape( + parentChildConfig.parent.delimiter, + ), + max_tokens: parentChildConfig.parent.maxLength, + }, + parent_mode: parentChildConfig.chunkForContext, + subchunk_segmentation: { + separator: unescape(parentChildConfig.child.delimiter), + max_tokens: parentChildConfig.child.maxLength, + }, + }, // api will check this. It will be removed after api refactored. + mode: 'hierarchical', + } as ProcessRule + } + return { + rules: { + pre_processing_rules: rules, + segmentation: { + separator: unescape(segmentIdentifier), + max_tokens: maxChunkLength, + chunk_overlap: overlap, + }, + }, // api will check this. It will be removed after api refactored. + mode: segmentationType, + } as ProcessRule } - const previewScrollHandle = (e: Event) => { - if ((e.target as HTMLDivElement).scrollTop > 0) - setPreviewScrolled(true) + const fileIndexingEstimateQuery = useFetchFileIndexingEstimateForFile({ + docForm, + docLanguage, + dataSourceType: DataSourceType.FILE, + files: previewFile + ? [files.find(file => file.name === previewFile.name)!] + : files, + indexingTechnique: getIndexing_technique() as any, + processRule: getProcessRule(), + dataset_id: datasetId!, + }) + const notionIndexingEstimateQuery = useFetchFileIndexingEstimateForNotion({ + docForm, + docLanguage, + dataSourceType: DataSourceType.NOTION, + notionPages: [previewNotionPage], + indexingTechnique: getIndexing_technique() as any, + processRule: getProcessRule(), + dataset_id: datasetId || '', + }) - else - setPreviewScrolled(false) - } - const getFileName = (name: string) => { - const arr = name.split('.') - return arr.slice(0, -1).join('.') - } + const websiteIndexingEstimateQuery = useFetchFileIndexingEstimateForWeb({ + docForm, + docLanguage, + dataSourceType: DataSourceType.WEB, + websitePages: [previewWebsitePage], + crawlOptions, + websiteCrawlProvider, + websiteCrawlJobId, + indexingTechnique: getIndexing_technique() as any, + processRule: getProcessRule(), + dataset_id: datasetId || '', + }) + + const currentEstimateMutation = dataSourceType === DataSourceType.FILE + ? fileIndexingEstimateQuery + : dataSourceType === DataSourceType.NOTION + ? notionIndexingEstimateQuery + : websiteIndexingEstimateQuery + + const fetchEstimate = useCallback(() => { + if (dataSourceType === DataSourceType.FILE) + fileIndexingEstimateQuery.mutate() + + if (dataSourceType === DataSourceType.NOTION) + notionIndexingEstimateQuery.mutate() + + if (dataSourceType === DataSourceType.WEB) + websiteIndexingEstimateQuery.mutate() + }, [dataSourceType, fileIndexingEstimateQuery, notionIndexingEstimateQuery, websiteIndexingEstimateQuery]) + + const estimate + = dataSourceType === DataSourceType.FILE + ? fileIndexingEstimateQuery.data + : dataSourceType === DataSourceType.NOTION + ? notionIndexingEstimateQuery.data + : websiteIndexingEstimateQuery.data const getRuleName = (key: string) => { if (key === 'remove_extra_spaces') @@ -198,128 +333,20 @@ const StepTwo = ({ if (defaultConfig) { setSegmentIdentifier(defaultConfig.segmentation.separator) setMaxChunkLength(defaultConfig.segmentation.max_tokens) - setOverlap(defaultConfig.segmentation.chunk_overlap) + setOverlap(defaultConfig.segmentation.chunk_overlap!) setRules(defaultConfig.pre_processing_rules) } + setParentChildConfig(defaultParentChildConfig) } - const fetchFileIndexingEstimate = async (docForm = DocForm.TEXT, language?: string) => { - // eslint-disable-next-line ts/no-use-before-define - const res = await didFetchFileIndexingEstimate(getFileIndexingEstimateParams(docForm, language)!) - if (segmentationType === SegmentType.CUSTOM) - setCustomFileIndexingEstimate(res) - else - setAutomaticFileIndexingEstimate(res) - } - - const confirmChangeCustomConfig = () => { - if (segmentationType === SegmentType.CUSTOM && maxChunkLength > limitMaxChunkLength) { - Toast.notify({ type: 'error', message: t('datasetCreation.stepTwo.maxLengthCheck', { limit: limitMaxChunkLength }) }) + const updatePreview = () => { + if (segmentationType === SegmentType.CUSTOM && maxChunkLength > 4000) { + Toast.notify({ type: 'error', message: t('datasetCreation.stepTwo.maxLengthCheck') }) return } - setCustomFileIndexingEstimate(null) - setShowPreview() - fetchFileIndexingEstimate() - setPreviewSwitched(false) + fetchEstimate() } - const getIndexing_technique = () => indexingType || indexType - - const getProcessRule = () => { - const processRule: ProcessRule = { - rules: {} as any, // api will check this. It will be removed after api refactored. - mode: segmentationType, - } - if (segmentationType === SegmentType.CUSTOM) { - const ruleObj = { - pre_processing_rules: rules, - segmentation: { - separator: unescape(segmentIdentifier), - max_tokens: maxChunkLength, - chunk_overlap: overlap, - }, - } - processRule.rules = ruleObj - } - return processRule - } - - const getNotionInfo = () => { - const workspacesMap = groupBy(notionPages, 'workspace_id') - const workspaces = Object.keys(workspacesMap).map((workspaceId) => { - return { - workspaceId, - pages: workspacesMap[workspaceId], - } - }) - return workspaces.map((workspace) => { - return { - workspace_id: workspace.workspaceId, - pages: workspace.pages.map((page) => { - const { page_id, page_name, page_icon, type } = page - return { - page_id, - page_name, - page_icon, - type, - } - }), - } - }) as NotionInfo[] - } - - const getWebsiteInfo = () => { - return { - provider: websiteCrawlProvider, - job_id: websiteCrawlJobId, - urls: websitePages.map(page => page.source_url), - only_main_content: crawlOptions?.only_main_content, - } - } - - const getFileIndexingEstimateParams = (docForm: DocForm, language?: string): IndexingEstimateParams | undefined => { - if (dataSourceType === DataSourceType.FILE) { - return { - info_list: { - data_source_type: dataSourceType, - file_info_list: { - file_ids: files.map(file => file.id) as string[], - }, - }, - indexing_technique: getIndexing_technique() as string, - process_rule: getProcessRule(), - doc_form: docForm, - doc_language: language || docLanguage, - dataset_id: datasetId as string, - } - } - if (dataSourceType === DataSourceType.NOTION) { - return { - info_list: { - data_source_type: dataSourceType, - notion_info_list: getNotionInfo(), - }, - indexing_technique: getIndexing_technique() as string, - process_rule: getProcessRule(), - doc_form: docForm, - doc_language: language || docLanguage, - dataset_id: datasetId as string, - } - } - if (dataSourceType === DataSourceType.WEB) { - return { - info_list: { - data_source_type: dataSourceType, - website_info_list: getWebsiteInfo(), - }, - indexing_technique: getIndexing_technique() as string, - process_rule: getProcessRule(), - doc_form: docForm, - doc_language: language || docLanguage, - dataset_id: datasetId as string, - } - } - } const { modelList: rerankModelList, defaultModel: rerankDefaultModel, @@ -403,29 +430,36 @@ const StepTwo = ({ } } if (dataSourceType === DataSourceType.NOTION) - params.data_source.info_list.notion_info_list = getNotionInfo() + params.data_source.info_list.notion_info_list = getNotionInfo(notionPages) - if (dataSourceType === DataSourceType.WEB) - params.data_source.info_list.website_info_list = getWebsiteInfo() + if (dataSourceType === DataSourceType.WEB) { + params.data_source.info_list.website_info_list = getWebsiteInfo({ + websiteCrawlProvider, + websiteCrawlJobId, + websitePages, + }) + } } return params } - const getRules = async () => { - try { - const res = await fetchDefaultProcessRule({ url: '/datasets/process-rule' }) - const separator = res.rules.segmentation.separator + const fetchDefaultProcessRuleMutation = useFetchDefaultProcessRule({ + onSuccess(data) { + const separator = data.rules.segmentation.separator setSegmentIdentifier(separator) - setMaxChunkLength(res.rules.segmentation.max_tokens) - setLimitMaxChunkLength(res.limits.indexing_max_segmentation_tokens_length) - setOverlap(res.rules.segmentation.chunk_overlap) - setRules(res.rules.pre_processing_rules) - setDefaultConfig(res.rules) - } - catch (err) { - console.log(err) - } - } + setMaxChunkLength(data.rules.segmentation.max_tokens) + setOverlap(data.rules.segmentation.chunk_overlap!) + setRules(data.rules.pre_processing_rules) + setDefaultConfig(data.rules) + setLimitMaxChunkLength(data.limits.indexing_max_segmentation_tokens_length) + }, + onError(error) { + Toast.notify({ + type: 'error', + message: `${error}`, + }) + }, + }) const getRulesFromDetail = () => { if (documentDetail) { @@ -435,7 +469,7 @@ const StepTwo = ({ const overlap = rules.segmentation.chunk_overlap setSegmentIdentifier(separator) setMaxChunkLength(max) - setOverlap(overlap) + setOverlap(overlap!) setRules(rules.pre_processing_rules) setDefaultConfig(rules) } @@ -443,119 +477,81 @@ const StepTwo = ({ const getDefaultMode = () => { if (documentDetail) + // @ts-expect-error fix after api refactored setSegmentationType(documentDetail.dataset_process_rule.mode) } - const createHandle = async () => { - if (isCreating) - return - setIsCreating(true) - try { - let res - const params = getCreationParams() - if (!params) - return false - - setIsCreating(true) - if (!datasetId) { - res = await createFirstDocument({ - body: params as CreateDocumentReq, - }) - updateIndexingTypeCache && updateIndexingTypeCache(indexType as string) - updateResultCache && updateResultCache(res) - } - else { - res = await createDocument({ - datasetId, - body: params as CreateDocumentReq, - }) - updateIndexingTypeCache && updateIndexingTypeCache(indexType as string) - updateResultCache && updateResultCache(res) - } - if (mutateDatasetRes) - mutateDatasetRes() - onStepChange && onStepChange(+1) - isSetting && onSave && onSave() - } - catch (err) { + const createFirstDocumentMutation = useCreateFirstDocument({ + onError(error) { Toast.notify({ type: 'error', - message: `${err}`, + message: `${error}`, + }) + }, + }) + const createDocumentMutation = useCreateDocument(datasetId!, { + onError(error) { + Toast.notify({ + type: 'error', + message: `${error}`, + }) + }, + }) + + const isCreating = createFirstDocumentMutation.isPending || createDocumentMutation.isPending + + const createHandle = async () => { + const params = getCreationParams() + if (!params) + return false + + if (!datasetId) { + await createFirstDocumentMutation.mutateAsync( + params, + { + onSuccess(data) { + updateIndexingTypeCache && updateIndexingTypeCache(indexType as string) + updateResultCache && updateResultCache(data) + // eslint-disable-next-line ts/no-use-before-define + updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string) + }, + }, + ) + } + else { + await createDocumentMutation.mutateAsync(params, { + onSuccess(data) { + updateIndexingTypeCache && updateIndexingTypeCache(indexType as string) + updateResultCache && updateResultCache(data) + }, }) } - finally { - setIsCreating(false) - } - } - - const handleSwitch = (state: boolean) => { - if (state) - setDocForm(DocForm.QA) - else - setDocForm(DocForm.TEXT) - } - - const previewSwitch = async (language?: string) => { - setPreviewSwitched(true) - setIsLanguageSelectDisabled(true) - if (segmentationType === SegmentType.AUTO) - setAutomaticFileIndexingEstimate(null) - else - setCustomFileIndexingEstimate(null) - try { - await fetchFileIndexingEstimate(DocForm.QA, language) - } - finally { - setIsLanguageSelectDisabled(false) - } - } - - const handleSelect = (language: string) => { - setDocLanguage(language) - // Switch language, re-cutter - if (docForm === DocForm.QA && previewSwitched) - previewSwitch(language) + if (mutateDatasetRes) + mutateDatasetRes() + onStepChange && onStepChange(+1) + isSetting && onSave && onSave() } const changeToEconomicalType = () => { - if (!hasSetIndexType) { + if (docForm !== ChuckingMode.text) + return + + if (!hasSetIndexType) setIndexType(IndexingType.ECONOMICAL) - setDocForm(DocForm.TEXT) - } } useEffect(() => { // fetch rules if (!isSetting) { - getRules() + fetchDefaultProcessRuleMutation.mutate('/datasets/process-rule') } else { getRulesFromDetail() getDefaultMode() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - useEffect(() => { - scrollRef.current?.addEventListener('scroll', scrollHandle) - return () => { - scrollRef.current?.removeEventListener('scroll', scrollHandle) - } - }, []) - - useLayoutEffect(() => { - if (showPreview) { - previewScrollRef.current?.addEventListener('scroll', previewScrollHandle) - return () => { - previewScrollRef.current?.removeEventListener('scroll', previewScrollHandle) - } - } - }, [showPreview]) - - useEffect(() => { - if (indexingType === IndexingType.ECONOMICAL && docForm === DocForm.QA) - setDocForm(DocForm.TEXT) - }, [indexingType, docForm]) - useEffect(() => { // get indexing type by props if (indexingType) @@ -565,20 +561,6 @@ const StepTwo = ({ setIndexType(isAPIKeySet ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL) }, [isAPIKeySet, indexingType, datasetId]) - useEffect(() => { - if (segmentationType === SegmentType.AUTO) { - setAutomaticFileIndexingEstimate(null) - !isMobile && setShowPreview() - fetchFileIndexingEstimate() - setPreviewSwitched(false) - } - else { - hidePreview() - setCustomFileIndexingEstimate(null) - setPreviewSwitched(false) - } - }, [segmentationType, indexType]) - const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict || { search_method: RETRIEVE_METHOD.semantic, reranking_enable: false, @@ -591,433 +573,558 @@ const StepTwo = ({ score_threshold: 0.5, } as RetrievalConfig) + const economyDomRef = useRef(null) + const isHoveringEconomy = useHover(economyDomRef) + return (
-
-
- {t('datasetCreation.steps.two')} - {(isMobile || !showPreview) && ( - - )} -
-
-
{t('datasetCreation.stepTwo.segmentation')}
-
-
setSegmentationType(SegmentType.AUTO)} - > - - -
-
{t('datasetCreation.stepTwo.auto')}
-
{t('datasetCreation.stepTwo.autoDescription')}
-
-
-
setSegmentationType(SegmentType.CUSTOM)} - > - - -
-
{t('datasetCreation.stepTwo.custom')}
-
{t('datasetCreation.stepTwo.customDescription')}
-
- {segmentationType === SegmentType.CUSTOM && ( -
-
-
-
- {t('datasetCreation.stepTwo.separator')} - - {t('datasetCreation.stepTwo.separatorTip')} -
- } - /> -
- setSegmentIdentifier(e.target.value)} - /> -
-
-
-
-
{t('datasetCreation.stepTwo.maxLength')}
- setMaxChunkLength(Number.parseInt(e.target.value.replace(/^0+/, ''), 10))} - /> -
-
-
-
-
- {t('datasetCreation.stepTwo.overlap')} - - {t('datasetCreation.stepTwo.overlapTip')} -
- } - /> -
- setOverlap(Number.parseInt(e.target.value.replace(/^0+/, ''), 10))} - /> -
-
-
-
-
{t('datasetCreation.stepTwo.rules')}
- {rules.map(rule => ( -
- ruleChangeHandle(rule.id)} className="w-4 h-4 rounded border-gray-300 text-blue-700 focus:ring-blue-700" /> - -
- ))} -
-
-
- - -
-
- )} -
-
-
{t('datasetCreation.stepTwo.indexMode')}
-
-
- {(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.QUALIFIED)) && ( -
{ - if (isAPIKeySet) - setIndexType(IndexingType.QUALIFIED) - }} - > - - {!hasSetIndexType && } -
-
- {t('datasetCreation.stepTwo.qualified')} - {!hasSetIndexType && {t('datasetCreation.stepTwo.recommend')}} -
-
{t('datasetCreation.stepTwo.qualifiedTip')}
-
- {!isAPIKeySet && ( -
- {t('datasetCreation.stepTwo.warning')}  - {t('datasetCreation.stepTwo.click')} -
- )} -
- )} - - {(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.ECONOMICAL)) && ( -
- - {!hasSetIndexType && } -
-
{t('datasetCreation.stepTwo.economical')}
-
{t('datasetCreation.stepTwo.economicalTip')}
-
-
- )} -
- {hasSetIndexType && indexType === IndexingType.ECONOMICAL && ( -
- {t('datasetCreation.stepTwo.indexSettingTip')} - {t('datasetCreation.stepTwo.datasetSettingLink')} -
- )} - {IS_CE_EDITION && indexType === IndexingType.QUALIFIED && ( -
-
-
- -
-
-
{t('datasetCreation.stepTwo.QATitle')}
-
- {t('datasetCreation.stepTwo.QALanguage')} - -
-
-
- -
-
- {docForm === DocForm.QA && !QATipHide && ( -
- {t('datasetCreation.stepTwo.QATip')} - setQATipHide(true)} /> -
- )} -
- )} - {/* Embedding model */} - {indexType === IndexingType.QUALIFIED && ( -
-
{t('datasetSettings.form.embeddingModel')}
- { - setEmbeddingModel(model) - }} - /> - {!!datasetId && ( -
- {t('datasetCreation.stepTwo.indexSettingTip')} - {t('datasetCreation.stepTwo.datasetSettingLink')} -
- )} -
- )} - {/* Retrieval Method Config */} -
- {!datasetId - ? ( -
-
{t('datasetSettings.form.retrievalSetting.title')}
-
- {t('datasetSettings.form.retrievalSetting.learnMore')} - {t('datasetSettings.form.retrievalSetting.longDescription')} -
-
- ) - : ( -
-
{t('datasetSettings.form.retrievalSetting.title')}
-
- )} - -
- { - getIndexing_technique() === IndexingType.QUALIFIED - ? ( - - ) - : ( - - ) - } -
-
- -
-
- {dataSourceType === DataSourceType.FILE && ( - <> -
{t('datasetCreation.stepTwo.fileSource')}
-
- - {getFileName(files[0].name || '')} - {files.length > 1 && ( - - {t('datasetCreation.stepTwo.other')} - {files.length - 1} - {t('datasetCreation.stepTwo.fileUnit')} - - )} -
- - )} - {dataSourceType === DataSourceType.NOTION && ( - <> -
{t('datasetCreation.stepTwo.notionSource')}
-
- - {notionPages[0]?.page_name} - {notionPages.length > 1 && ( - - {t('datasetCreation.stepTwo.other')} - {notionPages.length - 1} - {t('datasetCreation.stepTwo.notionUnit')} - - )} -
- - )} - {dataSourceType === DataSourceType.WEB && ( - <> -
{t('datasetCreation.stepTwo.websiteSource')}
-
- - {websitePages[0].source_url} - {websitePages.length > 1 && ( - - {t('datasetCreation.stepTwo.other')} - {websitePages.length - 1} - {t('datasetCreation.stepTwo.webpageUnit')} - - )} -
- - )} -
-
-
-
{t('datasetCreation.stepTwo.estimateSegment')}
-
- { - fileIndexingEstimate - ? ( -
{formatNumber(fileIndexingEstimate.total_segments)}
- ) - : ( -
{t('datasetCreation.stepTwo.calculating')}
- ) - } -
-
-
- {!isSetting - ? ( -
- -
- -
+
+
{t('datasetCreation.stepTwo.segmentation')}
+ {(!datasetId || [ChuckingMode.text, ChuckingMode.qa].includes(currentDataset!.doc_form)) + && } + activeHeaderClassName='bg-dataset-option-card-blue-gradient' + description={t('datasetCreation.stepTwo.generalTip')} + isActive={ + [ChuckingMode.text, ChuckingMode.qa].includes( + datasetId ? currentDataset!.doc_form : docForm, ) - : ( -
- - + } + onSwitched={() => + handleChangeDocform(ChuckingMode.text) + } + actions={ + <> + + + + } + noHighlight={Boolean(datasetId)} + > +
+
+ setSegmentIdentifier(e.target.value, true)} + /> + + +
+
+
+
+ {t('datasetCreation.stepTwo.rules')} +
+
- )} -
+
+ {rules.map(rule => ( +
{ + ruleChangeHandle(rule.id) + }}> + + +
+ ))} + {IS_CE_EDITION && <> +
+ { + if (docForm === ChuckingMode.qa) + handleChangeDocform(ChuckingMode.text) + else + handleChangeDocform(ChuckingMode.qa) + }} + /> +
+ +
+ +
+ +
+
+ {docForm === ChuckingMode.qa && ( +
+ + + {t('datasetCreation.stepTwo.QATip')} + +
+ )} + } +
+
+
+
} + { + (!datasetId || currentDataset!.doc_form === ChuckingMode.parentChild) + && } + effectImg={OrangeEffect.src} + activeHeaderClassName='bg-dataset-option-card-orange-gradient' + description={t('datasetCreation.stepTwo.parentChildTip')} + isActive={ + datasetId ? currentDataset!.doc_form === ChuckingMode.parentChild : docForm === ChuckingMode.parentChild + } + onSwitched={() => handleChangeDocform(ChuckingMode.parentChild)} + actions={ + <> + + + + } + noHighlight={Boolean(datasetId)} + > +
+
+
+
+ {t('datasetCreation.stepTwo.parentChunkForContext')} +
+ +
+ } + title={t('datasetCreation.stepTwo.paragraph')} + description={t('datasetCreation.stepTwo.paragraphTip')} + isChosen={parentChildConfig.chunkForContext === 'paragraph'} + onChosen={() => setParentChildConfig( + { + ...parentChildConfig, + chunkForContext: 'paragraph', + }, + )} + chosenConfig={ +
+ setParentChildConfig({ + ...parentChildConfig, + parent: { + ...parentChildConfig.parent, + delimiter: e.target.value ? escape(e.target.value) : '', + }, + })} + /> + setParentChildConfig({ + ...parentChildConfig, + parent: { + ...parentChildConfig.parent, + maxLength: value, + }, + })} + /> +
+ } + /> + } + title={t('datasetCreation.stepTwo.fullDoc')} + description={t('datasetCreation.stepTwo.fullDocTip')} + onChosen={() => setParentChildConfig( + { + ...parentChildConfig, + chunkForContext: 'full-doc', + }, + )} + isChosen={parentChildConfig.chunkForContext === 'full-doc'} + /> +
+ +
+
+
+ {t('datasetCreation.stepTwo.childChunkForRetrieval')} +
+ +
+
+ setParentChildConfig({ + ...parentChildConfig, + child: { + ...parentChildConfig.child, + delimiter: e.target.value ? escape(e.target.value) : '', + }, + })} + /> + setParentChildConfig({ + ...parentChildConfig, + child: { + ...parentChildConfig.child, + maxLength: value, + }, + })} + /> +
+
+
+
+
+ {t('datasetCreation.stepTwo.rules')} +
+ +
+
+ {rules.map(rule => ( +
{ + ruleChangeHandle(rule.id) + }}> + + +
+ ))} +
+
+
+
} + +
{t('datasetCreation.stepTwo.indexMode')}
+
+ {(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.QUALIFIED)) && ( + + {t('datasetCreation.stepTwo.qualified')} + {!hasSetIndexType + && {t('datasetCreation.stepTwo.recommend')}} + + {!hasSetIndexType && } + +

} + description={t('datasetCreation.stepTwo.qualifiedTip')} + icon={} + isActive={!hasSetIndexType && indexType === IndexingType.QUALIFIED} + disabled={!isAPIKeySet || hasSetIndexType} + onSwitched={() => { + if (isAPIKeySet) + setIndexType(IndexingType.QUALIFIED) + }} + /> + )} + + {(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.ECONOMICAL)) && ( + <> + setIsQAConfirmDialogOpen(false)} className='w-[432px]'> +
+

+ {t('datasetCreation.stepTwo.qaSwitchHighQualityTipTitle')} +

+

+ {t('datasetCreation.stepTwo.qaSwitchHighQualityTipContent')} +

+
+
+ + +
+
+ + + } + isActive={!hasSetIndexType && indexType === IndexingType.ECONOMICAL} + disabled={!isAPIKeySet || hasSetIndexType || docForm !== ChuckingMode.text} + ref={economyDomRef} + onSwitched={() => { + if (isAPIKeySet && docForm === ChuckingMode.text) + setIndexType(IndexingType.ECONOMICAL) + }} + /> + + +
+ { + docForm === ChuckingMode.qa + ? t('datasetCreation.stepTwo.notAvailableForQA') + : t('datasetCreation.stepTwo.notAvailableForParentChild') + } +
+
+
+ )}
-
- - {showPreview &&
-
-
-
-
{t('datasetCreation.stepTwo.previewTitle')}
- {docForm === DocForm.QA && !previewSwitched && ( - - )} -
-
- -
-
- {docForm === DocForm.QA && !previewSwitched && ( -
- {t('datasetCreation.stepTwo.previewSwitchTipStart')} - {t('datasetCreation.stepTwo.previewSwitchTipEnd')} -
- )} -
-
- {previewSwitched && docForm === DocForm.QA && fileIndexingEstimate?.qa_preview && ( - <> - {fileIndexingEstimate?.qa_preview.map((item, index) => ( - - ))} - - )} - {(docForm === DocForm.TEXT || !previewSwitched) && fileIndexingEstimate?.preview && ( - <> - {fileIndexingEstimate?.preview.map((item, index) => ( - - ))} - - )} - {previewSwitched && docForm === DocForm.QA && !fileIndexingEstimate?.qa_preview && ( -
- -
- )} - {!previewSwitched && !fileIndexingEstimate?.preview && ( -
- -
- )} -
-
} - {!showPreview && ( -
-
- -
{t('datasetCreation.stepTwo.sideTipTitle')}
-
-

{t('datasetCreation.stepTwo.sideTipP1')}

-

{t('datasetCreation.stepTwo.sideTipP2')}

-

{t('datasetCreation.stepTwo.sideTipP3')}

-

{t('datasetCreation.stepTwo.sideTipP4')}

-
-
+ {hasSetIndexType && indexType === IndexingType.ECONOMICAL && ( +
+ {t('datasetCreation.stepTwo.indexSettingTip')} + {t('datasetCreation.stepTwo.datasetSettingLink')}
)} + {/* Embedding model */} + {indexType === IndexingType.QUALIFIED && ( +
+
{t('datasetSettings.form.embeddingModel')}
+ { + setEmbeddingModel(model) + }} + /> + {!!datasetId && ( +
+ {t('datasetCreation.stepTwo.indexSettingTip')} + {t('datasetCreation.stepTwo.datasetSettingLink')} +
+ )} +
+ )} + + {/* Retrieval Method Config */} +
+ {!datasetId + ? ( +
+
{t('datasetSettings.form.retrievalSetting.title')}
+
+ {t('datasetSettings.form.retrievalSetting.learnMore')} + {t('datasetSettings.form.retrievalSetting.longDescription')} +
+
+ ) + : ( +
+
{t('datasetSettings.form.retrievalSetting.title')}
+
+ )} + +
+ { + getIndexing_technique() === IndexingType.QUALIFIED + ? ( + + ) + : ( + + ) + } +
+
+ + {!isSetting + ? ( +
+ + +
+ ) + : ( +
+ + +
+ )} +
+ { }} footer={null}> + +
+ {dataSourceType === DataSourceType.FILE + && >} + onChange={(selected) => { + currentEstimateMutation.reset() + setPreviewFile(selected) + currentEstimateMutation.mutate() + }} + value={previewFile} + /> + } + {dataSourceType === DataSourceType.NOTION + && ({ + id: page.page_id, + name: page.page_name, + extension: 'md', + })) + } + onChange={(selected) => { + currentEstimateMutation.reset() + const selectedPage = notionPages.find(page => page.page_id === selected.id) + setPreviewNotionPage(selectedPage!) + currentEstimateMutation.mutate() + }} + value={{ + id: previewNotionPage?.page_id || '', + name: previewNotionPage?.page_name || '', + extension: 'md', + }} + /> + } + {dataSourceType === DataSourceType.WEB + && ({ + id: page.source_url, + name: page.title, + extension: 'md', + })) + } + onChange={(selected) => { + currentEstimateMutation.reset() + const selectedPage = websitePages.find(page => page.source_url === selected.id) + setPreviewWebsitePage(selectedPage!) + currentEstimateMutation.mutate() + }} + value={ + { + id: previewWebsitePage?.source_url || '', + name: previewWebsitePage?.title || '', + extension: 'md', + } + } + /> + } + +
+ } + className={cn('flex shrink-0 w-1/2 p-4 pr-0 relative h-full', isMobile && 'w-full max-w-[524px]')} + mainClassName='space-y-6' + > + {docForm === ChuckingMode.qa && estimate?.qa_preview && ( + estimate?.qa_preview.map(item => ( + + )) + )} + {docForm === ChuckingMode.text && estimate?.preview && ( + estimate?.preview.map((item, index) => ( + + {item.content} + + )) + )} + {docForm === ChuckingMode.parentChild && currentEstimateMutation.data?.preview && ( + estimate?.preview?.map((item, index) => { + const indexForLabel = index + 1 + return ( + + + {item.child_chunks.map((child, index) => { + const indexForLabel = index + 1 + return ( + + ) + })} + + + ) + }) + )} + {currentEstimateMutation.isIdle && ( +
+
+ +

+ {t('datasetCreation.stepTwo.previewChunkTip')} +

+
+
+ )} + {currentEstimateMutation.isPending && ( +
+ {Array.from({ length: 10 }, (_, i) => ( + + + + + + + + + + + ))} +
+ )} +
) diff --git a/web/app/components/datasets/create/step-two/inputs.tsx b/web/app/components/datasets/create/step-two/inputs.tsx new file mode 100644 index 0000000000..3d38a256f1 --- /dev/null +++ b/web/app/components/datasets/create/step-two/inputs.tsx @@ -0,0 +1,77 @@ +import type { FC, PropsWithChildren, ReactNode } from 'react' +import { useTranslation } from 'react-i18next' +import type { InputProps } from '@/app/components/base/input' +import Input from '@/app/components/base/input' +import Tooltip from '@/app/components/base/tooltip' +import type { InputNumberProps } from '@/app/components/base/input-number' +import { InputNumber } from '@/app/components/base/input-number' + +const TextLabel: FC = (props) => { + return +} + +const FormField: FC> = (props) => { + return
+ {props.label} + {props.children} +
+} + +export const DelimiterInput: FC = (props) => { + const { t } = useTranslation() + return + {t('datasetCreation.stepTwo.separator')} + + {t('datasetCreation.stepTwo.separatorTip')} +
+ } + /> +
}> + + +} + +export const MaxLengthInput: FC = (props) => { + const { t } = useTranslation() + return + {t('datasetCreation.stepTwo.maxLength')} +
}> + + +} + +export const OverlapInput: FC = (props) => { + const { t } = useTranslation() + return + {t('datasetCreation.stepTwo.overlap')} + + {t('datasetCreation.stepTwo.overlapTip')} +
+ } + /> +
}> + + +} diff --git a/web/app/components/datasets/create/step-two/language-select/index.tsx b/web/app/components/datasets/create/step-two/language-select/index.tsx index 41f3e0abb5..016f2a5f20 100644 --- a/web/app/components/datasets/create/step-two/language-select/index.tsx +++ b/web/app/components/datasets/create/step-two/language-select/index.tsx @@ -22,12 +22,13 @@ const LanguageSelect: FC = ({ manualClose trigger='click' disabled={disabled} + popupClassName='z-20' htmlContent={
{languages.filter(language => language.supported).map(({ prompt_name }) => (
onSelect(prompt_name)}>{prompt_name}
))} @@ -39,7 +40,7 @@ const LanguageSelect: FC = ({
} - btnClassName={open => cn('!border-0 !px-0 !py-0 !bg-inherit !hover:bg-inherit', open ? 'text-blue-600' : 'text-gray-500')} + btnClassName={() => cn('!border-0 !px-0 !py-0 !bg-inherit !hover:bg-inherit text-components-button-tertiary-text')} className='!w-[120px] h-fit !z-20 !translate-x-0 !left-[-16px]' /> ) diff --git a/web/app/components/datasets/create/step-two/option-card.tsx b/web/app/components/datasets/create/step-two/option-card.tsx new file mode 100644 index 0000000000..7d3b06f375 --- /dev/null +++ b/web/app/components/datasets/create/step-two/option-card.tsx @@ -0,0 +1,99 @@ +import { type ComponentProps, type FC, type ReactNode, forwardRef } from 'react' +import Image from 'next/image' +import classNames from '@/utils/classnames' + +const TriangleArrow: FC> = props => ( + + + +) + +type OptionCardHeaderProps = { + icon: ReactNode + title: ReactNode + description: string + isActive?: boolean + activeClassName?: string + effectImg?: string +} + +export const OptionCardHeader: FC = (props) => { + const { icon, title, description, isActive, activeClassName, effectImg } = props + return
+
+ {isActive && effectImg && } +
+
+ {icon} +
+
+
+ +
+
{title}
+
{description}
+
+
+} + +type OptionCardProps = { + icon: ReactNode + className?: string + activeHeaderClassName?: string + title: ReactNode + description: string + isActive?: boolean + actions?: ReactNode + effectImg?: string + onSwitched?: () => void + noHighlight?: boolean + disabled?: boolean +} & Omit, 'title'> + +export const OptionCard: FC = forwardRef((props, ref) => { + const { icon, className, title, description, isActive, children, actions, activeHeaderClassName, style, effectImg, onSwitched, onClick, noHighlight, disabled, ...rest } = props + return
{ + if (!isActive) + onSwitched?.() + onClick?.(e) + }} + {...rest} + ref={ref} + > + + {/** Body */} + {isActive && (children || actions) &&
+ {children} + {actions &&
+ {actions} +
+ } +
} +
+}) + +OptionCard.displayName = 'OptionCard' diff --git a/web/app/components/datasets/create/stepper/index.tsx b/web/app/components/datasets/create/stepper/index.tsx new file mode 100644 index 0000000000..317c1a76ee --- /dev/null +++ b/web/app/components/datasets/create/stepper/index.tsx @@ -0,0 +1,27 @@ +import { type FC, Fragment } from 'react' +import type { Step } from './step' +import { StepperStep } from './step' + +export type StepperProps = { + steps: Step[] + activeIndex: number +} + +export const Stepper: FC = (props) => { + const { steps, activeIndex } = props + return
+ {steps.map((step, index) => { + const isLast = index === steps.length - 1 + return ( + + + {!isLast &&
} + + ) + })} +
+} diff --git a/web/app/components/datasets/create/stepper/step.tsx b/web/app/components/datasets/create/stepper/step.tsx new file mode 100644 index 0000000000..c230de1a6e --- /dev/null +++ b/web/app/components/datasets/create/stepper/step.tsx @@ -0,0 +1,46 @@ +import type { FC } from 'react' +import classNames from '@/utils/classnames' + +export type Step = { + name: string +} + +export type StepperStepProps = Step & { + index: number + activeIndex: number +} + +export const StepperStep: FC = (props) => { + const { name, activeIndex, index } = props + const isActive = index === activeIndex + const isDisabled = activeIndex < index + const label = isActive ? `STEP ${index + 1}` : `${index + 1}` + return
+
+
+ {label} +
+
+
{name}
+
+} diff --git a/web/app/components/datasets/create/top-bar/index.tsx b/web/app/components/datasets/create/top-bar/index.tsx new file mode 100644 index 0000000000..6f773d9a3e --- /dev/null +++ b/web/app/components/datasets/create/top-bar/index.tsx @@ -0,0 +1,41 @@ +import type { FC } from 'react' +import { RiArrowLeftLine } from '@remixicon/react' +import Link from 'next/link' +import { useTranslation } from 'react-i18next' +import { Stepper, type StepperProps } from '../stepper' +import classNames from '@/utils/classnames' + +export type TopbarProps = Pick & { + className?: string +} + +const STEP_T_MAP: Record = { + 1: 'datasetCreation.steps.one', + 2: 'datasetCreation.steps.two', + 3: 'datasetCreation.steps.three', +} + +export const Topbar: FC = (props) => { + const { className, ...rest } = props + const { t } = useTranslation() + return
+ +
+ +
+

+ {t('datasetCreation.steps.header.creation')} +

+ +
+ ({ + name: t(STEP_T_MAP[i + 1]), + }))} + {...rest} + /> +
+
+} diff --git a/web/app/components/datasets/create/website/base/error-message.tsx b/web/app/components/datasets/create/website/base/error-message.tsx index aa337ec4bf..f061c4624e 100644 --- a/web/app/components/datasets/create/website/base/error-message.tsx +++ b/web/app/components/datasets/create/website/base/error-message.tsx @@ -18,7 +18,7 @@ const ErrorMessage: FC = ({ return (
- +
{title}
{errorMsg && ( diff --git a/web/app/components/datasets/create/website/preview.tsx b/web/app/components/datasets/create/website/preview.tsx index 65abe83ed7..070aa7ae83 100644 --- a/web/app/components/datasets/create/website/preview.tsx +++ b/web/app/components/datasets/create/website/preview.tsx @@ -18,7 +18,7 @@ const WebsitePreview = ({ const { t } = useTranslation() return ( -
+
{t('datasetCreation.stepOne.pagePreview')} diff --git a/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx b/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx index 36216aa7c8..d340f90deb 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx @@ -97,7 +97,7 @@ const CSVDownload: FC<{ docForm: DocForm }> = ({ docForm }) => { bom={true} data={getTemplate()} > -
+
{t('datasetDocuments.list.batchModal.template')}
diff --git a/web/app/components/datasets/documents/detail/completed/InfiniteVirtualList.tsx b/web/app/components/datasets/documents/detail/completed/InfiniteVirtualList.tsx deleted file mode 100644 index 7b510bcf21..0000000000 --- a/web/app/components/datasets/documents/detail/completed/InfiniteVirtualList.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type { CSSProperties, FC } from 'react' -import React from 'react' -import { FixedSizeList as List } from 'react-window' -import InfiniteLoader from 'react-window-infinite-loader' -import SegmentCard from './SegmentCard' -import s from './style.module.css' -import type { SegmentDetailModel } from '@/models/datasets' - -type IInfiniteVirtualListProps = { - hasNextPage?: boolean // Are there more items to load? (This information comes from the most recent API request.) - isNextPageLoading: boolean // Are we currently loading a page of items? (This may be an in-flight flag in your Redux store for example.) - items: Array // Array of items loaded so far. - loadNextPage: () => Promise // Callback function responsible for loading the next page of items. - onClick: (detail: SegmentDetailModel) => void - onChangeSwitch: (segId: string, enabled: boolean) => Promise - onDelete: (segId: string) => Promise - archived?: boolean - embeddingAvailable: boolean -} - -const InfiniteVirtualList: FC = ({ - hasNextPage, - isNextPageLoading, - items, - loadNextPage, - onClick: onClickCard, - onChangeSwitch, - onDelete, - archived, - embeddingAvailable, -}) => { - // If there are more items to be loaded then add an extra row to hold a loading indicator. - const itemCount = hasNextPage ? items.length + 1 : items.length - - // Only load 1 page of items at a time. - // Pass an empty callback to InfiniteLoader in case it asks us to load more than once. - const loadMoreItems = isNextPageLoading ? () => { } : loadNextPage - - // Every row is loaded except for our loading indicator row. - const isItemLoaded = (index: number) => !hasNextPage || index < items.length - - // Render an item or a loading indicator. - const Item = ({ index, style }: { index: number; style: CSSProperties }) => { - let content - if (!isItemLoaded(index)) { - content = ( - <> - {[1, 2, 3].map(v => ( - - ))} - - ) - } - else { - content = items[index].map(segItem => ( - onClickCard(segItem)} - onChangeSwitch={onChangeSwitch} - onDelete={onDelete} - loading={false} - archived={archived} - embeddingAvailable={embeddingAvailable} - /> - )) - } - - return ( -
- {content} -
- ) - } - - return ( - - {({ onItemsRendered, ref }) => ( - - {Item} - - )} - - ) -} -export default InfiniteVirtualList diff --git a/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx b/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx index 5b76acc936..264d62b68a 100644 --- a/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx +++ b/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx @@ -6,9 +6,9 @@ import { RiDeleteBinLine, } from '@remixicon/react' import { StatusItem } from '../../list' -import { DocumentTitle } from '../index' +import style from '../../style.module.css' import s from './style.module.css' -import { SegmentIndexTag } from './index' +import { SegmentIndexTag } from './common/segment-index-tag' import cn from '@/utils/classnames' import Confirm from '@/app/components/base/confirm' import Switch from '@/app/components/base/switch' @@ -31,6 +31,22 @@ const ProgressBar: FC<{ percent: number; loading: boolean }> = ({ percent, loadi ) } +type DocumentTitleProps = { + extension?: string + name?: string + iconCls?: string + textCls?: string + wrapperCls?: string +} + +const DocumentTitle: FC = ({ extension, name, iconCls, textCls, wrapperCls }) => { + const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase() + return
+
+ {name || '--'} +
+} + export type UsageScene = 'doc' | 'hitTesting' type ISegmentCardProps = { diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx new file mode 100644 index 0000000000..34728170d7 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx @@ -0,0 +1,133 @@ +import React, { type FC, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, + RiExpandDiagonalLine, +} from '@remixicon/react' +import ActionButtons from './common/action-buttons' +import ChunkContent from './common/chunk-content' +import Dot from './common/dot' +import { SegmentIndexTag } from './common/segment-index-tag' +import { useSegmentListContext } from './index' +import type { ChildChunkDetail, ChuckingMode } from '@/models/datasets' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { formatNumber } from '@/utils/format' +import classNames from '@/utils/classnames' +import Divider from '@/app/components/base/divider' +import { formatTime } from '@/utils/time' + +type IChildSegmentDetailProps = { + chunkId: string + childChunkInfo?: Partial & { id: string } + onUpdate: (segmentId: string, childChunkId: string, content: string) => void + onCancel: () => void + docForm: ChuckingMode +} + +/** + * Show all the contents of the segment + */ +const ChildSegmentDetail: FC = ({ + chunkId, + childChunkInfo, + onUpdate, + onCancel, + docForm, +}) => { + const { t } = useTranslation() + const [content, setContent] = useState(childChunkInfo?.content || '') + const { eventEmitter } = useEventEmitterContextContext() + const [loading, setLoading] = useState(false) + const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + + eventEmitter?.useSubscription((v) => { + if (v === 'update-child-segment') + setLoading(true) + if (v === 'update-child-segment-done') + setLoading(false) + }) + + const handleCancel = () => { + onCancel() + setContent(childChunkInfo?.content || '') + } + + const handleSave = () => { + onUpdate(chunkId, childChunkInfo?.id || '', content) + } + + const wordCountText = useMemo(() => { + const count = content.length + return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [content.length]) + + const EditTimeText = useMemo(() => { + const timeText = formatTime({ + date: (childChunkInfo?.updated_at ?? 0) * 1000, + dateFormat: 'MM/DD/YYYY h:mm:ss', + }) + return `${t('datasetDocuments.segment.editedAt')} ${timeText}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childChunkInfo?.updated_at]) + + return ( +
+
+
+
{t('datasetDocuments.segment.editChildChunk')}
+
+ + + {wordCountText} + + + {EditTimeText} + +
+
+
+ {fullScreen && ( + <> + + + + )} +
+ +
+
+ +
+
+
+
+
+ setContent(content)} + isEditMode={true} + /> +
+
+ {!fullScreen && ( +
+ +
+ )} +
+ ) +} + +export default React.memo(ChildSegmentDetail) diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx new file mode 100644 index 0000000000..f862e82a0c --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx @@ -0,0 +1,176 @@ +import { type FC, useMemo, useState } from 'react' +import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { EditSlice } from '../../../formatted-text/flavours/edit-slice' +import { useDocumentContext } from '../index' +import { FormattedText } from '../../../formatted-text/formatted' +import Empty from './common/empty' +import FullDocListSkeleton from './skeleton/full-doc-list-skeleton' +import type { ChildChunkDetail } from '@/models/datasets' +import Input from '@/app/components/base/input' +import classNames from '@/utils/classnames' +import Divider from '@/app/components/base/divider' +import { formatNumber } from '@/utils/format' + +type IChildSegmentCardProps = { + childChunks: ChildChunkDetail[] + parentChunkId: string + handleInputChange?: (value: string) => void + handleAddNewChildChunk?: (parentChunkId: string) => void + enabled: boolean + onDelete?: (segId: string, childChunkId: string) => Promise + onClickSlice?: (childChunk: ChildChunkDetail) => void + total?: number + inputValue?: string + onClearFilter?: () => void + isLoading?: boolean +} + +const ChildSegmentList: FC = ({ + childChunks, + parentChunkId, + handleInputChange, + handleAddNewChildChunk, + enabled, + onDelete, + onClickSlice, + total, + inputValue, + onClearFilter, + isLoading, +}) => { + const { t } = useTranslation() + const parentMode = useDocumentContext(s => s.parentMode) + + const [collapsed, setCollapsed] = useState(true) + + const toggleCollapse = () => { + setCollapsed(!collapsed) + } + + const isParagraphMode = useMemo(() => { + return parentMode === 'paragraph' + }, [parentMode]) + + const isFullDocMode = useMemo(() => { + return parentMode === 'full-doc' + }, [parentMode]) + + const contentOpacity = useMemo(() => { + return enabled ? '' : 'opacity-50 group-hover/card:opacity-100' + }, [enabled]) + + const totalText = useMemo(() => { + const isSearch = inputValue !== '' && isFullDocMode + if (!isSearch) { + const text = isFullDocMode + ? !total + ? '--' + : formatNumber(total) + : formatNumber(childChunks.length) + const count = isFullDocMode + ? text === '--' + ? 0 + : total + : childChunks.length + return `${text} ${t('datasetDocuments.segment.childChunks', { count })}` + } + else { + const text = !total ? '--' : formatNumber(total) + const count = text === '--' ? 0 : total + return `${count} ${t('datasetDocuments.segment.searchResults', { count })}` + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFullDocMode, total, childChunks.length, inputValue]) + + return ( +
+ {isFullDocMode ? : null} +
+
{ + event.stopPropagation() + toggleCollapse() + }}> + { + isParagraphMode + ? collapsed + ? ( + + ) + : () + : null + } + {totalText} + · + +
+ {isFullDocMode + ? handleInputChange?.(e.target.value)} + onClear={() => handleInputChange?.('')} + /> + : null} +
+ {isLoading ? : null} + {((isFullDocMode && !isLoading) || !collapsed) + ?
+ {isParagraphMode && ( +
+ +
+ )} + {childChunks.length > 0 + ? + {childChunks.map((childChunk) => { + const edited = childChunk.updated_at !== childChunk.created_at + return onDelete?.(childChunk.segment_id, childChunk.id)} + className='line-clamp-3' + labelInnerClassName='text-[10px] font-semibold align-bottom leading-6' + contentClassName='!leading-6' + showDivider={false} + onClick={(e) => { + e.stopPropagation() + onClickSlice?.(childChunk) + }} + /> + })} + + : inputValue !== '' + ?
+ +
+ : null + } +
+ : null} +
+ ) +} + +export default ChildSegmentList diff --git a/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx new file mode 100644 index 0000000000..15bff500b5 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx @@ -0,0 +1,85 @@ +import React, { type FC, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useKeyPress } from 'ahooks' +import { useDocumentContext } from '../../index' +import Button from '@/app/components/base/button' +import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' + +type IActionButtonsProps = { + handleCancel: () => void + handleSave: () => void + loading: boolean + actionType?: 'edit' | 'add' + handleRegeneration?: () => void + isChildChunk?: boolean +} + +const ActionButtons: FC = ({ + handleCancel, + handleSave, + loading, + actionType = 'edit', + handleRegeneration, + isChildChunk = false, +}) => { + const { t } = useTranslation() + const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode]) + + useKeyPress(['esc'], (e) => { + e.preventDefault() + handleCancel() + }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.s`, (e) => { + e.preventDefault() + if (loading) + return + handleSave() + } + , { exactMatch: true, useCapture: true }) + + const isParentChildParagraphMode = useMemo(() => { + return mode === 'hierarchical' && parentMode === 'paragraph' + }, [mode, parentMode]) + + return ( +
+ + {(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk) + ? + : null + } + +
+ ) +} + +ActionButtons.displayName = 'ActionButtons' + +export default React.memo(ActionButtons) diff --git a/web/app/components/datasets/documents/detail/completed/common/add-another.tsx b/web/app/components/datasets/documents/detail/completed/common/add-another.tsx new file mode 100644 index 0000000000..444560e55f --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/add-another.tsx @@ -0,0 +1,32 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import classNames from '@/utils/classnames' +import Checkbox from '@/app/components/base/checkbox' + +type AddAnotherProps = { + className?: string + isChecked: boolean + onCheck: () => void +} + +const AddAnother: FC = ({ + className, + isChecked, + onCheck, +}) => { + const { t } = useTranslation() + + return ( +
+ + {t('datasetDocuments.segment.addAnother')} +
+ ) +} + +export default React.memo(AddAnother) diff --git a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx new file mode 100644 index 0000000000..df3ae6e1ec --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx @@ -0,0 +1,103 @@ +import React, { type FC } from 'react' +import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import Divider from '@/app/components/base/divider' +import classNames from '@/utils/classnames' +import Confirm from '@/app/components/base/confirm' + +const i18nPrefix = 'dataset.batchAction' +type IBatchActionProps = { + className?: string + selectedIds: string[] + onBatchEnable: () => void + onBatchDisable: () => void + onBatchDelete: () => Promise + onArchive?: () => void + onCancel: () => void +} + +const BatchAction: FC = ({ + className, + selectedIds, + onBatchEnable, + onBatchDisable, + onArchive, + onBatchDelete, + onCancel, +}) => { + const { t } = useTranslation() + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + const [isDeleting, { + setTrue: setIsDeleting, + }] = useBoolean(false) + + const handleBatchDelete = async () => { + setIsDeleting() + await onBatchDelete() + hideDeleteConfirm() + } + return ( +
+
+
+ + {selectedIds.length} + + {t(`${i18nPrefix}.selected`)} +
+ +
+ + +
+
+ + +
+ {onArchive && ( +
+ + +
+ )} +
+ + +
+ + + +
+ { + isShowDeleteConfirm && ( + + ) + } +
+ ) +} + +export default React.memo(BatchAction) diff --git a/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx b/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx new file mode 100644 index 0000000000..47bd3ab4a1 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx @@ -0,0 +1,66 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { ChuckingMode } from '@/models/datasets' +import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' + +type IChunkContentProps = { + question: string + answer?: string + onQuestionChange: (question: string) => void + onAnswerChange?: (answer: string) => void + isEditMode?: boolean + docForm: ChuckingMode +} + +const ChunkContent: FC = ({ + question, + answer, + onQuestionChange, + onAnswerChange, + isEditMode, + docForm, +}) => { + const { t } = useTranslation() + + if (docForm === ChuckingMode.qa) { + return ( + <> +
QUESTION
+ onQuestionChange(e.target.value)} + disabled={!isEditMode} + /> +
ANSWER
+ onAnswerChange?.(e.target.value)} + disabled={!isEditMode} + autoFocus + /> + + ) + } + + return ( + onQuestionChange(e.target.value)} + disabled={!isEditMode} + autoFocus + /> + ) +} + +ChunkContent.displayName = 'ChunkContent' + +export default React.memo(ChunkContent) diff --git a/web/app/components/datasets/documents/detail/completed/common/dot.tsx b/web/app/components/datasets/documents/detail/completed/common/dot.tsx new file mode 100644 index 0000000000..aa6a9f13b7 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/dot.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const Dot = () => { + return ( +
·
+ ) +} + +Dot.displayName = 'Dot' + +export default React.memo(Dot) diff --git a/web/app/components/datasets/documents/detail/completed/common/empty.tsx b/web/app/components/datasets/documents/detail/completed/common/empty.tsx new file mode 100644 index 0000000000..3551fd1b8b --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/empty.tsx @@ -0,0 +1,77 @@ +import React, { type FC } from 'react' +import { RiFileList2Line } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +type IEmptyProps = { + onClearFilter: () => void +} + +const EmptyCard = React.memo(() => { + return ( +
+ ) +}) + +EmptyCard.displayName = 'EmptyCard' + +type LineProps = { + className?: string +} + +const Line = React.memo(({ + className, +}: LineProps) => { + return ( + + + + + + + + + + + ) +}) + +Line.displayName = 'Line' + +const Empty: FC = ({ + onClearFilter, +}) => { + const { t } = useTranslation() + + return ( +
+
+
+ + + + + +
+
+ {t('datasetDocuments.segment.empty')} +
+ +
+
+ { + Array.from({ length: 10 }).map((_, i) => ( + + )) + } +
+
+
+ ) +} + +export default React.memo(Empty) diff --git a/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx b/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx new file mode 100644 index 0000000000..0d86d2de61 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx @@ -0,0 +1,35 @@ +import React, { type FC } from 'react' +import Drawer from '@/app/components/base/drawer' +import classNames from '@/utils/classnames' + +type IFullScreenDrawerProps = { + isOpen: boolean + onClose?: () => void + fullScreen: boolean + children: React.ReactNode +} + +const FullScreenDrawer: FC = ({ + isOpen, + onClose = () => {}, + fullScreen, + children, +}) => { + return ( + + {children} + ) +} + +export default FullScreenDrawer diff --git a/web/app/components/datasets/documents/detail/completed/common/keywords.tsx b/web/app/components/datasets/documents/detail/completed/common/keywords.tsx new file mode 100644 index 0000000000..8c85ec1378 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/keywords.tsx @@ -0,0 +1,47 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import classNames from '@/utils/classnames' +import type { SegmentDetailModel } from '@/models/datasets' +import TagInput from '@/app/components/base/tag-input' + +type IKeywordsProps = { + segInfo?: Partial & { id: string } + className?: string + keywords: string[] + onKeywordsChange: (keywords: string[]) => void + isEditMode?: boolean + actionType?: 'edit' | 'add' | 'view' +} + +const Keywords: FC = ({ + segInfo, + className, + keywords, + onKeywordsChange, + isEditMode, + actionType = 'view', +}) => { + const { t } = useTranslation() + return ( +
+
{t('datasetDocuments.segment.keywords')}
+
+ {(!segInfo?.keywords?.length && actionType === 'view') + ? '-' + : ( + onKeywordsChange(newKeywords)} + disableAdd={!isEditMode} + disableRemove={!isEditMode || (keywords.length === 1)} + /> + ) + } +
+
+ ) +} + +Keywords.displayName = 'Keywords' + +export default React.memo(Keywords) diff --git a/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx b/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx new file mode 100644 index 0000000000..c9356b7f8a --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx @@ -0,0 +1,131 @@ +import React, { type FC, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiLoader2Line } from '@remixicon/react' +import { useCountDown } from 'ahooks' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import { useEventEmitterContextContext } from '@/context/event-emitter' + +type IDefaultContentProps = { + onCancel: () => void + onConfirm: () => void +} + +const DefaultContent: FC = React.memo(({ + onCancel, + onConfirm, +}) => { + const { t } = useTranslation() + + return ( + <> +
+ {t('datasetDocuments.segment.regenerationConfirmTitle')} +

{t('datasetDocuments.segment.regenerationConfirmMessage')}

+
+
+ + +
+ + ) +}) + +DefaultContent.displayName = 'DefaultContent' + +const RegeneratingContent: FC = React.memo(() => { + const { t } = useTranslation() + + return ( + <> +
+ {t('datasetDocuments.segment.regeneratingTitle')} +

{t('datasetDocuments.segment.regeneratingMessage')}

+
+
+ +
+ + ) +}) + +RegeneratingContent.displayName = 'RegeneratingContent' + +type IRegenerationCompletedContentProps = { + onClose: () => void +} + +const RegenerationCompletedContent: FC = React.memo(({ + onClose, +}) => { + const { t } = useTranslation() + const targetTime = useRef(Date.now() + 5000) + const [countdown] = useCountDown({ + targetDate: targetTime.current, + onEnd: () => { + onClose() + }, + }) + + return ( + <> +
+ {t('datasetDocuments.segment.regenerationSuccessTitle')} +

{t('datasetDocuments.segment.regenerationSuccessMessage')}

+
+
+ +
+ + ) +}) + +RegenerationCompletedContent.displayName = 'RegenerationCompletedContent' + +type IRegenerationModalProps = { + isShow: boolean + onConfirm: () => void + onCancel: () => void + onClose: () => void +} + +const RegenerationModal: FC = ({ + isShow, + onConfirm, + onCancel, + onClose, +}) => { + const [loading, setLoading] = useState(false) + const [updateSucceeded, setUpdateSucceeded] = useState(false) + const { eventEmitter } = useEventEmitterContextContext() + + eventEmitter?.useSubscription((v) => { + if (v === 'update-segment') { + setLoading(true) + setUpdateSucceeded(false) + } + if (v === 'update-segment-success') + setUpdateSucceeded(true) + if (v === 'update-segment-done') + setLoading(false) + }) + + return ( + {}} className='!max-w-[480px] !rounded-2xl'> + {!loading && !updateSucceeded && } + {loading && !updateSucceeded && } + {!loading && updateSucceeded && } + + ) +} + +export default RegenerationModal diff --git a/web/app/components/datasets/documents/detail/completed/common/segment-index-tag.tsx b/web/app/components/datasets/documents/detail/completed/common/segment-index-tag.tsx new file mode 100644 index 0000000000..7d21311c9a --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/segment-index-tag.tsx @@ -0,0 +1,36 @@ +import React, { type FC, useMemo } from 'react' +import { Chunk } from '@/app/components/base/icons/src/public/knowledge' +import cn from '@/utils/classnames' + +type ISegmentIndexTagProps = { + positionId?: string | number + label?: string + className?: string + labelPrefix?: string +} + +export const SegmentIndexTag: FC = ({ + positionId, + label, + className, + labelPrefix = 'Chunk', +}) => { + const localPositionId = useMemo(() => { + const positionIdStr = String(positionId) + if (positionIdStr.length >= 3) + return `${labelPrefix}-${positionId}` + return `${labelPrefix}-${positionIdStr.padStart(2, '0')}` + }, [positionId, labelPrefix]) + return ( +
+ +
+ {label || localPositionId} +
+
+ ) +} + +SegmentIndexTag.displayName = 'SegmentIndexTag' + +export default React.memo(SegmentIndexTag) diff --git a/web/app/components/datasets/documents/detail/completed/common/tag.tsx b/web/app/components/datasets/documents/detail/completed/common/tag.tsx new file mode 100644 index 0000000000..c88bffc736 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/tag.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import cn from '@/utils/classnames' + +const Tag = ({ text, className }: { text: string; className?: string }) => { + return ( +
+ # + {text} +
+ ) +} + +Tag.displayName = 'Tag' + +export default React.memo(Tag) diff --git a/web/app/components/datasets/documents/detail/completed/display-toggle.tsx b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx new file mode 100644 index 0000000000..5630a2ba74 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx @@ -0,0 +1,39 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { RiLineHeight } from '@remixicon/react' +import Tooltip from '@/app/components/base/tooltip' +import { Collapse } from '@/app/components/base/icons/src/public/knowledge' + +type DisplayToggleProps = { + isCollapsed: boolean + toggleCollapsed: () => void +} + +const DisplayToggle: FC = ({ + isCollapsed, + toggleCollapsed, +}) => { + const { t } = useTranslation() + + return ( + + + + + ) +} + +export default React.memo(DisplayToggle) diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 2c9e6ca2ea..91697db5ce 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -1,220 +1,63 @@ 'use client' import type { FC } from 'react' -import React, { memo, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useDebounceFn } from 'ahooks' -import { HashtagIcon } from '@heroicons/react/24/solid' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { isNil, omitBy } from 'lodash-es' -import { - RiCloseLine, - RiEditLine, -} from '@remixicon/react' -import { StatusItem } from '../../list' -import { DocumentContext } from '../index' +import { createContext, useContext, useContextSelector } from 'use-context-selector' +import { useDocumentContext } from '../index' import { ProcessStatus } from '../segment-add' import s from './style.module.css' -import InfiniteVirtualList from './InfiniteVirtualList' +import SegmentList from './segment-list' +import DisplayToggle from './display-toggle' +import BatchAction from './common/batch-action' +import SegmentDetail from './segment-detail' +import SegmentCard from './segment-card' +import ChildSegmentList from './child-segment-list' +import NewChildSegment from './new-child-segment' +import FullScreenDrawer from './common/full-screen-drawer' +import ChildSegmentDetail from './child-segment-detail' +import Pagination from '@/app/components/base/pagination' import cn from '@/utils/classnames' import { formatNumber } from '@/utils/format' -import Modal from '@/app/components/base/modal' -import Switch from '@/app/components/base/switch' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' import { ToastContext } from '@/app/components/base/toast' import type { Item } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select' -import { deleteSegment, disableSegment, enableSegment, fetchSegments, updateSegment } from '@/service/datasets' -import type { SegmentDetailModel, SegmentUpdater, SegmentsQuery, SegmentsResponse } from '@/models/datasets' -import { asyncRunSafe } from '@/utils' -import type { CommonResponse } from '@/models/common' -import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' -import Button from '@/app/components/base/button' -import NewSegmentModal from '@/app/components/datasets/documents/detail/new-segment-modal' -import TagInput from '@/app/components/base/tag-input' +import { type ChildChunkDetail, ChuckingMode, type SegmentDetailModel, type SegmentUpdater } from '@/models/datasets' +import NewSegment from '@/app/components/datasets/documents/detail/new-segment' import { useEventEmitterContextContext } from '@/context/event-emitter' +import Checkbox from '@/app/components/base/checkbox' +import { + useChildSegmentList, + useChildSegmentListKey, + useDeleteChildSegment, + useDeleteSegment, + useDisableSegment, + useEnableSegment, + useSegmentList, + useSegmentListKey, + useUpdateChildSegment, + useUpdateSegment, +} from '@/service/knowledge/use-segment' +import { useInvalid } from '@/service/use-base' -export const SegmentIndexTag: FC<{ positionId: string | number; className?: string }> = ({ positionId, className }) => { - const localPositionId = useMemo(() => { - const positionIdStr = String(positionId) - if (positionIdStr.length >= 3) - return positionId - return positionIdStr.padStart(3, '0') - }, [positionId]) - return ( -
- - {localPositionId} -
- ) +const DEFAULT_LIMIT = 10 + +type SegmentListContextValue = { + isCollapsed: boolean + fullScreen: boolean + toggleFullScreen: (fullscreen?: boolean) => void } -type ISegmentDetailProps = { - embeddingAvailable: boolean - segInfo?: Partial & { id: string } - onChangeSwitch?: (segId: string, enabled: boolean) => Promise - onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void - onCancel: () => void - archived?: boolean -} -/** - * Show all the contents of the segment - */ -const SegmentDetailComponent: FC = ({ - embeddingAvailable, - segInfo, - archived, - onChangeSwitch, - onUpdate, - onCancel, -}) => { - const { t } = useTranslation() - const [isEditing, setIsEditing] = useState(false) - const [question, setQuestion] = useState(segInfo?.content || '') - const [answer, setAnswer] = useState(segInfo?.answer || '') - const [keywords, setKeywords] = useState(segInfo?.keywords || []) - const { eventEmitter } = useEventEmitterContextContext() - const [loading, setLoading] = useState(false) +const SegmentListContext = createContext({ + isCollapsed: true, + fullScreen: false, + toggleFullScreen: () => {}, +}) - eventEmitter?.useSubscription((v) => { - if (v === 'update-segment') - setLoading(true) - else - setLoading(false) - }) - - const handleCancel = () => { - setIsEditing(false) - setQuestion(segInfo?.content || '') - setAnswer(segInfo?.answer || '') - setKeywords(segInfo?.keywords || []) - } - const handleSave = () => { - onUpdate(segInfo?.id || '', question, answer, keywords) - } - - const renderContent = () => { - if (segInfo?.answer) { - return ( - <> -
QUESTION
- setQuestion(e.target.value)} - disabled={!isEditing} - /> -
ANSWER
- setAnswer(e.target.value)} - disabled={!isEditing} - autoFocus - /> - - ) - } - - return ( - setQuestion(e.target.value)} - disabled={!isEditing} - autoFocus - /> - ) - } - - return ( -
-
- {isEditing && ( - <> - - - - )} - {!isEditing && !archived && embeddingAvailable && ( - <> -
-
{t('common.operation.edit')}
- setIsEditing(true)} /> -
-
- - )} -
- -
-
- -
{renderContent()}
-
{t('datasetDocuments.segment.keywords')}
-
- {!segInfo?.keywords?.length - ? '-' - : ( - setKeywords(newKeywords)} - disableAdd={!isEditing} - disableRemove={!isEditing || (keywords.length === 1)} - /> - ) - } -
-
-
-
{formatNumber(segInfo?.word_count as number)} {t('datasetDocuments.segment.characters')} -
{formatNumber(segInfo?.hit_count as number)} {t('datasetDocuments.segment.hitCount')} -
{t('datasetDocuments.segment.vectorHash')}{segInfo?.index_node_hash} -
-
- - {embeddingAvailable && ( - <> - - { - await onChangeSwitch?.(segInfo?.id || '', val) - }} - disabled={archived} - /> - - )} -
-
-
- ) -} -export const SegmentDetail = memo(SegmentDetailComponent) - -export const splitArray = (arr: any[], size = 3) => { - if (!arr || !arr.length) - return [] - const result = [] - for (let i = 0; i < arr.length; i += size) - result.push(arr.slice(i, i + size)) - return result +export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => { + return useContextSelector(SegmentListContext, selector) } type ICompletedProps = { @@ -223,7 +66,6 @@ type ICompletedProps = { onNewSegmentModalChange: (state: boolean) => void importStatus: ProcessStatus | string | undefined archived?: boolean - // data: Array<{}> // all/part segments } /** * Embedding done, show list of all segments @@ -238,22 +80,33 @@ const Completed: FC = ({ }) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) - const { datasetId = '', documentId = '', docForm } = useContext(DocumentContext) + const [datasetId = '', documentId = '', docForm, mode, parentMode] = useDocumentContext(s => [s.datasetId, s.documentId, s.docForm, s.mode, s.parentMode]) // the current segment id and whether to show the modal - const [currSegment, setCurrSegment] = useState<{ segInfo?: SegmentDetailModel; showModal: boolean }>({ showModal: false }) + const [currSegment, setCurrSegment] = useState<{ segInfo?: SegmentDetailModel; showModal: boolean; isEditMode?: boolean }>({ showModal: false }) + const [currChildChunk, setCurrChildChunk] = useState<{ childChunkInfo?: ChildChunkDetail; showModal: boolean }>({ showModal: false }) + const [currChunkId, setCurrChunkId] = useState('') const [inputValue, setInputValue] = useState('') // the input value const [searchValue, setSearchValue] = useState('') // the search value const [selectedStatus, setSelectedStatus] = useState('all') // the selected status, enabled/disabled/undefined - const [lastSegmentsRes, setLastSegmentsRes] = useState(undefined) - const [allSegments, setAllSegments] = useState>([]) // all segments data - const [loading, setLoading] = useState(false) - const [total, setTotal] = useState() + const [segments, setSegments] = useState([]) // all segments data + const [childSegments, setChildSegments] = useState([]) // all child segments data + const [selectedSegmentIds, setSelectedSegmentIds] = useState([]) const { eventEmitter } = useEventEmitterContextContext() + const [isCollapsed, setIsCollapsed] = useState(true) + const [currentPage, setCurrentPage] = useState(1) // start from 1 + const [limit, setLimit] = useState(DEFAULT_LIMIT) + const [fullScreen, setFullScreen] = useState(false) + const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false) + + const segmentListRef = useRef(null) + const childSegmentListRef = useRef(null) + const needScrollToBottom = useRef(false) const { run: handleSearch } = useDebounceFn(() => { setSearchValue(inputValue) + setCurrentPage(1) }, { wait: 500 }) const handleInputChange = (value: string) => { @@ -263,78 +116,145 @@ const Completed: FC = ({ const onChangeStatus = ({ value }: Item) => { setSelectedStatus(value === 'all' ? 'all' : !!value) + setCurrentPage(1) } - const getSegments = async (needLastId?: boolean) => { - const finalLastId = lastSegmentsRes?.data?.[lastSegmentsRes.data.length - 1]?.id || '' - setLoading(true) - const [e, res] = await asyncRunSafe(fetchSegments({ + const isFullDocMode = useMemo(() => { + return mode === 'hierarchical' && parentMode === 'full-doc' + }, [mode, parentMode]) + + const { isFetching: isLoadingSegmentList, data: segmentListData } = useSegmentList( + { datasetId, documentId, - params: omitBy({ - last_id: !needLastId ? undefined : finalLastId, - limit: 12, - keyword: searchValue, + params: { + page: isFullDocMode ? 1 : currentPage, + limit: isFullDocMode ? 10 : limit, + keyword: isFullDocMode ? '' : searchValue, enabled: selectedStatus === 'all' ? 'all' : !!selectedStatus, - }, isNil) as SegmentsQuery, - }) as Promise) - if (!e) { - setAllSegments([...(!needLastId ? [] : allSegments), ...splitArray(res.data || [])]) - setLastSegmentsRes(res) - if (!lastSegmentsRes || !needLastId) - setTotal(res?.total || 0) + }, + }, + currentPage === 0, + ) + const invalidSegmentList = useInvalid(useSegmentListKey) + + useEffect(() => { + if (segmentListData) { + setSegments(segmentListData.data || []) + if (segmentListData.total_pages < currentPage) + setCurrentPage(segmentListData.total_pages) } - setLoading(false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segmentListData]) + + useEffect(() => { + if (segmentListRef.current && needScrollToBottom.current) { + segmentListRef.current.scrollTo({ top: segmentListRef.current.scrollHeight, behavior: 'smooth' }) + needScrollToBottom.current = false + } + }, [segments]) + + const { isFetching: isLoadingChildSegmentList, data: childChunkListData } = useChildSegmentList( + { + datasetId, + documentId, + segmentId: segments[0]?.id || '', + params: { + page: currentPage, + limit, + keyword: searchValue, + }, + }, + !isFullDocMode || segments.length === 0 || currentPage === 0, + ) + const invalidChildSegmentList = useInvalid(useChildSegmentListKey) + + useEffect(() => { + if (childSegmentListRef.current && needScrollToBottom.current) { + childSegmentListRef.current.scrollTo({ top: childSegmentListRef.current.scrollHeight, behavior: 'smooth' }) + needScrollToBottom.current = false + } + }, [childSegments]) + + useEffect(() => { + if (childChunkListData) { + setChildSegments(childChunkListData.data || []) + if (childChunkListData.total_pages < currentPage) + setCurrentPage(childChunkListData.total_pages) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childChunkListData]) + + const resetList = useCallback(() => { + setSegments([]) + setSelectedSegmentIds([]) + invalidSegmentList() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const resetChildList = useCallback(() => { + setChildSegments([]) + invalidChildSegmentList() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => { + setCurrSegment({ segInfo: detail, showModal: true, isEditMode }) } - const resetList = () => { - setLastSegmentsRes(undefined) - setAllSegments([]) - setLoading(false) - setTotal(undefined) - getSegments(false) - } - - const onClickCard = (detail: SegmentDetailModel) => { - setCurrSegment({ segInfo: detail, showModal: true }) - } - - const onCloseModal = () => { + const onCloseSegmentDetail = useCallback(() => { setCurrSegment({ ...currSegment, showModal: false }) - } + setFullScreen(false) + }, [currSegment]) - const onChangeSwitch = async (segId: string, enabled: boolean) => { - const opApi = enabled ? enableSegment : disableSegment - const [e] = await asyncRunSafe(opApi({ datasetId, segmentId: segId }) as Promise) - if (!e) { - notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - for (const item of allSegments) { - for (const seg of item) { - if (seg.id === segId) - seg.enabled = enabled + const { mutateAsync: enableSegment } = useEnableSegment() + const { mutateAsync: disableSegment } = useDisableSegment() + + const onChangeSwitch = useCallback(async (enable: boolean, segId?: string) => { + const operationApi = enable ? enableSegment : disableSegment + await operationApi({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, { + onSuccess: () => { + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + for (const seg of segments) { + if (segId ? seg.id === segId : selectedSegmentIds.includes(seg.id)) + seg.enabled = enable } - } - setAllSegments([...allSegments]) - } - else { - notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) - } - } + setSegments([...segments]) + }, + onError: () => { + notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + }, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [datasetId, documentId, selectedSegmentIds, segments]) - const onDelete = async (segId: string) => { - const [e] = await asyncRunSafe(deleteSegment({ datasetId, documentId, segmentId: segId }) as Promise) - if (!e) { - notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - resetList() - } - else { - notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) - } - } + const { mutateAsync: deleteSegment } = useDeleteSegment() - const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => { + const onDelete = useCallback(async (segId?: string) => { + await deleteSegment({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, { + onSuccess: () => { + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + resetList() + !segId && setSelectedSegmentIds([]) + }, + onError: () => { + notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + }, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [datasetId, documentId, selectedSegmentIds]) + + const { mutateAsync: updateSegment } = useUpdateSegment() + + const handleUpdateSegment = useCallback(async ( + segmentId: string, + question: string, + answer: string, + keywords: string[], + needRegenerate = false, + ) => { const params: SegmentUpdater = { content: '' } - if (docForm === 'qa_model') { + if (docForm === ChuckingMode.qa) { if (!question.trim()) return notify({ type: 'error', message: t('datasetDocuments.segment.questionEmpty') }) if (!answer.trim()) @@ -353,45 +273,246 @@ const Completed: FC = ({ if (keywords.length) params.keywords = keywords - try { - eventEmitter?.emit('update-segment') - const res = await updateSegment({ datasetId, documentId, segmentId, body: params }) - notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - onCloseModal() - for (const item of allSegments) { - for (const seg of item) { + if (needRegenerate) + params.regenerate_child_chunks = needRegenerate + + eventEmitter?.emit('update-segment') + await updateSegment({ datasetId, documentId, segmentId, body: params }, { + onSuccess(data) { + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + if (!needRegenerate) + onCloseSegmentDetail() + for (const seg of segments) { if (seg.id === segmentId) { - seg.answer = res.data.answer - seg.content = res.data.content - seg.keywords = res.data.keywords - seg.word_count = res.data.word_count - seg.hit_count = res.data.hit_count - seg.index_node_hash = res.data.index_node_hash - seg.enabled = res.data.enabled + seg.answer = data.data.answer + seg.content = data.data.content + seg.keywords = data.data.keywords + seg.word_count = data.data.word_count + seg.hit_count = data.data.hit_count + seg.enabled = data.data.enabled + seg.updated_at = data.data.updated_at + seg.child_chunks = data.data.child_chunks } } - } - setAllSegments([...allSegments]) - } - finally { - eventEmitter?.emit('') - } - } - - useEffect(() => { - if (lastSegmentsRes !== undefined) - getSegments(false) - }, [selectedStatus, searchValue]) + setSegments([...segments]) + eventEmitter?.emit('update-segment-success') + }, + onSettled() { + eventEmitter?.emit('update-segment-done') + }, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segments, datasetId, documentId]) useEffect(() => { if (importStatus === ProcessStatus.COMPLETED) resetList() - }, [importStatus]) + }, [importStatus, resetList]) + + const onCancelBatchOperation = useCallback(() => { + setSelectedSegmentIds([]) + }, []) + + const onSelected = useCallback((segId: string) => { + setSelectedSegmentIds(prev => + prev.includes(segId) + ? prev.filter(id => id !== segId) + : [...prev, segId], + ) + }, []) + + const isAllSelected = useMemo(() => { + return segments.length > 0 && segments.every(seg => selectedSegmentIds.includes(seg.id)) + }, [segments, selectedSegmentIds]) + + const isSomeSelected = useMemo(() => { + return segments.some(seg => selectedSegmentIds.includes(seg.id)) + }, [segments, selectedSegmentIds]) + + const onSelectedAll = useCallback(() => { + setSelectedSegmentIds((prev) => { + const currentAllSegIds = segments.map(seg => seg.id) + const prevSelectedIds = prev.filter(item => !currentAllSegIds.includes(item)) + return [...prevSelectedIds, ...((isAllSelected || selectedSegmentIds.length > 0) ? [] : currentAllSegIds)] + }) + }, [segments, isAllSelected, selectedSegmentIds]) + + const totalText = useMemo(() => { + const isSearch = searchValue !== '' || selectedStatus !== 'all' + if (!isSearch) { + const total = segmentListData?.total ? formatNumber(segmentListData.total) : '--' + const count = total === '--' ? 0 : segmentListData!.total + const translationKey = (mode === 'hierarchical' && parentMode === 'paragraph') + ? 'datasetDocuments.segment.parentChunks' + : 'datasetDocuments.segment.chunks' + return `${total} ${t(translationKey, { count })}` + } + else { + const total = typeof segmentListData?.total === 'number' ? formatNumber(segmentListData.total) : 0 + const count = segmentListData?.total || 0 + return `${total} ${t('datasetDocuments.segment.searchResults', { count })}` + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segmentListData?.total, mode, parentMode, searchValue, selectedStatus]) + + const toggleFullScreen = useCallback(() => { + setFullScreen(!fullScreen) + }, [fullScreen]) + + const viewNewlyAddedChunk = useCallback(async () => { + const totalPages = segmentListData?.total_pages || 0 + const total = segmentListData?.total || 0 + const newPage = Math.ceil((total + 1) / limit) + needScrollToBottom.current = true + if (newPage > totalPages) { + setCurrentPage(totalPages + 1) + } + else { + resetList() + currentPage !== totalPages && setCurrentPage(totalPages) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segmentListData, limit, currentPage]) + + const { mutateAsync: deleteChildSegment } = useDeleteChildSegment() + + const onDeleteChildChunk = useCallback(async (segmentId: string, childChunkId: string) => { + await deleteChildSegment( + { datasetId, documentId, segmentId, childChunkId }, + { + onSuccess: () => { + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + if (parentMode === 'paragraph') + resetList() + else + resetChildList() + }, + onError: () => { + notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + }, + }, + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [datasetId, documentId, parentMode]) + + const handleAddNewChildChunk = useCallback((parentChunkId: string) => { + setShowNewChildSegmentModal(true) + setCurrChunkId(parentChunkId) + }, []) + + const onSaveNewChildChunk = useCallback((newChildChunk?: ChildChunkDetail) => { + if (parentMode === 'paragraph') { + for (const seg of segments) { + if (seg.id === currChunkId) + seg.child_chunks?.push(newChildChunk!) + } + setSegments([...segments]) + } + else { + resetChildList() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [parentMode, currChunkId, segments]) + + const viewNewlyAddedChildChunk = useCallback(() => { + const totalPages = childChunkListData?.total_pages || 0 + const total = childChunkListData?.total || 0 + const newPage = Math.ceil((total + 1) / limit) + needScrollToBottom.current = true + if (newPage > totalPages) { + setCurrentPage(totalPages + 1) + } + else { + resetChildList() + currentPage !== totalPages && setCurrentPage(totalPages) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childChunkListData, limit, currentPage]) + + const onClickSlice = useCallback((detail: ChildChunkDetail) => { + setCurrChildChunk({ childChunkInfo: detail, showModal: true }) + setCurrChunkId(detail.segment_id) + }, []) + + const onCloseChildSegmentDetail = useCallback(() => { + setCurrChildChunk({ ...currChildChunk, showModal: false }) + setFullScreen(false) + }, [currChildChunk]) + + const { mutateAsync: updateChildSegment } = useUpdateChildSegment() + + const handleUpdateChildChunk = useCallback(async ( + segmentId: string, + childChunkId: string, + content: string, + ) => { + const params: SegmentUpdater = { content: '' } + if (!content.trim()) + return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') }) + + params.content = content + + try { + eventEmitter?.emit('update-child-segment') + const res = await updateChildSegment({ datasetId, documentId, segmentId, childChunkId, body: params }) + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + onCloseChildSegmentDetail() + if (parentMode === 'paragraph') { + for (const seg of segments) { + if (seg.id === segmentId) { + for (const childSeg of seg.child_chunks!) { + if (childSeg.id === childChunkId) { + childSeg.content = res.data.content + childSeg.type = res.data.type + childSeg.word_count = res.data.word_count + childSeg.updated_at = res.data.updated_at + } + } + } + } + setSegments([...segments]) + } + else { + for (const childSeg of childSegments) { + if (childSeg.id === childChunkId) { + childSeg.content = res.data.content + childSeg.type = res.data.type + childSeg.word_count = res.data.word_count + childSeg.updated_at = res.data.updated_at + } + } + setChildSegments([...childSegments]) + } + } + finally { + eventEmitter?.emit('update-child-segment-done') + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segments, childSegments, datasetId, documentId, parentMode]) + + const onClearFilter = useCallback(() => { + setInputValue('') + setSearchValue('') + setSelectedStatus('all') + setCurrentPage(1) + }, []) return ( - <> -
-
{total ? formatNumber(total) : '--'} {t('datasetDocuments.segment.paragraphs')}
+ + {/* Menu Bar */} + {!isFullDocMode &&
+ +
{totalText}
= ({ ]} defaultValue={'all'} className={s.select} - wrapperClassName='h-fit w-[120px] mr-2' /> + wrapperClassName='h-fit w-[100px] mr-2' /> = ({ onChange={e => handleInputChange(e.target.value)} onClear={() => handleInputChange('')} /> -
- + setIsCollapsed(!isCollapsed)} /> +
} + {/* Segment list */} + { + isFullDocMode + ?
+ onClickCard(segments[0])} + loading={isLoadingSegmentList} + /> + +
+ : + } + {/* Pagination */} + + setCurrentPage(cur + 1)} + total={(isFullDocMode ? childChunkListData?.total : segmentListData?.total) || 0} + limit={limit} + onLimitChange={limit => setLimit(limit)} + className={isFullDocMode ? 'px-3' : ''} /> - { }} className='!max-w-[640px] !overflow-visible'> + {/* Edit or view segment detail */} + - - onNewSegmentModalChange(false)} - onSave={resetList} - /> - + + {/* Create New Segment */} + + { + onNewSegmentModalChange(false) + setFullScreen(false) + }} + onSave={resetList} + viewNewlyAddedChunk={viewNewlyAddedChunk} + /> + + {/* Edit or view child segment detail */} + + + + {/* Create New Child Segment */} + + { + setShowNewChildSegmentModal(false) + setFullScreen(false) + }} + onSave={onSaveNewChildChunk} + viewNewlyAddedChildChunk={viewNewlyAddedChildChunk} + /> + + {/* Batch Action Buttons */} + {selectedSegmentIds.length > 0 + && } + ) } diff --git a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx new file mode 100644 index 0000000000..d9d23b421a --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx @@ -0,0 +1,170 @@ +import { memo, useMemo, useRef, useState } from 'react' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import { useParams } from 'next/navigation' +import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' +import { useShallow } from 'zustand/react/shallow' +import { useDocumentContext } from '../index' +import { SegmentIndexTag } from './common/segment-index-tag' +import ActionButtons from './common/action-buttons' +import ChunkContent from './common/chunk-content' +import AddAnother from './common/add-another' +import Dot from './common/dot' +import { useSegmentListContext } from './index' +import { useStore as useAppStore } from '@/app/components/app/store' +import { ToastContext } from '@/app/components/base/toast' +import { type ChildChunkDetail, ChuckingMode, type SegmentUpdater } from '@/models/datasets' +import classNames from '@/utils/classnames' +import { formatNumber } from '@/utils/format' +import Divider from '@/app/components/base/divider' +import { useAddChildSegment } from '@/service/knowledge/use-segment' + +type NewChildSegmentModalProps = { + chunkId: string + onCancel: () => void + onSave: (ChildChunk?: ChildChunkDetail) => void + viewNewlyAddedChildChunk?: () => void +} + +const NewChildSegmentModal: FC = ({ + chunkId, + onCancel, + onSave, + viewNewlyAddedChildChunk, +}) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const [content, setContent] = useState('') + const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>() + const [loading, setLoading] = useState(false) + const [addAnother, setAddAnother] = useState(true) + const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + const { appSidebarExpand } = useAppStore(useShallow(state => ({ + appSidebarExpand: state.appSidebarExpand, + }))) + const parentMode = useDocumentContext(s => s.parentMode) + + const refreshTimer = useRef(null) + + const isFullDocMode = useMemo(() => { + return parentMode === 'full-doc' + }, [parentMode]) + + const CustomButton = <> + + + + + const handleCancel = (actionType: 'esc' | 'add' = 'esc') => { + if (actionType === 'esc' || !addAnother) + onCancel() + setContent('') + } + + const { mutateAsync: addChildSegment } = useAddChildSegment() + + const handleSave = async () => { + const params: SegmentUpdater = { content: '' } + + if (!content.trim()) + return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') }) + + params.content = content + + setLoading(true) + try { + const res = await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }) + notify({ + type: 'success', + message: t('datasetDocuments.segment.childChunkAdded'), + className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} + !top-auto !right-auto !mb-[52px] !ml-11`, + customComponent: isFullDocMode && CustomButton, + }) + handleCancel('add') + if (isFullDocMode) { + refreshTimer.current = setTimeout(() => { + onSave() + }, 3000) + } + else { + onSave(res.data) + } + } + finally { + setLoading(false) + } + } + + const wordCountText = useMemo(() => { + const count = content.length + return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [content.length]) + + return ( +
+
+
+
{t('datasetDocuments.segment.addChildChunk')}
+
+ + + {wordCountText} +
+
+
+ {fullScreen && ( + <> + setAddAnother(!addAnother)} /> + + + + )} +
+ +
+
+ +
+
+
+
+
+ setContent(content)} + isEditMode={true} + /> +
+
+ {!fullScreen && ( +
+ setAddAnother(!addAnother)} /> + +
+ )} +
+ ) +} + +export default memo(NewChildSegmentModal) diff --git a/web/app/components/datasets/documents/detail/completed/segment-card.tsx b/web/app/components/datasets/documents/detail/completed/segment-card.tsx new file mode 100644 index 0000000000..42d92f7599 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/segment-card.tsx @@ -0,0 +1,256 @@ +import React, { type FC, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiDeleteBinLine, RiEditLine } from '@remixicon/react' +import { StatusItem } from '../../list' +import { useDocumentContext } from '../index' +import ChildSegmentList from './child-segment-list' +import Tag from './common/tag' +import Dot from './common/dot' +import { SegmentIndexTag } from './common/segment-index-tag' +import ParentChunkCardSkeleton from './skeleton/parent-chunk-card-skeleton' +import { useSegmentListContext } from './index' +import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets' +import Switch from '@/app/components/base/switch' +import Divider from '@/app/components/base/divider' +import { formatNumber } from '@/utils/format' +import Confirm from '@/app/components/base/confirm' +import cn from '@/utils/classnames' +import Badge from '@/app/components/base/badge' +import { isAfter } from '@/utils/time' +import Tooltip from '@/app/components/base/tooltip' + +type ISegmentCardProps = { + loading: boolean + detail?: SegmentDetailModel & { document?: { name: string } } + onClick?: () => void + onChangeSwitch?: (enabled: boolean, segId?: string) => Promise + onDelete?: (segId: string) => Promise + onDeleteChildChunk?: (segId: string, childChunkId: string) => Promise + handleAddNewChildChunk?: (parentChunkId: string) => void + onClickSlice?: (childChunk: ChildChunkDetail) => void + onClickEdit?: () => void + className?: string + archived?: boolean + embeddingAvailable?: boolean +} + +const SegmentCard: FC = ({ + detail = {}, + onClick, + onChangeSwitch, + onDelete, + onDeleteChildChunk, + handleAddNewChildChunk, + onClickSlice, + onClickEdit, + loading = true, + className = '', + archived, + embeddingAvailable, +}) => { + const { t } = useTranslation() + const { + id, + position, + enabled, + content, + word_count, + hit_count, + answer, + keywords, + child_chunks = [], + created_at, + updated_at, + } = detail as Required['detail'] + const [showModal, setShowModal] = useState(false) + const isCollapsed = useSegmentListContext(s => s.isCollapsed) + const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode]) + + const isGeneralMode = useMemo(() => { + return mode === 'custom' + }, [mode]) + + const isParentChildMode = useMemo(() => { + return mode === 'hierarchical' + }, [mode]) + + const isFullDocMode = useMemo(() => { + return mode === 'hierarchical' && parentMode === 'full-doc' + }, [mode, parentMode]) + + const chunkEdited = useMemo(() => { + if (mode === 'hierarchical' && parentMode === 'full-doc') + return false + return isAfter(updated_at * 1000, created_at * 1000) + }, [mode, parentMode, updated_at, created_at]) + + const textOpacity = useMemo(() => { + return enabled ? '' : 'opacity-50 group-hover/card:opacity-100' + }, [enabled]) + + const handleClickCard = useCallback(() => { + if (mode !== 'hierarchical' || parentMode !== 'full-doc') + onClick?.() + }, [mode, parentMode, onClick]) + + const renderContent = () => { + if (answer) { + return ( + <> +
+
Q
+
+ {content} +
+
+
+
A
+
+ {answer} +
+
+ + ) + } + return content + } + + const wordCountText = useMemo(() => { + const total = formatNumber(word_count) + return `${total} ${t('datasetDocuments.segment.characters', { count: word_count })}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [word_count]) + + const labelPrefix = useMemo(() => { + return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isParentChildMode]) + + if (loading) + return + + return ( +
+
+ <> +
+ + +
{wordCountText}
+ +
{`${formatNumber(hit_count)} ${t('datasetDocuments.segment.hitCount')}`}
+ {chunkEdited && ( + <> + + + + )} +
+ {!isFullDocMode + ?
+ + {embeddingAvailable && ( +
+ {!archived && ( + <> + +
{ + e.stopPropagation() + onClickEdit?.() + }}> + +
+
+ +
{ + e.stopPropagation() + setShowModal(true) + } + }> + +
+
+ + + )} +
) => + e.stopPropagation() + } + className="flex items-center" + > + { + await onChangeSwitch?.(val, id) + }} + /> +
+
+ )} +
+ : null} + +
+
+ {renderContent()} +
+ {isGeneralMode &&
+ {keywords?.map(keyword => )} +
} + { + isFullDocMode + ? + : null + } + { + child_chunks.length > 0 + && + } + {showModal + && { await onDelete?.(id) }} + onCancel={() => setShowModal(false)} + /> + } +
+ ) +} + +export default React.memo(SegmentCard) diff --git a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx new file mode 100644 index 0000000000..0bd8aaa8ea --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx @@ -0,0 +1,184 @@ +import React, { type FC, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, + RiExpandDiagonalLine, +} from '@remixicon/react' +import { useDocumentContext } from '../index' +import ActionButtons from './common/action-buttons' +import ChunkContent from './common/chunk-content' +import Keywords from './common/keywords' +import RegenerationModal from './common/regeneration-modal' +import { SegmentIndexTag } from './common/segment-index-tag' +import Dot from './common/dot' +import { useSegmentListContext } from './index' +import { ChuckingMode, type SegmentDetailModel } from '@/models/datasets' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { formatNumber } from '@/utils/format' +import classNames from '@/utils/classnames' +import Divider from '@/app/components/base/divider' + +type ISegmentDetailProps = { + segInfo?: Partial & { id: string } + onUpdate: (segmentId: string, q: string, a: string, k: string[], needRegenerate?: boolean) => void + onCancel: () => void + isEditMode?: boolean + docForm: ChuckingMode +} + +/** + * Show all the contents of the segment + */ +const SegmentDetail: FC = ({ + segInfo, + onUpdate, + onCancel, + isEditMode, + docForm, +}) => { + const { t } = useTranslation() + const [question, setQuestion] = useState(segInfo?.content || '') + const [answer, setAnswer] = useState(segInfo?.answer || '') + const [keywords, setKeywords] = useState(segInfo?.keywords || []) + const { eventEmitter } = useEventEmitterContextContext() + const [loading, setLoading] = useState(false) + const [showRegenerationModal, setShowRegenerationModal] = useState(false) + const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + const mode = useDocumentContext(s => s.mode) + + eventEmitter?.useSubscription((v) => { + if (v === 'update-segment') + setLoading(true) + if (v === 'update-segment-done') + setLoading(false) + }) + + const handleCancel = () => { + onCancel() + setQuestion(segInfo?.content || '') + setAnswer(segInfo?.answer || '') + setKeywords(segInfo?.keywords || []) + } + + const handleSave = () => { + onUpdate(segInfo?.id || '', question, answer, keywords) + } + + const handleRegeneration = () => { + setShowRegenerationModal(true) + } + + const onCancelRegeneration = () => { + setShowRegenerationModal(false) + } + + const onConfirmRegeneration = () => { + onUpdate(segInfo?.id || '', question, answer, keywords, true) + } + + const isParentChildMode = useMemo(() => { + return mode === 'hierarchical' + }, [mode]) + + const titleText = useMemo(() => { + return isEditMode ? t('datasetDocuments.segment.editChunk') : t('datasetDocuments.segment.chunkDetail') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEditMode]) + + const isQAModel = useMemo(() => { + return docForm === ChuckingMode.qa + }, [docForm]) + + const wordCountText = useMemo(() => { + const contentLength = isQAModel ? (question.length + answer.length) : question.length + const total = formatNumber(isEditMode ? contentLength : segInfo!.word_count as number) + const count = isEditMode ? contentLength : segInfo!.word_count as number + return `${total} ${t('datasetDocuments.segment.characters', { count })}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEditMode, question.length, answer.length, segInfo?.word_count, isQAModel]) + + const labelPrefix = useMemo(() => { + return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isParentChildMode]) + + return ( +
+
+
+
{titleText}
+
+ + + {wordCountText} +
+
+
+ {isEditMode && fullScreen && ( + <> + + + + )} +
+ +
+
+ +
+
+
+
+
+ setQuestion(question)} + onAnswerChange={answer => setAnswer(answer)} + isEditMode={isEditMode} + /> +
+ {mode === 'custom' && setKeywords(keywords)} + />} +
+ {isEditMode && !fullScreen && ( +
+ +
+ )} + { + showRegenerationModal && ( + + ) + } +
+ ) +} + +export default React.memo(SegmentDetail) diff --git a/web/app/components/datasets/documents/detail/completed/segment-list.tsx b/web/app/components/datasets/documents/detail/completed/segment-list.tsx new file mode 100644 index 0000000000..5604086ccb --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/segment-list.tsx @@ -0,0 +1,103 @@ +import React, { type ForwardedRef, useMemo } from 'react' +import { useDocumentContext } from '../index' +import SegmentCard from './segment-card' +import Empty from './common/empty' +import GeneralListSkeleton from './skeleton/general-list-skeleton' +import ParagraphListSkeleton from './skeleton/paragraph-list-skeleton' +import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets' +import Checkbox from '@/app/components/base/checkbox' +import Divider from '@/app/components/base/divider' + +type ISegmentListProps = { + isLoading: boolean + items: SegmentDetailModel[] + selectedSegmentIds: string[] + onSelected: (segId: string) => void + onClick: (detail: SegmentDetailModel, isEditMode?: boolean) => void + onChangeSwitch: (enabled: boolean, segId?: string,) => Promise + onDelete: (segId: string) => Promise + onDeleteChildChunk: (sgId: string, childChunkId: string) => Promise + handleAddNewChildChunk: (parentChunkId: string) => void + onClickSlice: (childChunk: ChildChunkDetail) => void + archived?: boolean + embeddingAvailable: boolean + onClearFilter: () => void +} + +const SegmentList = React.forwardRef(({ + isLoading, + items, + selectedSegmentIds, + onSelected, + onClick: onClickCard, + onChangeSwitch, + onDelete, + onDeleteChildChunk, + handleAddNewChildChunk, + onClickSlice, + archived, + embeddingAvailable, + onClearFilter, +}: ISegmentListProps, +ref: ForwardedRef, +) => { + const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode]) + + const Skeleton = useMemo(() => { + return (mode === 'hierarchical' && parentMode === 'paragraph') ? ParagraphListSkeleton : GeneralListSkeleton + }, [mode, parentMode]) + + // Loading skeleton + if (isLoading) + return + // Search result is empty + if (items.length === 0) { + return ( +
+ +
+ ) + } + return ( +
+ { + items.map((segItem) => { + const isLast = items[items.length - 1].id === segItem.id + return ( +
+ onSelected(segItem.id)} + /> +
+ onClickCard(segItem, true)} + onChangeSwitch={onChangeSwitch} + onClickEdit={() => onClickCard(segItem, true)} + onDelete={onDelete} + onDeleteChildChunk={onDeleteChildChunk} + handleAddNewChildChunk={handleAddNewChildChunk} + onClickSlice={onClickSlice} + loading={false} + archived={archived} + embeddingAvailable={embeddingAvailable} + /> + {!isLast &&
+ +
} +
+
+ ) + }) + } +
+ ) +}) + +SegmentList.displayName = 'SegmentList' + +export default SegmentList diff --git a/web/app/components/datasets/documents/detail/completed/skeleton/full-doc-list-skeleton.tsx b/web/app/components/datasets/documents/detail/completed/skeleton/full-doc-list-skeleton.tsx new file mode 100644 index 0000000000..b09e72c6f9 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/skeleton/full-doc-list-skeleton.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +const Slice = React.memo(() => { + return ( +
+
+ +
+
+
+ ) +}) + +Slice.displayName = 'Slice' + +const FullDocListSkeleton = () => { + return ( +
+
+ {[...Array.from({ length: 15 })].map((_, index) => )} +
+ ) +} + +export default React.memo(FullDocListSkeleton) diff --git a/web/app/components/datasets/documents/detail/completed/skeleton/general-list-skeleton.tsx b/web/app/components/datasets/documents/detail/completed/skeleton/general-list-skeleton.tsx new file mode 100644 index 0000000000..5cbe285e41 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/skeleton/general-list-skeleton.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { + SkeletonContanier, + SkeletonPoint, + SkeletonRectangle, + SkeletonRow, +} from '@/app/components/base/skeleton' +import Checkbox from '@/app/components/base/checkbox' +import Divider from '@/app/components/base/divider' + +const CardSkelton = React.memo(() => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +}) + +CardSkelton.displayName = 'CardSkelton' + +const GeneralListSkeleton = () => { + return ( +
+
+ {[...Array.from({ length: 10 })].map((_, index) => { + return ( +
+ +
+ + {index !== 9 &&
+ +
} +
+
+ ) + })} +
+ ) +} + +export default React.memo(GeneralListSkeleton) diff --git a/web/app/components/datasets/documents/detail/completed/skeleton/paragraph-list-skeleton.tsx b/web/app/components/datasets/documents/detail/completed/skeleton/paragraph-list-skeleton.tsx new file mode 100644 index 0000000000..eaa7126510 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/skeleton/paragraph-list-skeleton.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import { RiArrowRightSLine } from '@remixicon/react' +import { + SkeletonContainer, + SkeletonPoint, + SkeletonRectangle, + SkeletonRow, +} from '@/app/components/base/skeleton' +import Checkbox from '@/app/components/base/checkbox' +import Divider from '@/app/components/base/divider' + +const CardSkelton = React.memo(() => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +}) + +CardSkelton.displayName = 'CardSkelton' + +const ParagraphListSkeleton = () => { + return ( +
+
+ {[...Array.from({ length: 10 })].map((_, index) => { + return ( +
+ +
+ + {index !== 9 &&
+ +
} +
+
+ ) + })} +
+ ) +} + +export default React.memo(ParagraphListSkeleton) diff --git a/web/app/components/datasets/documents/detail/completed/skeleton/parent-chunk-card-skeleton.tsx b/web/app/components/datasets/documents/detail/completed/skeleton/parent-chunk-card-skeleton.tsx new file mode 100644 index 0000000000..edad7575b2 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/skeleton/parent-chunk-card-skeleton.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + SkeletonContainer, + SkeletonPoint, + SkeletonRectangle, + SkeletonRow, +} from '@/app/components/base/skeleton' + +const ParentChunkCardSkelton = () => { + const { t } = useTranslation() + return ( +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+ ) +} + +ParentChunkCardSkelton.displayName = 'ParentChunkCardSkelton' + +export default React.memo(ParentChunkCardSkelton) diff --git a/web/app/components/datasets/documents/detail/completed/style.module.css b/web/app/components/datasets/documents/detail/completed/style.module.css index 7633d53209..e9bab39782 100644 --- a/web/app/components/datasets/documents/detail/completed/style.module.css +++ b/web/app/components/datasets/documents/detail/completed/style.module.css @@ -5,10 +5,10 @@ grid-auto-rows: 180px; } */ .totalText { - @apply text-gray-900 font-medium text-base flex-1; + @apply text-text-secondary flex-1; } .docSearchWrapper { - @apply sticky w-full py-1 -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1; + @apply sticky w-full -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1 pr-3; } .listContainer { height: calc(100% - 3.25rem); diff --git a/web/app/components/datasets/documents/detail/embedding/index.tsx b/web/app/components/datasets/documents/detail/embedding/index.tsx index 1bc6c91c2a..30590b39f1 100644 --- a/web/app/components/datasets/documents/detail/embedding/index.tsx +++ b/web/app/components/datasets/documents/detail/embedding/index.tsx @@ -9,7 +9,7 @@ import { ArrowRightIcon } from '@heroicons/react/24/solid' import SegmentCard from '../completed/SegmentCard' import { FieldInfo } from '../metadata' import style from '../completed/style.module.css' -import { DocumentContext } from '../index' +import { useDocumentContext } from '../index' import s from './style.module.css' import cn from '@/utils/classnames' import Button from '@/app/components/base/button' @@ -111,7 +111,7 @@ const EmbeddingDetail: FC = ({ detail, stopPosition = 'top', datasetId: d const { t } = useTranslation() const { notify } = useContext(ToastContext) - const { datasetId = '', documentId = '' } = useContext(DocumentContext) + const [datasetId, documentId] = useDocumentContext(s => [s.datasetId, s.documentId]) const localDatasetId = dstId ?? datasetId const localDocumentId = docId ?? documentId @@ -221,7 +221,7 @@ const EmbeddingDetail: FC = ({ detail, stopPosition = 'top', datasetId: d
{/* progress bar */}
- {new Array(10).fill('').map((_, idx) =>
)} diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 4f1e850fc8..aa15f4a487 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -1,14 +1,14 @@ 'use client' import type { FC } from 'react' -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import useSWR from 'swr' -import { ArrowLeftIcon } from '@heroicons/react/24/solid' -import { createContext, useContext } from 'use-context-selector' +import { createContext, useContext, useContextSelector } from 'use-context-selector' import { useTranslation } from 'react-i18next' import { useRouter } from 'next/navigation' import { omit } from 'lodash-es' +import { RiArrowLeftLine, RiLayoutRight2Line } from '@remixicon/react' import { OperationAction, StatusItem } from '../list' -import s from '../style.module.css' +import DocumentPicker from '../../common/document-picker' import Completed from './completed' import Embedding from './embedding' import Metadata from './metadata' @@ -21,27 +21,55 @@ import Loading from '@/app/components/base/loading' import type { MetadataType } from '@/service/datasets' import { checkSegmentBatchImportProgress, fetchDocumentDetail, segmentBatchImport } from '@/service/datasets' import { ToastContext } from '@/app/components/base/toast' -import type { DocForm } from '@/models/datasets' +import type { DocForm, ParentMode, ProcessMode } from '@/models/datasets' import { useDatasetDetailContext } from '@/context/dataset-detail' import FloatRightContainer from '@/app/components/base/float-right-container' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { LayoutRight2LineMod } from '@/app/components/base/icons/src/public/knowledge' -export const DocumentContext = createContext<{ datasetId?: string; documentId?: string; docForm: string }>({ docForm: '' }) +type DocumentContextValue = { + datasetId?: string + documentId?: string + docForm: string + mode?: ProcessMode + parentMode?: ParentMode +} + +export const DocumentContext = createContext({ docForm: '' }) + +export const useDocumentContext = (selector: (value: DocumentContextValue) => any) => { + return useContextSelector(DocumentContext, selector) +} type DocumentTitleProps = { + datasetId: string extension?: string name?: string + processMode?: ProcessMode + parent_mode?: ParentMode iconCls?: string textCls?: string wrapperCls?: string } -export const DocumentTitle: FC = ({ extension, name, iconCls, textCls, wrapperCls }) => { - const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase() - return
-
- {name || '--'} -
+export const DocumentTitle: FC = ({ datasetId, extension, name, processMode, parent_mode, wrapperCls }) => { + const router = useRouter() + return ( +
+ { + router.push(`/datasets/${datasetId}/documents/${doc.id}`) + }} + /> +
+ ) } type Props = { @@ -127,25 +155,64 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { detailMutate() } + const mode = useMemo(() => { + return documentDetail?.dataset_process_rule?.mode + }, [documentDetail?.dataset_process_rule]) + + const parentMode = useMemo(() => { + return documentDetail?.dataset_process_rule?.rules?.parent_mode + }, [documentDetail?.dataset_process_rule]) + + const isFullDocMode = useMemo(() => { + return mode === 'hierarchical' && parentMode === 'full-doc' + }, [mode, parentMode]) + return ( - +
-
-
- +
+
+
- - -
- - {embeddingAvailable && documentDetail && !documentDetail.archived && ( - + +
+ {embeddingAvailable && documentDetail && !documentDetail.archived && !isFullDocMode && ( + <> + + + )} + = ({ datasetId, documentId }) => { }} datasetId={datasetId} onUpdate={handleOperate} - className='!w-[216px]' + className='!w-[200px]' />
{isDetailLoading ? - :
+ :
{embedding ? : { type IFieldInfoProps = { label: string value?: string + valueIcon?: ReactNode displayedValue?: string defaultValue?: string showEdit?: boolean @@ -43,6 +44,7 @@ type IFieldInfoProps = { export const FieldInfo: FC = ({ label, value = '', + valueIcon, displayedValue = '', defaultValue, showEdit = false, @@ -56,9 +58,10 @@ export const FieldInfo: FC = ({ const readAlignTop = !showEdit && textNeedWrap return ( -
-
{label}
-
+
+
{label}
+
+ {valueIcon} {!showEdit ? displayedValue : inputType === 'select' @@ -147,7 +150,7 @@ const Metadata: FC = ({ docDetail, loading, onUpdate }) => { const [saveLoading, setSaveLoading] = useState(false) const { notify } = useContext(ToastContext) - const { datasetId = '', documentId = '' } = useContext(DocumentContext) + const [datasetId, documentId] = useDocumentContext(s => [s.datasetId, s.documentId]) useEffect(() => { if (docDetail?.doc_type) { @@ -348,7 +351,7 @@ const Metadata: FC = ({ docDetail, loading, onUpdate }) => { ·
{ setShowDocTypes(true) }} - className='cursor-pointer hover:text-[#155EEF]' + className='cursor-pointer hover:text-text-accent' > {t('common.operation.change')}
diff --git a/web/app/components/datasets/documents/detail/metadata/style.module.css b/web/app/components/datasets/documents/detail/metadata/style.module.css index 60420c196e..37796d38dc 100644 --- a/web/app/components/datasets/documents/detail/metadata/style.module.css +++ b/web/app/components/datasets/documents/detail/metadata/style.module.css @@ -53,18 +53,7 @@ .desc { @apply text-gray-500 text-xs; } -.fieldInfo { - /* height: 1.75rem; */ - min-height: 1.75rem; - @apply flex flex-row items-center gap-4; -} -.fieldInfo > .label { - @apply w-2/5 max-w-[128px] text-gray-500 text-xs font-medium overflow-hidden text-ellipsis whitespace-nowrap; -} -.fieldInfo > .value { - overflow-wrap: anywhere; - @apply w-3/5 text-gray-700 font-normal text-xs; -} + .changeTip { @apply text-[#D92D20] text-xs text-center; } diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx new file mode 100644 index 0000000000..666a1caf85 --- /dev/null +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -0,0 +1,204 @@ +import { memo, useMemo, useRef, useState } from 'react' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import { useParams } from 'next/navigation' +import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' +import { useShallow } from 'zustand/react/shallow' +import { useSegmentListContext } from './completed' +import { SegmentIndexTag } from './completed/common/segment-index-tag' +import ActionButtons from './completed/common/action-buttons' +import Keywords from './completed/common/keywords' +import ChunkContent from './completed/common/chunk-content' +import AddAnother from './completed/common/add-another' +import Dot from './completed/common/dot' +import { useDocumentContext } from './index' +import { useStore as useAppStore } from '@/app/components/app/store' +import { ToastContext } from '@/app/components/base/toast' +import { ChuckingMode, type SegmentUpdater } from '@/models/datasets' +import classNames from '@/utils/classnames' +import { formatNumber } from '@/utils/format' +import Divider from '@/app/components/base/divider' +import { useAddSegment } from '@/service/knowledge/use-segment' + +type NewSegmentModalProps = { + onCancel: () => void + docForm: ChuckingMode + onSave: () => void + viewNewlyAddedChunk: () => void +} + +const NewSegmentModal: FC = ({ + onCancel, + docForm, + onSave, + viewNewlyAddedChunk, +}) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const [question, setQuestion] = useState('') + const [answer, setAnswer] = useState('') + const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>() + const [keywords, setKeywords] = useState([]) + const [loading, setLoading] = useState(false) + const [addAnother, setAddAnother] = useState(true) + const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + const mode = useDocumentContext(s => s.mode) + const { appSidebarExpand } = useAppStore(useShallow(state => ({ + appSidebarExpand: state.appSidebarExpand, + }))) + const refreshTimer = useRef(null) + + const CustomButton = <> + + + + + const isQAModel = useMemo(() => { + return docForm === ChuckingMode.qa + }, [docForm]) + + const handleCancel = (actionType: 'esc' | 'add' = 'esc') => { + if (actionType === 'esc' || !addAnother) + onCancel() + setQuestion('') + setAnswer('') + setKeywords([]) + } + + const { mutateAsync: addSegment } = useAddSegment() + + const handleSave = async () => { + const params: SegmentUpdater = { content: '' } + if (isQAModel) { + if (!question.trim()) { + return notify({ + type: 'error', + message: t('datasetDocuments.segment.questionEmpty'), + }) + } + if (!answer.trim()) { + return notify({ + type: 'error', + message: t('datasetDocuments.segment.answerEmpty'), + }) + } + + params.content = question + params.answer = answer + } + else { + if (!question.trim()) { + return notify({ + type: 'error', + message: t('datasetDocuments.segment.contentEmpty'), + }) + } + + params.content = question + } + + if (keywords?.length) + params.keywords = keywords + + setLoading(true) + await addSegment({ datasetId, documentId, body: params }, { + onSuccess() { + notify({ + type: 'success', + message: t('datasetDocuments.segment.chunkAdded'), + className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} + !top-auto !right-auto !mb-[52px] !ml-11`, + customComponent: CustomButton, + }) + handleCancel('add') + refreshTimer.current = setTimeout(() => { + onSave() + }, 3000) + }, + onSettled() { + setLoading(false) + }, + }) + } + + const wordCountText = useMemo(() => { + const count = isQAModel ? (question.length + answer.length) : question.length + return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [question.length, answer.length, isQAModel]) + + return ( +
+
+
+
{ + t('datasetDocuments.segment.addChunk') + }
+
+ + + {wordCountText} +
+
+
+ {fullScreen && ( + <> + setAddAnother(!addAnother)} /> + + + + )} +
+ +
+
+ +
+
+
+
+
+ setQuestion(question)} + onAnswerChange={answer => setAnswer(answer)} + isEditMode={true} + /> +
+ {mode === 'custom' && setKeywords(keywords)} + />} +
+ {!fullScreen && ( +
+ setAddAnother(!addAnother)} /> + +
+ )} +
+ ) +} + +export default memo(NewSegmentModal) diff --git a/web/app/components/datasets/documents/detail/segment-add/index.tsx b/web/app/components/datasets/documents/detail/segment-add/index.tsx index e69f3e9ab0..17ddc86f74 100644 --- a/web/app/components/datasets/documents/detail/segment-add/index.tsx +++ b/web/app/components/datasets/documents/detail/segment-add/index.tsx @@ -3,11 +3,12 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' import { + RiAddLine, + RiArrowDownSLine, RiErrorWarningFill, RiLoader2Line, } from '@remixicon/react' import cn from '@/utils/classnames' -import { FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general' import Popover from '@/app/components/base/popover' @@ -37,25 +38,36 @@ const SegmentAdd: FC = ({ return ( <> {(importStatus === ProcessStatus.WAITING || importStatus === ProcessStatus.PROCESSING) && ( -
- {importStatus === ProcessStatus.WAITING &&
} - {importStatus === ProcessStatus.PROCESSING &&
} - - {t('datasetDocuments.list.batchModal.processing')} +
+
+ + {t('datasetDocuments.list.batchModal.processing')}
)} {importStatus === ProcessStatus.COMPLETED && ( -
- - {t('datasetDocuments.list.batchModal.completed')} - {t('datasetDocuments.list.batchModal.ok')} +
+
+ + {t('datasetDocuments.list.batchModal.completed')} +
+
+ {t('datasetDocuments.list.batchModal.ok')} +
+
)} {importStatus === ProcessStatus.ERROR && ( -
- - {t('datasetDocuments.list.batchModal.error')} - {t('datasetDocuments.list.batchModal.ok')} +
+
+ + {t('datasetDocuments.list.batchModal.error')} +
+
+ {t('datasetDocuments.list.batchModal.ok')} +
+
)} @@ -63,24 +75,43 @@ const SegmentAdd: FC = ({ } return ( - -
{t('datasetDocuments.list.action.add')}
-
{t('datasetDocuments.list.action.batchAdd')}
-
- } - btnElement={ -
- - {t('datasetDocuments.list.action.addButton')} -
- } - btnClassName={open => cn('mr-2 !py-[6px] !text-[13px] !leading-[18px] hover:bg-gray-50 border border-gray-200 hover:border-gray-300 hover:shadow-[0_1px_2px_rgba(16,24,40,0.05)]', open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')} - className='!w-[132px] h-fit !z-20 !translate-x-0 !left-0' - /> +
+
+ + + {t('datasetDocuments.list.action.addButton')} + +
+ +
+ {t('datasetDocuments.list.action.batchAdd')} +
+
+ } + btnElement={ +
+ +
+ } + btnClassName={open => cn('!p-2 !border-0 !rounded-l-none !rounded-r-lg !hover:bg-state-base-hover shadow-xs shadow-shadow-3 backdrop-blur-[5px]', + open ? '!bg-state-base-hover' : '')} + popupClassName='!min-w-[128px] !bg-components-panel-bg-blur !rounded-xl border-[0.5px] !ring-0 + border-components-panel-border !shadow-xl !shadow-shadow-shadow-5 backdrop-blur-[5px]' + className='min-w-[128px] h-fit' + /> +
) } export default React.memo(SegmentAdd) diff --git a/web/app/components/datasets/documents/detail/style.module.css b/web/app/components/datasets/documents/detail/style.module.css index 69295ab31c..8a59ef6f06 100644 --- a/web/app/components/datasets/documents/detail/style.module.css +++ b/web/app/components/datasets/documents/detail/style.module.css @@ -5,11 +5,7 @@ @apply h-6 w-6 !important; } .layoutRightIcon { - @apply w-8 h-8 ml-2 box-border border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer hover:shadow-[0_1px_2px_rgba(16,24,40,0.05)]; -} -.iconShow { - background: center center url(../assets/layoutRightShow.svg) no-repeat; -} -.iconClose { - background: center center url(../assets/layoutRightClose.svg) no-repeat; + @apply p-2 ml-2 border-[0.5px] border-components-button-secondary-border hover:border-components-button-secondary-border-hover + rounded-lg bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover cursor-pointer + shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]; } diff --git a/web/app/components/datasets/documents/index.tsx b/web/app/components/datasets/documents/index.tsx index 7a61d4d580..e7d11097bd 100644 --- a/web/app/components/datasets/documents/index.tsx +++ b/web/app/components/datasets/documents/index.tsx @@ -7,12 +7,13 @@ import { useRouter } from 'next/navigation' import { useDebounce, useDebounceFn } from 'ahooks' import { groupBy, omit } from 'lodash-es' import { PlusIcon } from '@heroicons/react/24/solid' +import { RiExternalLinkLine } from '@remixicon/react' +import AutoDisabledDocument from '../common/document-status-with-action/auto-disabled-document' import List from './list' import s from './style.module.css' import Loading from '@/app/components/base/loading' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Pagination from '@/app/components/base/pagination' import { get } from '@/service/base' import { createDocument, fetchDocuments } from '@/service/datasets' import { useDatasetDetailContext } from '@/context/dataset-detail' @@ -20,10 +21,9 @@ import { NotionPageSelectorModal } from '@/app/components/base/notion-page-selec import type { NotionPage } from '@/models/common' import type { CreateDocumentReq } from '@/models/datasets' import { DataSourceType } from '@/models/datasets' -import RetryButton from '@/app/components/base/retry-button' -// Custom page count is not currently supported. -const limit = 15 - +import IndexFailed from '@/app/components/datasets/common/document-status-with-action/index-failed' +import { useProviderContext } from '@/context/provider-context' +import cn from '@/utils/classnames' const FolderPlusIcon = ({ className }: React.SVGProps) => { return @@ -74,12 +74,16 @@ type IDocumentsProps = { } export const fetcher = (url: string) => get(url, {}, {}) +const DEFAULT_LIMIT = 15 const Documents: FC = ({ datasetId }) => { const { t } = useTranslation() + const { plan } = useProviderContext() + const isFreePlan = plan.type === 'sandbox' const [inputValue, setInputValue] = useState('') // the input value const [searchValue, setSearchValue] = useState('') const [currPage, setCurrPage] = React.useState(0) + const [limit, setLimit] = useState(DEFAULT_LIMIT) const router = useRouter() const { dataset } = useDatasetDetailContext() const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false) @@ -93,7 +97,7 @@ const Documents: FC = ({ datasetId }) => { const query = useMemo(() => { return { page: currPage + 1, limit, keyword: debouncedSearchValue, fetch: isDataSourceNotion ? true : '' } - }, [currPage, debouncedSearchValue, isDataSourceNotion]) + }, [currPage, debouncedSearchValue, isDataSourceNotion, limit]) const { data: documentsRes, error, mutate } = useSWR( { @@ -195,7 +199,7 @@ const Documents: FC = ({ datasetId }) => { } const documentsList = isDataSourceNotion ? documentsWithProgress?.data : documentsRes?.data - + const [selectedIds, setSelectedIds] = useState([]) const { run: handleSearch } = useDebounceFn(() => { setSearchValue(inputValue) }, { wait: 500 }) @@ -208,8 +212,17 @@ const Documents: FC = ({ datasetId }) => { return (
-

{t('datasetDocuments.list.title')}

-

{t('datasetDocuments.list.desc')}

+

{t('datasetDocuments.list.title')}

+
+ {t('datasetDocuments.list.desc')} + + {t('datasetDocuments.list.learnMore')} + + +
@@ -222,13 +235,14 @@ const Documents: FC = ({ datasetId }) => { onClear={() => handleInputChange('')} />
- + {!isFreePlan && } + {embeddingAvailable && ( )}
@@ -236,13 +250,22 @@ const Documents: FC = ({ datasetId }) => { {isLoading ? : total > 0 - ? + ? : } - {/* Show Pagination only if the total is more than the limit */} - {(total && total > limit) - ? - : null} setNotionPageSelectorModalVisible(false)} diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index 0e0eebb034..04f41767c6 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -1,11 +1,15 @@ -/* eslint-disable no-mixed-operators */ 'use client' -import type { FC, SVGProps } from 'react' -import React, { useCallback, useEffect, useState } from 'react' +import type { FC } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useBoolean, useDebounceFn } from 'ahooks' -import { ArrowDownIcon, TrashIcon } from '@heroicons/react/24/outline' -import { pick } from 'lodash-es' +import { ArrowDownIcon } from '@heroicons/react/24/outline' +import { pick, uniq } from 'lodash-es' import { + RiArchive2Line, + RiDeleteBinLine, + RiEditLine, + RiEqualizer2Line, + RiLoopLeftLine, RiMoreFill, } from '@remixicon/react' import { useContext } from 'use-context-selector' @@ -14,49 +18,31 @@ import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' import { Edit03 } from '../../base/icons/src/vender/solid/general' import { Globe01 } from '../../base/icons/src/vender/line/mapsAndTravel' +import ChunkingModeLabel from '../common/chunking-mode-label' import s from './style.module.css' import RenameModal from './rename-modal' +import BatchAction from './detail/completed/common/batch-action' import cn from '@/utils/classnames' import Switch from '@/app/components/base/switch' import Divider from '@/app/components/base/divider' import Popover from '@/app/components/base/popover' import Confirm from '@/app/components/base/confirm' import Tooltip from '@/app/components/base/tooltip' -import { ToastContext } from '@/app/components/base/toast' -import type { IndicatorProps } from '@/app/components/header/indicator' +import Toast, { ToastContext } from '@/app/components/base/toast' +import type { ColorMap, IndicatorProps } from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator' import { asyncRunSafe } from '@/utils' import { formatNumber } from '@/utils/format' -import { archiveDocument, deleteDocument, disableDocument, enableDocument, syncDocument, syncWebsite, unArchiveDocument } from '@/service/datasets' import NotionIcon from '@/app/components/base/notion-icon' import ProgressBar from '@/app/components/base/progress-bar' -import { DataSourceType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets' +import { ChuckingMode, DataSourceType, DocumentActionType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets' import type { CommonResponse } from '@/models/common' import useTimestamp from '@/hooks/use-timestamp' - -export const SettingsIcon = ({ className }: SVGProps) => { - return - - -} - -export const SyncIcon = () => { - return - - -} - -export const FilePlusIcon = ({ className }: SVGProps) => { - return - - -} - -export const ArchiveIcon = ({ className }: SVGProps) => { - return - - -} +import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '@/context/dataset-detail' +import type { Props as PaginationProps } from '@/app/components/base/pagination' +import Pagination from '@/app/components/base/pagination' +import Checkbox from '@/app/components/base/checkbox' +import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document' export const useIndexStatus = () => { const { t } = useTranslation() @@ -72,6 +58,15 @@ export const useIndexStatus = () => { } } +const STATUS_TEXT_COLOR_MAP: ColorMap = { + green: 'text-util-colors-green-green-600', + orange: 'text-util-colors-warning-warning-600', + red: 'text-util-colors-red-red-600', + blue: 'text-util-colors-blue-light-blue-light-600', + yellow: 'text-util-colors-warning-warning-600', + gray: 'text-text-tertiary', +} + // status item for list export const StatusItem: FC<{ status: DocumentDisplayStatus @@ -79,16 +74,78 @@ export const StatusItem: FC<{ scene?: 'list' | 'detail' textCls?: string errorMessage?: string -}> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage }) => { + detail?: { + enabled: boolean + archived: boolean + id: string + } + datasetId?: string + onUpdate?: (operationName?: string) => void + +}> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage, datasetId = '', detail, onUpdate }) => { const DOC_INDEX_STATUS_MAP = useIndexStatus() const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP + const { enabled = false, archived = false, id = '' } = detail || {} + const { notify } = useContext(ToastContext) + const { t } = useTranslation() + const { mutateAsync: enableDocument } = useDocumentEnable() + const { mutateAsync: disableDocument } = useDocumentDisable() + const { mutateAsync: deleteDocument } = useDocumentDelete() + + const onOperate = async (operationName: OperationName) => { + let opApi = deleteDocument + switch (operationName) { + case 'enable': + opApi = enableDocument + break + case 'disable': + opApi = disableDocument + break + } + const [e] = await asyncRunSafe(opApi({ datasetId, documentId: id }) as Promise) + if (!e) { + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + onUpdate?.(operationName) + } + else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } + } + + const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => { + if (operationName === 'enable' && enabled) + return + if (operationName === 'disable' && !enabled) + return + onOperate(operationName) + }, { wait: 500 }) + return
- {DOC_INDEX_STATUS_MAP[localStatus]?.text} + + {DOC_INDEX_STATUS_MAP[localStatus]?.text} + + { + scene === 'detail' && ( +
+ + !archived && handleSwitch(v ? 'enable' : 'disable')} + disabled={archived} + size='md' + /> + +
+ ) + } { errorMessage && ( { @@ -147,10 +210,8 @@ export const OperationAction: FC<{ case 'sync': if (data_source_type === 'notion_import') opApi = syncDocument - else opApi = syncWebsite - break default: opApi = deleteDocument @@ -158,13 +219,13 @@ export const OperationAction: FC<{ break } const [e] = await asyncRunSafe(opApi({ datasetId, documentId: id }) as Promise) - if (!e) + if (!e) { notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - else - notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + onUpdate(operationName) + } + else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } if (operationName === 'delete') setDeleting(false) - onUpdate(operationName) } const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => { @@ -216,85 +277,71 @@ export const OperationAction: FC<{ )} {embeddingAvailable && ( - - {!isListScene && <> -
- - {!archived && enabled ? t('datasetDocuments.list.index.enable') : t('datasetDocuments.list.index.disable')} - - -
- !archived && handleSwitch(v ? 'enable' : 'disable')} - disabled={archived} - size='md' - /> + <> + + + + + {!archived && ( + <> +
{ + handleShowRenameModal({ + id: detail.id, + name: detail.name, + }) + }}> + + {t('datasetDocuments.list.table.rename')}
- -
-
- {!archived && enabled ? t('datasetDocuments.list.index.enableTip') : t('datasetDocuments.list.index.disableTip')} -
- - } - {!archived && ( - <> -
{ - handleShowRenameModal({ - id: detail.id, - name: detail.name, - }) - }}> - - {t('datasetDocuments.list.table.rename')} + {['notion_import', DataSourceType.WEB].includes(data_source_type) && ( +
onOperate('sync')}> + + {t('datasetDocuments.list.action.sync')} +
+ )} + + + )} + {!archived &&
onOperate('archive')}> + + {t('datasetDocuments.list.action.archive')} +
} + {archived && ( +
onOperate('un_archive')}> + + {t('datasetDocuments.list.action.unarchive')}
-
router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}> - - {t('datasetDocuments.list.action.settings')} -
- {['notion_import', DataSourceType.WEB].includes(data_source_type) && ( -
onOperate('sync')}> - - {t('datasetDocuments.list.action.sync')} -
- )} - - - )} - {!archived &&
onOperate('archive')}> - - {t('datasetDocuments.list.action.archive')} -
} - {archived && ( -
onOperate('un_archive')}> - - {t('datasetDocuments.list.action.unarchive')} + )} +
setShowModal(true)}> + + {t('datasetDocuments.list.action.delete')}
- )} -
setShowModal(true)}> - - {t('datasetDocuments.list.action.delete')}
-
- } - trigger='click' - position='br' - btnElement={ -
- -
- } - btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')} - className={`flex justify-end !w-[200px] h-fit !z-20 ${className}`} - /> + } + trigger='click' + position='br' + btnElement={ +
+ +
+ } + btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!hover:bg-state-base-hover !shadow-none' : '!bg-transparent')} + popupClassName='!w-full' + className={`flex justify-end !w-[200px] h-fit !z-20 ${className}`} + /> + )} {showModal && { return ( -
+
{value ?? '-'}
) @@ -343,19 +390,34 @@ type LocalDoc = SimpleDocumentDetail & { percent?: number } type IDocumentListProps = { embeddingAvailable: boolean documents: LocalDoc[] + selectedIds: string[] + onSelectedIdChange: (selectedIds: string[]) => void datasetId: string + pagination: PaginationProps onUpdate: () => void } /** * Document list component including basic information */ -const DocumentList: FC = ({ embeddingAvailable, documents = [], datasetId, onUpdate }) => { +const DocumentList: FC = ({ + embeddingAvailable, + documents = [], + selectedIds, + onSelectedIdChange, + datasetId, + pagination, + onUpdate, +}) => { const { t } = useTranslation() const { formatTime } = useTimestamp() const router = useRouter() + const [datasetConfig] = useDatasetDetailContext(s => [s.dataset]) + const chunkingMode = datasetConfig?.doc_form + const isGeneralMode = chunkingMode !== ChuckingMode.parentChild + const isQAMode = chunkingMode === ChuckingMode.qa const [localDocs, setLocalDocs] = useState(documents) - const [enableSort, setEnableSort] = useState(false) + const [enableSort, setEnableSort] = useState(true) useEffect(() => { setLocalDocs(documents) @@ -363,7 +425,7 @@ const DocumentList: FC = ({ embeddingAvailable, documents = const onClickSort = () => { setEnableSort(!enableSort) - if (!enableSort) { + if (enableSort) { const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1) setLocalDocs(sortedDocs) } @@ -385,40 +447,113 @@ const DocumentList: FC = ({ embeddingAvailable, documents = onUpdate() }, [onUpdate]) + const isAllSelected = useMemo(() => { + return localDocs.length > 0 && localDocs.every(doc => selectedIds.includes(doc.id)) + }, [localDocs, selectedIds]) + + const isSomeSelected = useMemo(() => { + return localDocs.some(doc => selectedIds.includes(doc.id)) + }, [localDocs, selectedIds]) + + const onSelectedAll = useCallback(() => { + if (isAllSelected) + onSelectedIdChange([]) + else + onSelectedIdChange(uniq([...selectedIds, ...localDocs.map(doc => doc.id)])) + }, [isAllSelected, localDocs, onSelectedIdChange, selectedIds]) + const { mutateAsync: archiveDocument } = useDocumentArchive() + const { mutateAsync: enableDocument } = useDocumentEnable() + const { mutateAsync: disableDocument } = useDocumentDisable() + const { mutateAsync: deleteDocument } = useDocumentDelete() + + const handleAction = (actionName: DocumentActionType) => { + return async () => { + let opApi = deleteDocument + switch (actionName) { + case DocumentActionType.archive: + opApi = archiveDocument + break + case DocumentActionType.enable: + opApi = enableDocument + break + case DocumentActionType.disable: + opApi = disableDocument + break + default: + opApi = deleteDocument + break + } + const [e] = await asyncRunSafe(opApi({ datasetId, documentIds: selectedIds }) as Promise) + + if (!e) { + Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + onUpdate() + } + else { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } + } + } + return ( -
+
- + - + + - - {localDocs.map((doc) => { + + {localDocs.map((doc, index) => { const isFile = doc.data_source_type === DataSourceType.FILE const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : '' return { router.push(`/datasets/${datasetId}/documents/${doc.id}`) }}> - + + -
# +
e.stopPropagation()}> + + # +
+
{t('datasetDocuments.list.table.header.fileName')}
{t('datasetDocuments.list.table.header.chunkingMode')} {t('datasetDocuments.list.table.header.words')} {t('datasetDocuments.list.table.header.hitCount')} -
+
{t('datasetDocuments.list.table.header.uploadTime')} - +
{t('datasetDocuments.list.table.header.status')} {t('datasetDocuments.list.table.header.action')}
{doc.position} +
e.stopPropagation()}> + { + onSelectedIdChange( + selectedIds.includes(doc.id) + ? selectedIds.filter(id => id !== doc.id) + : [...selectedIds, doc.id], + ) + }} + /> + {/* {doc.position} */} + {index + 1} +
+
@@ -436,22 +571,27 @@ const DocumentList: FC = ({ embeddingAvailable, documents = popupContent={t('datasetDocuments.list.table.rename')} >
{ e.stopPropagation() handleShowRenameModal(doc) }} > - +
- +
+ {renderCount(doc.word_count)} {renderCount(doc.hit_count)} + {formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)} @@ -473,6 +613,26 @@ const DocumentList: FC = ({ embeddingAvailable, documents = })}
+ {(selectedIds.length > 0) && ( + { + onSelectedIdChange([]) + }} + /> + )} + {/* Show Pagination only if the total is more than the limit */} + {pagination.total && pagination.total > (pagination.limit || 10) && ( + + )} {isShowRenameModal && currDocument && ( void + labelInnerClassName?: string + contentClassName?: string + showDivider?: boolean +}> + +export const EditSlice: FC = (props) => { + const { + label, + className, + text, + onDelete, + labelInnerClassName, + contentClassName, + showDivider = true, + ...rest + } = props + const [delBtnShow, setDelBtnShow] = useState(false) + const [isDelBtnHover, setDelBtnHover] = useState(false) + + const { refs, floatingStyles, context } = useFloating({ + open: delBtnShow, + onOpenChange: setDelBtnShow, + placement: 'right', + whileElementsMounted: autoUpdate, + middleware: [ + flip(), + shift(), + ], + }) + const hover = useHover(context, {}) + const dismiss = useDismiss(context) + const role = useRole(context) + const { getReferenceProps, getFloatingProps } = useInteractions([hover, dismiss, role]) + + const isDestructive = delBtnShow && isDelBtnHover + + return ( + + + + {label} + + + {text} + + {showDivider && } + {delBtnShow && +
setDelBtnHover(true)} + onMouseLeave={() => setDelBtnHover(false)} + > + { + e.stopPropagation() + onDelete() + setDelBtnShow(false) + }} + state={ActionButtonState.Destructive} + > + + +
+
} +
+
+ ) +} diff --git a/web/app/components/datasets/formatted-text/flavours/preview-slice.tsx b/web/app/components/datasets/formatted-text/flavours/preview-slice.tsx new file mode 100644 index 0000000000..e9fd72e3c0 --- /dev/null +++ b/web/app/components/datasets/formatted-text/flavours/preview-slice.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react' +import type { FC, ReactNode } from 'react' +import { autoUpdate, flip, inline, shift, useDismiss, useFloating, useHover, useInteractions, useRole } from '@floating-ui/react' +import type { SliceProps } from './type' +import { SliceContainer, SliceContent, SliceDivider, SliceLabel } from './shared' + +type PreviewSliceProps = SliceProps<{ + label: ReactNode + tooltip: ReactNode + labelInnerClassName?: string +}> + +export const PreviewSlice: FC = (props) => { + const { label, className, text, tooltip, labelInnerClassName, ...rest } = props + const [tooltipOpen, setTooltipOpen] = useState(false) + const { refs, floatingStyles, context } = useFloating({ + open: tooltipOpen, + onOpenChange: setTooltipOpen, + whileElementsMounted: autoUpdate, + placement: 'top', + middleware: [ + inline(), + flip(), + shift(), + ], + }) + const hover = useHover(context, { + delay: { open: 500 }, + move: true, + }) + const dismiss = useDismiss(context) + const role = useRole(context, { role: 'tooltip' }) + const { getReferenceProps, getFloatingProps } = useInteractions([hover, dismiss, role]) + return ( + <> + + {label} + {text} + + + {tooltipOpen && + {tooltip} + } + + ) +} diff --git a/web/app/components/datasets/formatted-text/flavours/shared.tsx b/web/app/components/datasets/formatted-text/flavours/shared.tsx new file mode 100644 index 0000000000..f130391e62 --- /dev/null +++ b/web/app/components/datasets/formatted-text/flavours/shared.tsx @@ -0,0 +1,60 @@ +import { type ComponentProps, type FC, forwardRef } from 'react' +import classNames from '@/utils/classnames' + +const baseStyle = 'py-[3px]' + +export type SliceContainerProps = ComponentProps<'span'> + +export const SliceContainer: FC = forwardRef((props, ref) => { + const { className, ...rest } = props + return +}) +SliceContainer.displayName = 'SliceContainer' + +export type SliceLabelProps = ComponentProps<'span'> & { labelInnerClassName?: string } + +export const SliceLabel: FC = forwardRef((props, ref) => { + const { className, children, labelInnerClassName, ...rest } = props + return + + {children} + + +}) +SliceLabel.displayName = 'SliceLabel' + +export type SliceContentProps = ComponentProps<'span'> + +export const SliceContent: FC = forwardRef((props, ref) => { + const { className, children, ...rest } = props + return + {children} + +}) +SliceContent.displayName = 'SliceContent' + +export type SliceDividerProps = ComponentProps<'span'> + +export const SliceDivider: FC = forwardRef((props, ref) => { + const { className, ...rest } = props + return + {/* use a zero-width space to make the hover area bigger */} + ​ + +}) +SliceDivider.displayName = 'SliceDivider' diff --git a/web/app/components/datasets/formatted-text/flavours/type.ts b/web/app/components/datasets/formatted-text/flavours/type.ts new file mode 100644 index 0000000000..8d2fb431f9 --- /dev/null +++ b/web/app/components/datasets/formatted-text/flavours/type.ts @@ -0,0 +1,5 @@ +import type { ComponentProps } from 'react' + +export type SliceProps = T & { + text: string +} & ComponentProps<'span'> diff --git a/web/app/components/datasets/formatted-text/formatted.tsx b/web/app/components/datasets/formatted-text/formatted.tsx new file mode 100644 index 0000000000..14d339e688 --- /dev/null +++ b/web/app/components/datasets/formatted-text/formatted.tsx @@ -0,0 +1,12 @@ +import type { ComponentProps, FC } from 'react' +import classNames from '@/utils/classnames' + +export type FormattedTextProps = ComponentProps<'p'> + +export const FormattedText: FC = (props) => { + const { className, ...rest } = props + return

{props.children}

+} diff --git a/web/app/components/datasets/hit-testing/components/child-chunks-item.tsx b/web/app/components/datasets/hit-testing/components/child-chunks-item.tsx new file mode 100644 index 0000000000..043aa3cea7 --- /dev/null +++ b/web/app/components/datasets/hit-testing/components/child-chunks-item.tsx @@ -0,0 +1,30 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { SliceContent } from '../../formatted-text/flavours/shared' +import Score from './score' +import type { HitTestingChildChunk } from '@/models/datasets' + +type Props = { + payload: HitTestingChildChunk + isShowAll: boolean +} + +const ChildChunks: FC = ({ + payload, + isShowAll, +}) => { + const { id, score, content, position } = payload + return ( +
+
+
C-{position}
+ +
+ {content} +
+ ) +} +export default React.memo(ChildChunks) diff --git a/web/app/components/datasets/hit-testing/components/chunk-detail-modal.tsx b/web/app/components/datasets/hit-testing/components/chunk-detail-modal.tsx new file mode 100644 index 0000000000..c1e2e41406 --- /dev/null +++ b/web/app/components/datasets/hit-testing/components/chunk-detail-modal.tsx @@ -0,0 +1,89 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { SegmentIndexTag } from '../../documents/detail/completed/common/segment-index-tag' +import Dot from '../../documents/detail/completed/common/dot' +import Score from './score' +import ChildChunksItem from './child-chunks-item' +import Modal from '@/app/components/base/modal' +import type { HitTesting } from '@/models/datasets' +import FileIcon from '@/app/components/base/file-uploader/file-type-icon' +import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' +import cn from '@/utils/classnames' +import Tag from '@/app/components/datasets/documents/detail/completed/common/tag' + +const i18nPrefix = 'datasetHitTesting' + +type Props = { + payload: HitTesting + onHide: () => void +} + +const ChunkDetailModal: FC = ({ + payload, + onHide, +}) => { + const { t } = useTranslation() + const { segment, score, child_chunks } = payload + const { position, content, keywords, document } = segment + const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0) + const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum + const maxHeighClassName = 'max-h-[min(752px,_80vh)] overflow-y-auto' + return ( + +
+
+ {/* Meta info */} +
+
+ + +
+ + {document.name} +
+
+ +
+
+ {content} +
+ {!isParentChildRetrieval && keywords && keywords.length > 0 && ( +
+
{t(`${i18nPrefix}.keyword`)}
+
+ {keywords.map(keyword => ( + + ))} +
+
+ )} +
+ + {isParentChildRetrieval && ( +
+
{t(`${i18nPrefix}.hitChunks`, { num: child_chunks.length })}
+
+ {child_chunks.map(item => ( + + ))} +
+
+ )} +
+
+ ) +} + +export default React.memo(ChunkDetailModal) diff --git a/web/app/components/datasets/hit-testing/components/result-item.tsx b/web/app/components/datasets/hit-testing/components/result-item.tsx new file mode 100644 index 0000000000..c18becd825 --- /dev/null +++ b/web/app/components/datasets/hit-testing/components/result-item.tsx @@ -0,0 +1,114 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { RiArrowDownSLine, RiArrowRightSLine, RiArrowRightUpLine } from '@remixicon/react' +import { useBoolean } from 'ahooks' +import { SegmentIndexTag } from '../../documents/detail/completed/common/segment-index-tag' +import Dot from '../../documents/detail/completed/common/dot' +import Score from './score' +import ChildChunkItem from './child-chunks-item' +import ChunkDetailModal from './chunk-detail-modal' +import type { HitTesting } from '@/models/datasets' +import cn from '@/utils/classnames' +import FileIcon from '@/app/components/base/file-uploader/file-type-icon' +import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' +import Tag from '@/app/components/datasets/documents/detail/completed/common/tag' + +const i18nPrefix = 'datasetHitTesting' +type Props = { + isExternal: boolean + payload: HitTesting +} + +const ResultItem: FC = ({ + isExternal, + payload, +}) => { + const { t } = useTranslation() + const { segment, content: externalContent, score, child_chunks } = payload + const data = isExternal ? externalContent : segment + const { position, word_count, content, keywords, document } = data + const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0) + const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum + const [isFold, { + toggle: toggleFold, + }] = useBoolean(false) + const Icon = isFold ? RiArrowRightSLine : RiArrowDownSLine + + const [isShowDetailModal, { + setTrue: showDetailModal, + setFalse: hideDetailModal, + }] = useBoolean(false) + + return ( +
+ {/* Meta info */} +
+
+ + +
{word_count} {t('datasetDocuments.segment.characters', { count: word_count })}
+
+ +
+ + {/* Main */} +
+
{content}
+ {isParentChildRetrieval && ( +
+
+ +
{t(`${i18nPrefix}.hitChunks`, { num: child_chunks.length })}
+
+ {!isFold && ( +
+ {child_chunks.map(item => ( +
+ +
+ ))} +
+ )} +
+ )} + {!isParentChildRetrieval && keywords && keywords.length > 0 && ( +
+ {keywords.map(keyword => ( + + ))} +
+ )} +
+ {/* Foot */} +
+
+ + {document.name} +
+
+
{t(`${i18nPrefix}.open`)}
+ +
+
+ + { + isShowDetailModal && ( + + ) + } +
+ ) +} +export default React.memo(ResultItem) diff --git a/web/app/components/datasets/hit-testing/components/score.tsx b/web/app/components/datasets/hit-testing/components/score.tsx new file mode 100644 index 0000000000..115141eaaa --- /dev/null +++ b/web/app/components/datasets/hit-testing/components/score.tsx @@ -0,0 +1,25 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import cn from '@/utils/classnames' + +type Props = { + value: number + besideChunkName?: boolean +} + +const Score: FC = ({ + value, + besideChunkName, +}) => { + return ( +
+
+
+
score
+
{value.toFixed(2)}
+
+
+ ) +} +export default React.memo(Score) diff --git a/web/app/components/datasets/hit-testing/hit-detail.tsx b/web/app/components/datasets/hit-testing/hit-detail.tsx deleted file mode 100644 index 066e2238c8..0000000000 --- a/web/app/components/datasets/hit-testing/hit-detail.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { FC } from 'react' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { SegmentIndexTag } from '../documents/detail/completed' -import s from '../documents/detail/completed/style.module.css' -import cn from '@/utils/classnames' -import type { SegmentDetailModel } from '@/models/datasets' -import Divider from '@/app/components/base/divider' - -type IHitDetailProps = { - segInfo?: Partial & { id: string } -} - -const HitDetail: FC = ({ segInfo }) => { - const { t } = useTranslation() - - const renderContent = () => { - if (segInfo?.answer) { - return ( - <> -
QUESTION
-
{segInfo.content}
-
ANSWER
-
{segInfo.answer}
- - ) - } - - return
{segInfo?.content}
- } - - return ( - segInfo?.id === 'external' - ?
-
{renderContent()}
-
- :
-
- -
- - {segInfo?.word_count} {t('datasetDocuments.segment.characters')} - -
- - {segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')} - -
- -
{renderContent()}
-
- {t('datasetDocuments.segment.keywords')} -
-
- {!segInfo?.keywords?.length - ? '-' - : segInfo?.keywords?.map((word, index) => { - return
{word}
- })} -
-
- ) -} - -export default HitDetail diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index ce47f2bfa6..30be6fb7e7 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -6,16 +6,15 @@ import useSWR from 'swr' import { omit } from 'lodash-es' import { useBoolean } from 'ahooks' import { useContext } from 'use-context-selector' +import { RiApps2Line, RiFocus2Line } from '@remixicon/react' import SegmentCard from '../documents/detail/completed/SegmentCard' -import docStyle from '../documents/detail/completed/style.module.css' import Textarea from './textarea' import s from './style.module.css' -import HitDetail from './hit-detail' import ModifyRetrievalModal from './modify-retrieval-modal' +import ResultItem from './components/result-item' import cn from '@/utils/classnames' -import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets' +import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets' import Loading from '@/app/components/base/loading' -import Modal from '@/app/components/base/modal' import Drawer from '@/app/components/base/drawer' import Pagination from '@/app/components/base/pagination' import FloatRightContainer from '@/app/components/base/float-right-container' @@ -24,6 +23,7 @@ import DatasetDetailContext from '@/context/dataset-detail' import type { RetrievalConfig } from '@/types/app' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' +import docStyle from '@/app/components/datasets/documents/detail/completed/style.module.css' const limit = 10 @@ -51,8 +51,6 @@ const HitTesting: FC = ({ datasetId }: Props) => { const [hitResult, setHitResult] = useState() // 初始化记录为空数组 const [externalHitResult, setExternalHitResult] = useState() const [submitLoading, setSubmitLoading] = useState(false) - const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTestingType; showModal: boolean }>({ showModal: false }) - const [externalCurrParagraph, setExternalCurrParagraph] = useState<{ paraInfo?: ExternalKnowledgeBaseHitTestingType; showModal: boolean }>({ showModal: false }) const [text, setText] = useState('') const [currPage, setCurrPage] = React.useState(0) @@ -64,51 +62,33 @@ const HitTesting: FC = ({ datasetId }: Props) => { const total = recordsRes?.total || 0 - const onClickCard = (detail: HitTestingType) => { - setCurrParagraph({ paraInfo: detail, showModal: true }) - } - - const onClickExternalCard = (detail: ExternalKnowledgeBaseHitTestingType) => { - setExternalCurrParagraph({ paraInfo: detail, showModal: true }) - } const { dataset: currentDataset } = useContext(DatasetDetailContext) const isExternal = currentDataset?.provider === 'external' const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig) const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false) const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile) - - const renderHitResults = (results: any[], onClickCard: (record: any) => void) => ( - <> -
{t('datasetHitTesting.hit.title')}
-
-
- {results.map((record, idx) => ( - onClickCard(record)} - /> - ))} -
+ const renderHitResults = (results: any[]) => ( +
+
+ {t('datasetHitTesting.hit.title', { num: results.length })}
- +
+ {results.map((record, idx) => ( + + ))} +
+
) const renderEmptyState = () => ( -
-
-
+
+
+
{t('datasetHitTesting.hit.emptyTip')}
@@ -120,10 +100,10 @@ const HitTesting: FC = ({ datasetId }: Props) => { return (
-
-
-

{t('datasetHitTesting.title')}

-

{t('datasetHitTesting.desc')}

+
+
+

{t('datasetHitTesting.title')}

+

{t('datasetHitTesting.desc')}