mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Compare commits
6 Commits
856201c0f2
...
8604c4f57c
| Author | SHA1 | Date | |
|---|---|---|---|
| 8604c4f57c | |||
| a674338c21 | |||
| 89d82ff031 | |||
| c71d25f744 | |||
| f57f32cf3a | |||
| b6314164c5 |
@ -281,6 +281,7 @@ class Canvas(Graph):
|
||||
"sys.conversation_turns": 0,
|
||||
"sys.files": []
|
||||
}
|
||||
self.variables = {}
|
||||
super().__init__(dsl, tenant_id, task_id)
|
||||
|
||||
def load(self):
|
||||
@ -295,6 +296,10 @@ class Canvas(Graph):
|
||||
"sys.conversation_turns": 0,
|
||||
"sys.files": []
|
||||
}
|
||||
if "variables" in self.dsl:
|
||||
self.variables = self.dsl["variables"]
|
||||
else:
|
||||
self.variables = {}
|
||||
|
||||
self.retrieval = self.dsl["retrieval"]
|
||||
self.memory = self.dsl.get("memory", [])
|
||||
@ -311,8 +316,9 @@ class Canvas(Graph):
|
||||
self.history = []
|
||||
self.retrieval = []
|
||||
self.memory = []
|
||||
print(self.variables)
|
||||
for k in self.globals.keys():
|
||||
if k.startswith("sys.") or k.startswith("env."):
|
||||
if k.startswith("sys."):
|
||||
if isinstance(self.globals[k], str):
|
||||
self.globals[k] = ""
|
||||
elif isinstance(self.globals[k], int):
|
||||
@ -325,6 +331,29 @@ class Canvas(Graph):
|
||||
self.globals[k] = {}
|
||||
else:
|
||||
self.globals[k] = None
|
||||
if k.startswith("env."):
|
||||
key = k[4:]
|
||||
if key in self.variables:
|
||||
variable = self.variables[key]
|
||||
if variable["value"]:
|
||||
self.globals[k] = variable["value"]
|
||||
else:
|
||||
if variable["type"] == "string":
|
||||
self.globals[k] = ""
|
||||
elif variable["type"] == "number":
|
||||
self.globals[k] = 0
|
||||
elif variable["type"] == "boolean":
|
||||
self.globals[k] = False
|
||||
elif variable["type"] == "object":
|
||||
self.globals[k] = {}
|
||||
elif variable["type"].startswith("array"):
|
||||
self.globals[k] = []
|
||||
else:
|
||||
self.globals[k] = ""
|
||||
else:
|
||||
self.globals[k] = ""
|
||||
print(self.globals)
|
||||
|
||||
|
||||
async def run(self, **kwargs):
|
||||
st = time.perf_counter()
|
||||
@ -473,7 +502,7 @@ class Canvas(Graph):
|
||||
else:
|
||||
self.error = cpn_obj.error()
|
||||
|
||||
if cpn_obj.component_name.lower() != "iteration":
|
||||
if cpn_obj.component_name.lower() not in ("iteration","loop"):
|
||||
if isinstance(cpn_obj.output("content"), partial):
|
||||
if self.error:
|
||||
cpn_obj.set_output("content", None)
|
||||
@ -498,14 +527,16 @@ class Canvas(Graph):
|
||||
for cpn_id in cpn_ids:
|
||||
_append_path(cpn_id)
|
||||
|
||||
if cpn_obj.component_name.lower() == "iterationitem" and cpn_obj.end():
|
||||
if cpn_obj.component_name.lower() in ("iterationitem","loopitem") and cpn_obj.end():
|
||||
iter = cpn_obj.get_parent()
|
||||
yield _node_finished(iter)
|
||||
_extend_path(self.get_component(cpn["parent_id"])["downstream"])
|
||||
elif cpn_obj.component_name.lower() in ["categorize", "switch"]:
|
||||
_extend_path(cpn_obj.output("_next"))
|
||||
elif cpn_obj.component_name.lower() == "iteration":
|
||||
elif cpn_obj.component_name.lower() in ("iteration", "loop"):
|
||||
_append_path(cpn_obj.get_start())
|
||||
elif cpn_obj.component_name.lower() == "exitloop" and cpn_obj.get_parent().component_name.lower() == "loop":
|
||||
_extend_path(self.get_component(cpn["parent_id"])["downstream"])
|
||||
elif not cpn["downstream"] and cpn_obj.get_parent():
|
||||
_append_path(cpn_obj.get_parent().get_start())
|
||||
else:
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -29,7 +30,7 @@ from api.db.services.tenant_llm_service import TenantLLMService
|
||||
from api.db.services.mcp_server_service import MCPServerService
|
||||
from common.connection_utils import timeout
|
||||
from rag.prompts.generator import next_step, COMPLETE_TASK, analyze_task, \
|
||||
citation_prompt, reflect, rank_memories, kb_prompt, citation_plus, full_question, message_fit_in
|
||||
citation_prompt, reflect, rank_memories, kb_prompt, citation_plus, full_question, message_fit_in, structured_output_prompt
|
||||
from common.mcp_tool_call_conn import MCPToolCallSession, mcp_tool_metadata_to_openai_tool
|
||||
from agent.component.llm import LLMParam, LLM
|
||||
|
||||
@ -137,6 +138,29 @@ class Agent(LLM, ToolBase):
|
||||
res.update(cpn.get_input_form())
|
||||
return res
|
||||
|
||||
def _get_output_schema(self):
|
||||
try:
|
||||
cand = self._param.outputs.get("structured")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if isinstance(cand, dict):
|
||||
if isinstance(cand.get("properties"), dict) and len(cand["properties"]) > 0:
|
||||
return cand
|
||||
for k in ("schema", "structured"):
|
||||
if isinstance(cand.get(k), dict) and isinstance(cand[k].get("properties"), dict) and len(cand[k]["properties"]) > 0:
|
||||
return cand[k]
|
||||
|
||||
return None
|
||||
|
||||
def _force_format_to_schema(self, text: str, schema_prompt: str) -> str:
|
||||
fmt_msgs = [
|
||||
{"role": "system", "content": schema_prompt + "\nIMPORTANT: Output ONLY valid JSON. No markdown, no extra text."},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
_, fmt_msgs = message_fit_in(fmt_msgs, int(self.chat_mdl.max_length * 0.97))
|
||||
return self._generate(fmt_msgs)
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 20*60)))
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("Agent processing"):
|
||||
@ -160,17 +184,22 @@ class Agent(LLM, ToolBase):
|
||||
return LLM._invoke(self, **kwargs)
|
||||
|
||||
prompt, msg, user_defined_prompt = self._prepare_prompt_variables()
|
||||
output_schema = self._get_output_schema()
|
||||
schema_prompt = ""
|
||||
if output_schema:
|
||||
schema = json.dumps(output_schema, ensure_ascii=False, indent=2)
|
||||
schema_prompt = structured_output_prompt(schema)
|
||||
|
||||
downstreams = self._canvas.get_component(self._id)["downstream"] if self._canvas.get_component(self._id) else []
|
||||
ex = self.exception_handler()
|
||||
if any([self._canvas.get_component_obj(cid).component_name.lower()=="message" for cid in downstreams]) and not (ex and ex["goto"]):
|
||||
if any([self._canvas.get_component_obj(cid).component_name.lower()=="message" for cid in downstreams]) and not (ex and ex["goto"]) and not output_schema:
|
||||
self.set_output("content", partial(self.stream_output_with_tools, prompt, msg, user_defined_prompt))
|
||||
return
|
||||
|
||||
_, msg = message_fit_in([{"role": "system", "content": prompt}, *msg], int(self.chat_mdl.max_length * 0.97))
|
||||
use_tools = []
|
||||
ans = ""
|
||||
for delta_ans, tk in self._react_with_tools_streamly(prompt, msg, use_tools, user_defined_prompt):
|
||||
for delta_ans, tk in self._react_with_tools_streamly(prompt, msg, use_tools, user_defined_prompt,schema_prompt=schema_prompt):
|
||||
if self.check_if_canceled("Agent processing"):
|
||||
return
|
||||
ans += delta_ans
|
||||
@ -183,6 +212,28 @@ class Agent(LLM, ToolBase):
|
||||
self.set_output("_ERROR", ans)
|
||||
return
|
||||
|
||||
if output_schema:
|
||||
error = ""
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
try:
|
||||
def clean_formated_answer(ans: str) -> str:
|
||||
ans = re.sub(r"^.*</think>", "", ans, flags=re.DOTALL)
|
||||
ans = re.sub(r"^.*```json", "", ans, flags=re.DOTALL)
|
||||
return re.sub(r"```\n*$", "", ans, flags=re.DOTALL)
|
||||
obj = json_repair.loads(clean_formated_answer(ans))
|
||||
self.set_output("structured", obj)
|
||||
if use_tools:
|
||||
self.set_output("use_tools", use_tools)
|
||||
return obj
|
||||
except Exception:
|
||||
error = "The answer cannot be parsed as JSON"
|
||||
ans = self._force_format_to_schema(ans, schema_prompt)
|
||||
if ans.find("**ERROR**") >= 0:
|
||||
continue
|
||||
|
||||
self.set_output("_ERROR", error)
|
||||
return
|
||||
|
||||
self.set_output("content", ans)
|
||||
if use_tools:
|
||||
self.set_output("use_tools", use_tools)
|
||||
@ -219,7 +270,7 @@ class Agent(LLM, ToolBase):
|
||||
]):
|
||||
yield delta_ans
|
||||
|
||||
def _react_with_tools_streamly(self, prompt, history: list[dict], use_tools, user_defined_prompt={}):
|
||||
def _react_with_tools_streamly(self, prompt, history: list[dict], use_tools, user_defined_prompt={}, schema_prompt: str = ""):
|
||||
token_count = 0
|
||||
tool_metas = self.tool_meta
|
||||
hist = deepcopy(history)
|
||||
@ -256,9 +307,13 @@ class Agent(LLM, ToolBase):
|
||||
def complete():
|
||||
nonlocal hist
|
||||
need2cite = self._param.cite and self._canvas.get_reference()["chunks"] and self._id.find("-->") < 0
|
||||
if schema_prompt:
|
||||
need2cite = False
|
||||
cited = False
|
||||
if hist[0]["role"] == "system" and need2cite:
|
||||
if len(hist) < 7:
|
||||
if hist and hist[0]["role"] == "system":
|
||||
if schema_prompt:
|
||||
hist[0]["content"] += "\n" + schema_prompt
|
||||
if need2cite and len(hist) < 7:
|
||||
hist[0]["content"] += citation_prompt()
|
||||
cited = True
|
||||
yield "", token_count
|
||||
@ -369,7 +424,7 @@ Respond immediately with your final comprehensive answer.
|
||||
"""
|
||||
for k in self._param.outputs.keys():
|
||||
self._param.outputs[k]["value"] = None
|
||||
|
||||
|
||||
for k, cpn in self.tools.items():
|
||||
if hasattr(cpn, "reset") and callable(cpn.reset):
|
||||
cpn.reset()
|
||||
|
||||
32
agent/component/exit_loop.py
Normal file
32
agent/component/exit_loop.py
Normal file
@ -0,0 +1,32 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from abc import ABC
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
class ExitLoopParam(ComponentParamBase, ABC):
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
|
||||
class ExitLoop(ComponentBase, ABC):
|
||||
component_name = "ExitLoop"
|
||||
|
||||
def _invoke(self, **kwargs):
|
||||
pass
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return ""
|
||||
@ -222,7 +222,7 @@ class LLM(ComponentBase):
|
||||
output_structure = self._param.outputs['structured']
|
||||
except Exception:
|
||||
pass
|
||||
if output_structure and isinstance(output_structure, dict) and output_structure.get("properties"):
|
||||
if output_structure and isinstance(output_structure, dict) and output_structure.get("properties") and len(output_structure["properties"]) > 0:
|
||||
schema=json.dumps(output_structure, ensure_ascii=False, indent=2)
|
||||
prompt += structured_output_prompt(schema)
|
||||
for _ in range(self._param.max_retries+1):
|
||||
|
||||
80
agent/component/loop.py
Normal file
80
agent/component/loop.py
Normal file
@ -0,0 +1,80 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from abc import ABC
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
class LoopParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Loop component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.loop_variables = []
|
||||
self.loop_termination_condition=[]
|
||||
self.maximum_loop_count = 0
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"items": {
|
||||
"type": "json",
|
||||
"name": "Items"
|
||||
}
|
||||
}
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
|
||||
class Loop(ComponentBase, ABC):
|
||||
component_name = "Loop"
|
||||
|
||||
def get_start(self):
|
||||
for cid in self._canvas.components.keys():
|
||||
if self._canvas.get_component(cid)["obj"].component_name.lower() != "loopitem":
|
||||
continue
|
||||
if self._canvas.get_component(cid)["parent_id"] == self._id:
|
||||
return cid
|
||||
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("Loop processing"):
|
||||
return
|
||||
|
||||
for item in self._param.loop_variables:
|
||||
if any([not item.get("variable"), not item.get("input_mode"), not item.get("value"),not item.get("type")]):
|
||||
assert "Loop Variable is not complete."
|
||||
if item["input_mode"]=="variable":
|
||||
self.set_output(item["variable"],self._canvas.get_variable_value(item["value"]))
|
||||
elif item["input_mode"]=="constant":
|
||||
self.set_output(item["variable"],item["value"])
|
||||
else:
|
||||
if item["type"] == "number":
|
||||
self.set_output(item["variable"], 0)
|
||||
elif item["type"] == "string":
|
||||
self.set_output(item["variable"], "")
|
||||
elif item["type"] == "boolean":
|
||||
self.set_output(item["variable"], False)
|
||||
elif item["type"].startswith("object"):
|
||||
self.set_output(item["variable"], {})
|
||||
elif item["type"].startswith("array"):
|
||||
self.set_output(item["variable"], [])
|
||||
else:
|
||||
self.set_output(item["variable"], "")
|
||||
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Loop from canvas."
|
||||
163
agent/component/loopitem.py
Normal file
163
agent/component/loopitem.py
Normal file
@ -0,0 +1,163 @@
|
||||
#
|
||||
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from abc import ABC
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
|
||||
|
||||
class LoopItemParam(ComponentParamBase):
|
||||
"""
|
||||
Define the LoopItem component parameters.
|
||||
"""
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
class LoopItem(ComponentBase, ABC):
|
||||
component_name = "LoopItem"
|
||||
|
||||
def __init__(self, canvas, id, param: ComponentParamBase):
|
||||
super().__init__(canvas, id, param)
|
||||
self._idx = 0
|
||||
|
||||
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("LoopItem processing"):
|
||||
return
|
||||
parent = self.get_parent()
|
||||
maximum_loop_count = parent._param.maximum_loop_count
|
||||
if self._idx >= maximum_loop_count:
|
||||
self._idx = -1
|
||||
return
|
||||
if self._idx > 0:
|
||||
if self.check_if_canceled("LoopItem processing"):
|
||||
return
|
||||
self._idx += 1
|
||||
|
||||
def evaluate_condition(self,var, operator, value):
|
||||
if isinstance(var, str):
|
||||
if operator == "contains":
|
||||
return value in var
|
||||
elif operator == "not contains":
|
||||
return value not in var
|
||||
elif operator == "start with":
|
||||
return var.startswith(value)
|
||||
elif operator == "end with":
|
||||
return var.endswith(value)
|
||||
elif operator == "is":
|
||||
return var == value
|
||||
elif operator == "is not":
|
||||
return var != value
|
||||
elif operator == "empty":
|
||||
return var == ""
|
||||
elif operator == "not empty":
|
||||
return var != ""
|
||||
|
||||
elif isinstance(var, (int, float)):
|
||||
if operator == "=":
|
||||
return var == value
|
||||
elif operator == "≠":
|
||||
return var != value
|
||||
elif operator == ">":
|
||||
return var > value
|
||||
elif operator == "<":
|
||||
return var < value
|
||||
elif operator == "≥":
|
||||
return var >= value
|
||||
elif operator == "≤":
|
||||
return var <= value
|
||||
elif operator == "empty":
|
||||
return var is None
|
||||
elif operator == "not empty":
|
||||
return var is not None
|
||||
|
||||
elif isinstance(var, bool):
|
||||
if operator == "is":
|
||||
return var is value
|
||||
elif operator == "is not":
|
||||
return var is not value
|
||||
elif operator == "empty":
|
||||
return var is None
|
||||
elif operator == "not empty":
|
||||
return var is not None
|
||||
|
||||
elif isinstance(var, dict):
|
||||
if operator == "empty":
|
||||
return len(var) == 0
|
||||
elif operator == "not empty":
|
||||
return len(var) > 0
|
||||
|
||||
elif isinstance(var, list):
|
||||
if operator == "contains":
|
||||
return value in var
|
||||
elif operator == "not contains":
|
||||
return value not in var
|
||||
|
||||
elif operator == "is":
|
||||
return var == value
|
||||
elif operator == "is not":
|
||||
return var != value
|
||||
|
||||
elif operator == "empty":
|
||||
return len(var) == 0
|
||||
elif operator == "not empty":
|
||||
return len(var) > 0
|
||||
|
||||
raise Exception(f"Invalid operator: {operator}")
|
||||
|
||||
def end(self):
|
||||
if self._idx == -1:
|
||||
return True
|
||||
parent = self.get_parent()
|
||||
logical_operator = parent._param.logical_operator if hasattr(parent._param, "logical_operator") else "and"
|
||||
conditions = []
|
||||
for item in parent._param.loop_termination_condition:
|
||||
if not item.get("variable") or not item.get("operator"):
|
||||
raise ValueError("Loop condition is incomplete.")
|
||||
var = self._canvas.get_variable_value(item["variable"])
|
||||
operator = item["operator"]
|
||||
input_mode = item.get("input_mode", "constant")
|
||||
|
||||
if input_mode == "variable":
|
||||
value = self._canvas.get_variable_value(item.get("value", ""))
|
||||
elif input_mode == "constant":
|
||||
value = item.get("value", "")
|
||||
else:
|
||||
raise ValueError("Invalid input mode.")
|
||||
conditions.append(self.evaluate_condition(var, operator, value))
|
||||
should_end = (
|
||||
all(conditions) if logical_operator == "and"
|
||||
else any(conditions) if logical_operator == "or"
|
||||
else None
|
||||
)
|
||||
if should_end is None:
|
||||
raise ValueError("Invalid logical operator,should be 'and' or 'or'.")
|
||||
|
||||
if should_end:
|
||||
self._idx = -1
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def next(self):
|
||||
if self._idx == -1:
|
||||
self._idx = 0
|
||||
else:
|
||||
self._idx += 1
|
||||
if self._idx >= len(self._items):
|
||||
self._idx = -1
|
||||
return False
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Next turn..."
|
||||
@ -7,6 +7,20 @@
|
||||
"status": "1",
|
||||
"rank": "999",
|
||||
"llm": [
|
||||
{
|
||||
"llm_name": "gpt-5.1",
|
||||
"tags": "LLM,CHAT,400k,IMAGE2TEXT",
|
||||
"max_tokens": 400000,
|
||||
"model_type": "chat",
|
||||
"is_tools": true
|
||||
},
|
||||
{
|
||||
"llm_name": "gpt-5.1-chat-latest",
|
||||
"tags": "LLM,CHAT,400k,IMAGE2TEXT",
|
||||
"max_tokens": 400000,
|
||||
"model_type": "chat",
|
||||
"is_tools": true
|
||||
},
|
||||
{
|
||||
"llm_name": "gpt-5",
|
||||
"tags": "LLM,CHAT,400k,IMAGE2TEXT",
|
||||
@ -3218,6 +3232,13 @@
|
||||
"status": "1",
|
||||
"rank": "990",
|
||||
"llm": [
|
||||
{
|
||||
"llm_name": "claude-opus-4-5-20251101",
|
||||
"tags": "LLM,CHAT,IMAGE2TEXT,200k",
|
||||
"max_tokens": 204800,
|
||||
"model_type": "chat",
|
||||
"is_tools": true
|
||||
},
|
||||
{
|
||||
"llm_name": "claude-opus-4-1-20250805",
|
||||
"tags": "LLM,CHAT,IMAGE2TEXT,200k",
|
||||
|
||||
@ -402,7 +402,6 @@ class RAGFlowPdfParser:
|
||||
continue
|
||||
else:
|
||||
score = 0
|
||||
print(f"{k=},{score=}",flush=True)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_k = k
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
# import re
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
|
||||
@ -62,8 +62,9 @@ class LayoutRecognizer(Recognizer):
|
||||
|
||||
def __call__(self, image_list, ocr_res, scale_factor=3, thr=0.2, batch_size=16, drop=True):
|
||||
def __is_garbage(b):
|
||||
patt = [r"^•+$", "^[0-9]{1,2} / ?[0-9]{1,2}$", r"^[0-9]{1,2} of [0-9]{1,2}$", "^http://[^ ]{12,}", "\\(cid *: *[0-9]+ *\\)"]
|
||||
return any([re.search(p, b["text"]) for p in patt])
|
||||
return False
|
||||
# patt = [r"^•+$", "^[0-9]{1,2} / ?[0-9]{1,2}$", r"^[0-9]{1,2} of [0-9]{1,2}$", "^http://[^ ]{12,}", "\\(cid *: *[0-9]+ *\\)"]
|
||||
# return any([re.search(p, b["text"]) for p in patt])
|
||||
|
||||
if self.client:
|
||||
layouts = self.client.predict(image_list)
|
||||
|
||||
@ -132,6 +132,11 @@ class Base(ABC):
|
||||
|
||||
gen_conf = {k: v for k, v in gen_conf.items() if k in allowed_conf}
|
||||
|
||||
model_name_lower = (self.model_name or "").lower()
|
||||
# gpt-5 and gpt-5.1 endpoints have inconsistent parameter support, clear custom generation params to prevent unexpected issues
|
||||
if "gpt-5" in model_name_lower:
|
||||
gen_conf = {}
|
||||
|
||||
return gen_conf
|
||||
|
||||
def _chat(self, history, gen_conf, **kwargs):
|
||||
|
||||
18
web/src/components/bool-segmented.tsx
Normal file
18
web/src/components/bool-segmented.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Segmented, SegmentedProps } from './ui/segmented';
|
||||
|
||||
export function BoolSegmented({ ...props }: Omit<SegmentedProps, 'options'>) {
|
||||
return (
|
||||
<Segmented
|
||||
options={
|
||||
[
|
||||
{ value: true, label: 'True' },
|
||||
{ value: false, label: 'False' },
|
||||
] as any
|
||||
}
|
||||
sizeType="sm"
|
||||
itemClassName="justify-center flex-1"
|
||||
{...omit(props, 'options')}
|
||||
></Segmented>
|
||||
);
|
||||
}
|
||||
@ -6,7 +6,9 @@ import {
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import {
|
||||
currentReg,
|
||||
preprocessLaTeX,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
showImage,
|
||||
} from '@/utils/chat';
|
||||
@ -32,7 +34,6 @@ import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import { visitParents } from 'unist-util-visit-parents';
|
||||
import { currentReg, replaceTextByOldReg } from '../pages/next-chats/utils';
|
||||
import styles from './floating-chat-widget-markdown.less';
|
||||
import { useIsDarkTheme } from './theme-provider';
|
||||
|
||||
|
||||
24
web/src/components/logical-operator.tsx
Normal file
24
web/src/components/logical-operator.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { RAGFlowFormItem } from './ragflow-form';
|
||||
import { RAGFlowSelect } from './ui/select';
|
||||
|
||||
type LogicalOperatorProps = { name: string };
|
||||
|
||||
export function LogicalOperator({ name }: LogicalOperatorProps) {
|
||||
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
|
||||
|
||||
return (
|
||||
<div className="relative min-w-14">
|
||||
<RAGFlowFormItem
|
||||
name={name}
|
||||
className="absolute top-1/2 -translate-y-1/2 right-1 left-0 z-10 bg-bg-base"
|
||||
>
|
||||
<RAGFlowSelect
|
||||
options={switchLogicOperatorOptions}
|
||||
triggerClassName="w-full text-xs px-1 py-0 h-6"
|
||||
></RAGFlowSelect>
|
||||
</RAGFlowFormItem>
|
||||
<div className="absolute border-l border-y w-5 right-0 top-4 bottom-4 rounded-l-lg"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -21,11 +21,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
|
||||
import {
|
||||
currentReg,
|
||||
preprocessLaTeX,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
showImage,
|
||||
} from '@/utils/chat';
|
||||
import { currentReg, replaceTextByOldReg } from '../utils';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
@ -1,6 +1,10 @@
|
||||
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||
import {
|
||||
IMessage,
|
||||
IReference,
|
||||
IReferenceChunk,
|
||||
} from '@/interfaces/database/chat';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
@ -10,9 +14,8 @@ import {
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { IMessage } from '@/pages/chat/interface';
|
||||
import MarkdownContent from '@/pages/chat/markdown-content';
|
||||
import { Avatar, Flex, Space } from 'antd';
|
||||
import MarkdownContent from '../markdown-content';
|
||||
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
|
||||
import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files';
|
||||
import { useTheme } from '../theme-provider';
|
||||
|
||||
@ -17,15 +17,13 @@ import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { SwitchLogicOperator, SwitchOperatorOptions } from '@/constants/agent';
|
||||
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
|
||||
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
|
||||
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RAGFlowFormItem } from '../ragflow-form';
|
||||
import { RAGFlowSelect } from '../ui/select';
|
||||
import { LogicalOperator } from '../logical-operator';
|
||||
|
||||
export function MetadataFilterConditions({
|
||||
kbIds,
|
||||
@ -44,8 +42,6 @@ export function MetadataFilterConditions({
|
||||
|
||||
const switchOperatorOptions = useBuildSwitchOperatorOptions();
|
||||
|
||||
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name,
|
||||
control: form.control,
|
||||
@ -53,14 +49,16 @@ export function MetadataFilterConditions({
|
||||
|
||||
const add = useCallback(
|
||||
(key: string) => () => {
|
||||
form.setValue(logic, SwitchLogicOperator.And);
|
||||
if (fields.length === 1) {
|
||||
form.setValue(logic, SwitchLogicOperator.And);
|
||||
}
|
||||
append({
|
||||
key,
|
||||
value: '',
|
||||
op: SwitchOperatorOptions[0].value,
|
||||
});
|
||||
},
|
||||
[append, form, logic],
|
||||
[append, fields.length, form, logic],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -85,20 +83,7 @@ export function MetadataFilterConditions({
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<section className="flex">
|
||||
{fields.length > 1 && (
|
||||
<div className="relative min-w-14">
|
||||
<RAGFlowFormItem
|
||||
name={logic}
|
||||
className="absolute top-1/2 -translate-y-1/2 right-1 left-0 z-10 bg-bg-base"
|
||||
>
|
||||
<RAGFlowSelect
|
||||
options={switchLogicOperatorOptions}
|
||||
triggerClassName="w-full text-xs px-1 py-0 h-6"
|
||||
></RAGFlowSelect>
|
||||
</RAGFlowFormItem>
|
||||
<div className="absolute border-l border-y w-5 right-0 top-4 bottom-4 rounded-l-lg"></div>
|
||||
</div>
|
||||
)}
|
||||
{fields.length > 1 && <LogicalOperator name={logic}></LogicalOperator>}
|
||||
<div className="space-y-5 flex-1">
|
||||
{fields.map((field, index) => {
|
||||
const typeField = `${name}.${index}.key`;
|
||||
|
||||
@ -19,13 +19,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
|
||||
import {
|
||||
currentReg,
|
||||
preprocessLaTeX,
|
||||
replaceTextByOldReg,
|
||||
replaceThinkToSection,
|
||||
showImage,
|
||||
} from '@/utils/chat';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { currentReg, replaceTextByOldReg } from '@/pages/chat/utils';
|
||||
import classNames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
import { pipe } from 'lodash/fp';
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
|
||||
import {
|
||||
IMessage,
|
||||
IReferenceChunk,
|
||||
IReferenceObject,
|
||||
} from '@/interfaces/database/chat';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
PropsWithChildren,
|
||||
@ -17,7 +21,6 @@ import { INodeEvent, MessageEventType } from '@/hooks/use-send-message';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AgentChatContext } from '@/pages/agent/context';
|
||||
import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workflow-timeline';
|
||||
import { IMessage } from '@/pages/chat/interface';
|
||||
import { downloadFile } from '@/services/file-manager-service';
|
||||
import { downloadFileFromBlob } from '@/utils/file-util';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@ -5,8 +5,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { TagRenameId } from '@/constants/knowledge';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ButtonLoading } from '../ui/button';
|
||||
|
||||
@ -13,8 +13,8 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { TagRenameId } from '@/constants/knowledge';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@ -75,7 +75,6 @@ export enum Operator {
|
||||
Message = 'Message',
|
||||
Relevant = 'Relevant',
|
||||
RewriteQuestion = 'RewriteQuestion',
|
||||
KeywordExtract = 'KeywordExtract',
|
||||
DuckDuckGo = 'DuckDuckGo',
|
||||
Wikipedia = 'Wikipedia',
|
||||
PubMed = 'PubMed',
|
||||
@ -84,14 +83,10 @@ export enum Operator {
|
||||
Bing = 'Bing',
|
||||
GoogleScholar = 'GoogleScholar',
|
||||
GitHub = 'GitHub',
|
||||
QWeather = 'QWeather',
|
||||
ExeSQL = 'ExeSQL',
|
||||
Switch = 'Switch',
|
||||
WenCai = 'WenCai',
|
||||
AkShare = 'AkShare',
|
||||
YahooFinance = 'YahooFinance',
|
||||
Jin10 = 'Jin10',
|
||||
TuShare = 'TuShare',
|
||||
Note = 'Note',
|
||||
Crawler = 'Crawler',
|
||||
Invoke = 'Invoke',
|
||||
@ -118,6 +113,9 @@ export enum Operator {
|
||||
Splitter = 'Splitter',
|
||||
HierarchicalMerger = 'HierarchicalMerger',
|
||||
Extractor = 'Extractor',
|
||||
Loop = 'Loop',
|
||||
LoopStart = 'LoopItem',
|
||||
ExitLoop = 'ExitLoop',
|
||||
}
|
||||
|
||||
export enum ComparisonOperator {
|
||||
|
||||
@ -92,3 +92,5 @@ export enum DocumentParserType {
|
||||
Tag = 'tag',
|
||||
KnowledgeGraph = 'knowledge_graph',
|
||||
}
|
||||
|
||||
export const TagRenameId = 'tagRename';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ChatSearchParams } from '@/constants/chat';
|
||||
import {
|
||||
IClientConversation,
|
||||
IConversation,
|
||||
IDialog,
|
||||
IStats,
|
||||
@ -10,8 +11,7 @@ import {
|
||||
IFeedbackRequestBody,
|
||||
} from '@/interfaces/request/chat';
|
||||
import i18n from '@/locales/config';
|
||||
import { IClientConversation } from '@/pages/chat/interface';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
|
||||
import chatService from '@/services/chat-service';
|
||||
import {
|
||||
buildMessageListWithUuid,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DSL, IFlow } from '@/interfaces/database/flow';
|
||||
import { IDebugSingleRequestBody } from '@/interfaces/request/flow';
|
||||
import i18n from '@/locales/config';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
|
||||
import flowService from '@/services/flow-service';
|
||||
import { buildMessageListWithUuid } from '@/utils/chat';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
@ -2,9 +2,13 @@ import { Authorization } from '@/constants/authorization';
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { LanguageTranslationMap } from '@/constants/common';
|
||||
import { ResponseType } from '@/interfaces/database/base';
|
||||
import { IAnswer, Message } from '@/interfaces/database/chat';
|
||||
import {
|
||||
IAnswer,
|
||||
IClientConversation,
|
||||
IMessage,
|
||||
Message,
|
||||
} from '@/interfaces/database/chat';
|
||||
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import { IClientConversation, IMessage } from '@/pages/chat/interface';
|
||||
import api from '@/utils/api';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import { buildMessageUuid } from '@/utils/chat';
|
||||
|
||||
@ -14,7 +14,7 @@ import { IDebugSingleRequestBody } from '@/interfaces/request/agent';
|
||||
import i18n from '@/locales/config';
|
||||
import { BeginId } from '@/pages/agent/constant';
|
||||
import { IInputs } from '@/pages/agent/interface';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
|
||||
import agentService, {
|
||||
fetchAgentLogsByCanvasId,
|
||||
fetchPipeLineList,
|
||||
|
||||
@ -2,12 +2,12 @@ import { FileUploadProps } from '@/components/file-upload';
|
||||
import message from '@/components/ui/message';
|
||||
import { ChatSearchParams } from '@/constants/chat';
|
||||
import {
|
||||
IClientConversation,
|
||||
IConversation,
|
||||
IDialog,
|
||||
IExternalChatInfo,
|
||||
} from '@/interfaces/database/chat';
|
||||
import { IAskRequestBody } from '@/interfaces/request/chat';
|
||||
import { IClientConversation } from '@/pages/next-chats/chat/interface';
|
||||
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
|
||||
import { isConversationIdExist } from '@/pages/next-chats/utils';
|
||||
import chatService from '@/services/next-chat-service';
|
||||
|
||||
@ -183,3 +183,12 @@ export interface IExternalChatInfo {
|
||||
title: string;
|
||||
prologue?: string;
|
||||
}
|
||||
|
||||
export interface IMessage extends Message {
|
||||
id: string;
|
||||
reference?: IReference; // the latest news has reference
|
||||
}
|
||||
|
||||
export interface IClientConversation extends IConversation {
|
||||
message: IMessage[];
|
||||
}
|
||||
|
||||
@ -1170,8 +1170,13 @@ Example: Virtual Hosted Style`,
|
||||
addField: 'Add option',
|
||||
addMessage: 'Add message',
|
||||
loop: 'Loop',
|
||||
loopTip:
|
||||
loopDescription:
|
||||
'Loop is the upper limit of the number of loops of the current component, when the number of loops exceeds the value of loop, it means that the component can not complete the current task, please re-optimize agent',
|
||||
exitLoop: 'Exit loop',
|
||||
exitLoopDescription: `Equivalent to "break". This node has no configuration items. When the loop body reaches this node, the loop terminates.`,
|
||||
loopVariables: 'Loop Variables',
|
||||
maximumLoopCount: 'Maximum loop count',
|
||||
loopTerminationCondition: 'Loop termination condition',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
key: 'Key',
|
||||
@ -1655,9 +1660,8 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
variableAssignerDescription:
|
||||
'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.',
|
||||
variableAggregator: 'Variable aggregator',
|
||||
variableAggregatorDescription: `This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes.
|
||||
|
||||
The variable aggregation node (originally the variable assignment node) is a crucial node in the workflow. It is responsible for integrating the output results of different branches, ensuring that regardless of which branch is executed, its result can be referenced and accessed through a unified variable. This is extremely useful in multi-branch scenarios, as it maps variables with the same function across different branches to a single output variable, avoiding redundant definitions in downstream nodes.`,
|
||||
variableAggregatorDescription: `
|
||||
This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes.`,
|
||||
inputVariables: 'Input variables',
|
||||
runningHintText: 'is running...🕞',
|
||||
openingSwitch: 'Opening switch',
|
||||
@ -1886,10 +1890,10 @@ Important structured information may include: names, dates, locations, events, k
|
||||
overwrite: 'Overwritten By',
|
||||
clear: 'Clear',
|
||||
set: 'Set',
|
||||
'+=': 'Add',
|
||||
'-=': 'Subtract',
|
||||
'*=': 'Multiply',
|
||||
'/=': 'Divide',
|
||||
add: 'Add',
|
||||
subtract: 'Subtract',
|
||||
multiply: 'Multiply',
|
||||
divide: 'Divide',
|
||||
append: 'Append',
|
||||
extend: 'Extend',
|
||||
removeFirst: 'Remove first',
|
||||
|
||||
@ -1102,9 +1102,14 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
messageMsg: '请输入消息或删除此字段。',
|
||||
addField: '新增字段',
|
||||
addMessage: '新增消息',
|
||||
loop: '循环上限',
|
||||
loopTip:
|
||||
loop: '循环',
|
||||
loopDescription:
|
||||
'loop为当前组件循环次数上限,当循环次数超过loop的值时,说明组件不能完成当前任务,请重新优化agent',
|
||||
exitLoop: '退出循环',
|
||||
exitLoopDescription: `等同于 "break"。此节点没有配置项。当循环体到达此节点时,循环终止。`,
|
||||
loopVariables: '循环变量',
|
||||
maximumLoopCount: '最大循环次数',
|
||||
loopTerminationCondition: '循环终止条件',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
key: '键',
|
||||
@ -1499,7 +1504,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
contentTip: 'content: 邮件内容(可选)',
|
||||
jsonUploadTypeErrorMessage: '请上传json文件',
|
||||
jsonUploadContentErrorMessage: 'json 文件错误',
|
||||
iteration: '循环',
|
||||
iteration: '迭代',
|
||||
iterationDescription: `该组件负责迭代生成新的内容,对列表对象执行多次步骤直至输出所有结果。`,
|
||||
delimiterTip: `该分隔符用于将输入文本分割成几个文本片段,每个文本片段的回显将作为每次迭代的输入项。`,
|
||||
delimiterOptions: {
|
||||
@ -1545,8 +1550,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
variableAssignerDescription:
|
||||
'此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。',
|
||||
variableAggregator: '变量聚合',
|
||||
variableAggregatorDescription: `将多路分支的变量聚合为一个变量,以实现下游节点统一配置。
|
||||
变量聚合节点(原变量赋值节点)是工作流程中的一个关键节点,它负责整合不同分支的输出结果,确保无论哪个分支被执行,其结果都能通过一个统一的变量来引用和访问。这在多分支的情况下非常有用,可将不同分支下相同作用的变量映射为一个输出变量,避免下游节点重复定义。`,
|
||||
variableAggregatorDescription: `该过程将来自多个分支的变量聚合到一个变量中,以实现下游节点的统一配置。`,
|
||||
inputVariables: '输入变量',
|
||||
addVariable: '新增变量',
|
||||
runningHintText: '正在运行中...🕞',
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
.image {
|
||||
width: 100px !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
max-width: 50vw;
|
||||
max-height: 50vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
.chunkText;
|
||||
}
|
||||
|
||||
.contentEllipsis {
|
||||
.multipleLineEllipsis(3);
|
||||
}
|
||||
|
||||
.contentText {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
.chunkCard {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cardSelected {
|
||||
background-color: @selectedBackgroundColor;
|
||||
}
|
||||
.cardSelectedDark {
|
||||
background-color: #ffffff2f;
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
import Image from '@/components/image';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { ChunkTextMode } from '../../constant';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
item: IChunk;
|
||||
checked: boolean;
|
||||
switchChunk: (available?: number, chunkIds?: string[]) => void;
|
||||
editChunk: (chunkId: string) => void;
|
||||
handleCheckboxClick: (chunkId: string, checked: boolean) => void;
|
||||
selected: boolean;
|
||||
clickChunkCard: (chunkId: string) => void;
|
||||
textMode: ChunkTextMode;
|
||||
}
|
||||
|
||||
const ChunkCard = ({
|
||||
item,
|
||||
checked,
|
||||
handleCheckboxClick,
|
||||
editChunk,
|
||||
switchChunk,
|
||||
selected,
|
||||
clickChunkCard,
|
||||
textMode,
|
||||
}: IProps) => {
|
||||
const available = Number(item.available_int);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const onChange = (checked: boolean) => {
|
||||
setEnabled(checked);
|
||||
switchChunk(available === 0 ? 1 : 0, [item.chunk_id]);
|
||||
};
|
||||
|
||||
const handleCheck: CheckboxProps['onChange'] = (e) => {
|
||||
handleCheckboxClick(item.chunk_id, e.target.checked);
|
||||
};
|
||||
|
||||
const handleContentDoubleClick = () => {
|
||||
editChunk(item.chunk_id);
|
||||
};
|
||||
|
||||
const handleContentClick = () => {
|
||||
clickChunkCard(item.chunk_id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setEnabled(available === 1);
|
||||
}, [available]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={classNames(styles.chunkCard, {
|
||||
[`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]:
|
||||
selected,
|
||||
})}
|
||||
>
|
||||
<Flex gap={'middle'} justify={'space-between'}>
|
||||
<Checkbox onChange={handleCheck} checked={checked}></Checkbox>
|
||||
{item.image_id && (
|
||||
<Popover
|
||||
placement="right"
|
||||
content={
|
||||
<Image id={item.image_id} className={styles.imagePreview}></Image>
|
||||
}
|
||||
>
|
||||
<Image id={item.image_id} className={styles.image}></Image>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<section
|
||||
onDoubleClick={handleContentDoubleClick}
|
||||
onClick={handleContentClick}
|
||||
className={styles.content}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(item.content_with_weight),
|
||||
}}
|
||||
className={classNames(styles.contentText, {
|
||||
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse,
|
||||
})}
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<Switch checked={enabled} onChange={onChange} />
|
||||
</div>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChunkCard;
|
||||
@ -1,140 +0,0 @@
|
||||
import EditTag from '@/components/edit-tag';
|
||||
import { useFetchChunk } from '@/hooks/chunk-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Divider, Form, Input, Modal, Space, Switch } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDeleteChunkByIds } from '../../hooks';
|
||||
import {
|
||||
transformTagFeaturesArrayToObject,
|
||||
transformTagFeaturesObjectToArray,
|
||||
} from '../../utils';
|
||||
import { TagFeatureItem } from './tag-feature-item';
|
||||
|
||||
type FieldType = Pick<
|
||||
IChunk,
|
||||
'content_with_weight' | 'tag_kwd' | 'question_kwd' | 'important_kwd'
|
||||
>;
|
||||
|
||||
interface kFProps {
|
||||
doc_id: string;
|
||||
chunkId: string | undefined;
|
||||
parserId: string;
|
||||
}
|
||||
|
||||
const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
|
||||
doc_id,
|
||||
chunkId,
|
||||
hideModal,
|
||||
onOk,
|
||||
loading,
|
||||
parserId,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [checked, setChecked] = useState(false);
|
||||
const { removeChunk } = useDeleteChunkByIds();
|
||||
const { data } = useFetchChunk(chunkId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isTagParser = parserId === 'tag';
|
||||
|
||||
const handleOk = useCallback(async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
console.log('🚀 ~ handleOk ~ values:', values);
|
||||
|
||||
onOk?.({
|
||||
...values,
|
||||
tag_feas: transformTagFeaturesArrayToObject(values.tag_feas),
|
||||
available_int: checked ? 1 : 0, // available_int
|
||||
});
|
||||
} catch (errorInfo) {
|
||||
console.log('Failed:', errorInfo);
|
||||
}
|
||||
}, [checked, form, onOk]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
if (chunkId) {
|
||||
return removeChunk([chunkId], doc_id);
|
||||
}
|
||||
}, [chunkId, doc_id, removeChunk]);
|
||||
|
||||
const handleCheck = useCallback(() => {
|
||||
setChecked(!checked);
|
||||
}, [checked]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.code === 0) {
|
||||
const { available_int, tag_feas } = data.data;
|
||||
form.setFieldsValue({
|
||||
...(data.data || {}),
|
||||
tag_feas: transformTagFeaturesObjectToArray(tag_feas),
|
||||
});
|
||||
|
||||
setChecked(available_int !== 0);
|
||||
}
|
||||
}, [data, form, chunkId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${chunkId ? t('common.edit') : t('common.create')} ${t('chunk.chunk')}`}
|
||||
open={true}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
okButtonProps={{ loading }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} autoComplete="off" layout={'vertical'}>
|
||||
<Form.Item<FieldType>
|
||||
label={t('chunk.chunk')}
|
||||
name="content_with_weight"
|
||||
rules={[{ required: true, message: t('chunk.chunkMessage') }]}
|
||||
>
|
||||
<Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType> label={t('chunk.keyword')} name="important_kwd">
|
||||
<EditTag></EditTag>
|
||||
</Form.Item>
|
||||
<Form.Item<FieldType>
|
||||
label={t('chunk.question')}
|
||||
name="question_kwd"
|
||||
tooltip={t('chunk.questionTip')}
|
||||
>
|
||||
<EditTag></EditTag>
|
||||
</Form.Item>
|
||||
{isTagParser && (
|
||||
<Form.Item<FieldType>
|
||||
label={t('knowledgeConfiguration.tagName')}
|
||||
name="tag_kwd"
|
||||
>
|
||||
<EditTag></EditTag>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{!isTagParser && <TagFeatureItem></TagFeatureItem>}
|
||||
</Form>
|
||||
|
||||
{chunkId && (
|
||||
<section>
|
||||
<Divider></Divider>
|
||||
<Space size={'large'}>
|
||||
<Switch
|
||||
checkedChildren={t('chunk.enabled')}
|
||||
unCheckedChildren={t('chunk.disabled')}
|
||||
onChange={handleCheck}
|
||||
checked={checked}
|
||||
/>
|
||||
|
||||
<span onClick={handleRemove}>
|
||||
<DeleteOutlined /> {t('common.delete')}
|
||||
</span>
|
||||
</Space>
|
||||
</section>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default ChunkCreatingModal;
|
||||
@ -1,107 +0,0 @@
|
||||
import {
|
||||
useFetchKnowledgeBaseConfiguration,
|
||||
useFetchTagListByKnowledgeIds,
|
||||
} from '@/hooks/knowledge-hooks';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, InputNumber, Select } from 'antd';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormListItem } from '../../utils';
|
||||
|
||||
const FieldKey = 'tag_feas';
|
||||
|
||||
export const TagFeatureItem = () => {
|
||||
const form = Form.useFormInstance();
|
||||
const { t } = useTranslation();
|
||||
const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
const { setKnowledgeIds, list } = useFetchTagListByKnowledgeIds();
|
||||
|
||||
const tagKnowledgeIds = useMemo(() => {
|
||||
return knowledgeConfiguration?.parser_config?.tag_kb_ids ?? [];
|
||||
}, [knowledgeConfiguration?.parser_config?.tag_kb_ids]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return list.map((x) => ({
|
||||
value: x[0],
|
||||
label: x[0],
|
||||
}));
|
||||
}, [list]);
|
||||
|
||||
const filterOptions = useCallback(
|
||||
(index: number) => {
|
||||
const tags: FormListItem[] = form.getFieldValue(FieldKey) ?? [];
|
||||
|
||||
// Exclude it's own current data
|
||||
const list = tags
|
||||
.filter((x, idx) => x && index !== idx)
|
||||
.map((x) => x.tag);
|
||||
|
||||
// Exclude the selected data from other options from one's own options.
|
||||
return options.filter((x) => !list.some((y) => x.value === y));
|
||||
},
|
||||
[form, options],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setKnowledgeIds(tagKnowledgeIds);
|
||||
}, [setKnowledgeIds, tagKnowledgeIds]);
|
||||
|
||||
return (
|
||||
<Form.Item label={t('knowledgeConfiguration.tags')}>
|
||||
<Form.List name={FieldKey} initialValue={[]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="flex gap-3 items-center">
|
||||
<div className="flex flex-1 gap-8">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'tag']}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') },
|
||||
]}
|
||||
className="w-2/3"
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder={t('knowledgeConfiguration.tagName')}
|
||||
options={filterOptions(name)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'frequency']}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseInput') },
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('knowledgeConfiguration.frequency')}
|
||||
max={10}
|
||||
min={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<MinusCircleOutlined
|
||||
onClick={() => remove(name)}
|
||||
className="mb-6"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{t('knowledgeConfiguration.addTag')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
@ -1,221 +0,0 @@
|
||||
import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
|
||||
import { KnowledgeRouteKey } from '@/constants/knowledge';
|
||||
import { IChunkListResult, useSelectChunkList } from '@/hooks/chunk-hooks';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
FilePdfOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Input,
|
||||
Menu,
|
||||
MenuProps,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Segmented,
|
||||
SegmentedProps,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Link } from 'umi';
|
||||
import { ChunkTextMode } from '../../constant';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps
|
||||
extends Pick<
|
||||
IChunkListResult,
|
||||
'searchString' | 'handleInputChange' | 'available' | 'handleSetAvailable'
|
||||
> {
|
||||
checked: boolean;
|
||||
selectAllChunk: (checked: boolean) => void;
|
||||
createChunk: () => void;
|
||||
removeChunk: () => void;
|
||||
switchChunk: (available: number) => void;
|
||||
changeChunkTextMode(mode: ChunkTextMode): void;
|
||||
}
|
||||
|
||||
const ChunkToolBar = ({
|
||||
selectAllChunk,
|
||||
checked,
|
||||
createChunk,
|
||||
removeChunk,
|
||||
switchChunk,
|
||||
changeChunkTextMode,
|
||||
available,
|
||||
handleSetAvailable,
|
||||
searchString,
|
||||
handleInputChange,
|
||||
}: IProps) => {
|
||||
const data = useSelectChunkList();
|
||||
const documentInfo = data?.documentInfo;
|
||||
const knowledgeBaseId = useKnowledgeBaseId();
|
||||
const [isShowSearchBox, setIsShowSearchBox] = useState(false);
|
||||
const { t } = useTranslate('chunk');
|
||||
|
||||
const handleSelectAllCheck = useCallback(
|
||||
(e: any) => {
|
||||
selectAllChunk(e.target.checked);
|
||||
},
|
||||
[selectAllChunk],
|
||||
);
|
||||
|
||||
const handleSearchIconClick = () => {
|
||||
setIsShowSearchBox(true);
|
||||
};
|
||||
|
||||
const handleSearchBlur = () => {
|
||||
if (!searchString?.trim()) {
|
||||
setIsShowSearchBox(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
removeChunk();
|
||||
}, [removeChunk]);
|
||||
|
||||
const handleEnabledClick = useCallback(() => {
|
||||
switchChunk(1);
|
||||
}, [switchChunk]);
|
||||
|
||||
const handleDisabledClick = useCallback(() => {
|
||||
switchChunk(0);
|
||||
}, [switchChunk]);
|
||||
|
||||
const items: MenuProps['items'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<>
|
||||
<Checkbox onChange={handleSelectAllCheck} checked={checked}>
|
||||
<b>{t('selectAll')}</b>
|
||||
</Checkbox>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<Space onClick={handleEnabledClick}>
|
||||
<CheckCircleOutlined />
|
||||
<b>{t('enabledSelected')}</b>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<Space onClick={handleDisabledClick}>
|
||||
<CloseCircleOutlined />
|
||||
<b>{t('disabledSelected')}</b>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '4',
|
||||
label: (
|
||||
<Space onClick={handleDelete}>
|
||||
<DeleteOutlined />
|
||||
<b>{t('deleteSelected')}</b>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
checked,
|
||||
handleSelectAllCheck,
|
||||
handleDelete,
|
||||
handleEnabledClick,
|
||||
handleDisabledClick,
|
||||
t,
|
||||
]);
|
||||
|
||||
const content = (
|
||||
<Menu style={{ width: 200 }} items={items} selectable={false} />
|
||||
);
|
||||
|
||||
const handleFilterChange = (e: RadioChangeEvent) => {
|
||||
selectAllChunk(false);
|
||||
handleSetAvailable(e.target.value);
|
||||
};
|
||||
|
||||
const filterContent = (
|
||||
<Radio.Group onChange={handleFilterChange} value={available}>
|
||||
<Space direction="vertical">
|
||||
<Radio value={undefined}>{t('all')}</Radio>
|
||||
<Radio value={1}>{t('enabled')}</Radio>
|
||||
<Radio value={0}>{t('disabled')}</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Space size={'middle'}>
|
||||
<Link
|
||||
to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
</Link>
|
||||
<FilePdfOutlined />
|
||||
<Text ellipsis={{ tooltip: documentInfo?.name }} style={{ width: 150 }}>
|
||||
{documentInfo?.name}
|
||||
</Text>
|
||||
</Space>
|
||||
<Space>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: t(ChunkTextMode.Full), value: ChunkTextMode.Full },
|
||||
{ label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse },
|
||||
]}
|
||||
onChange={changeChunkTextMode as SegmentedProps['onChange']}
|
||||
/>
|
||||
<Popover content={content} placement="bottom" arrow={false}>
|
||||
<Button>
|
||||
{t('bulk')}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
{isShowSearchBox ? (
|
||||
<Input
|
||||
size="middle"
|
||||
placeholder={t('search')}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleSearchBlur}
|
||||
value={searchString}
|
||||
/>
|
||||
) : (
|
||||
<Button icon={<SearchOutlined />} onClick={handleSearchIconClick} />
|
||||
)}
|
||||
|
||||
<Popover content={filterContent} placement="bottom" arrow={false}>
|
||||
<Button icon={<FilterIcon />} />
|
||||
</Popover>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={() => createChunk()}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChunkToolBar;
|
||||
@ -1,55 +0,0 @@
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
||||
import { api_host } from '@/utils/api';
|
||||
import { useSize } from 'ahooks';
|
||||
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const useDocumentResizeObserver = () => {
|
||||
const [containerWidth, setContainerWidth] = useState<number>();
|
||||
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
|
||||
const size = useSize(containerRef);
|
||||
|
||||
const onResize = useCallback((width?: number) => {
|
||||
if (width) {
|
||||
setContainerWidth(width);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onResize(size?.width);
|
||||
}, [size?.width, onResize]);
|
||||
|
||||
return { containerWidth, setContainerRef };
|
||||
};
|
||||
|
||||
function highlightPattern(text: string, pattern: string, pageNumber: number) {
|
||||
if (pageNumber === 2) {
|
||||
return `<mark>${text}</mark>`;
|
||||
}
|
||||
if (text.trim() !== '' && pattern.match(text)) {
|
||||
// return pattern.replace(text, (value) => `<mark>${value}</mark>`);
|
||||
return `<mark>${text}</mark>`;
|
||||
}
|
||||
return text.replace(pattern, (value) => `<mark>${value}</mark>`);
|
||||
}
|
||||
|
||||
export const useHighlightText = (searchText: string = '') => {
|
||||
const textRenderer: CustomTextRenderer = useCallback(
|
||||
(textItem) => {
|
||||
return highlightPattern(textItem.str, searchText, textItem.pageNumber);
|
||||
},
|
||||
[searchText],
|
||||
);
|
||||
|
||||
return textRenderer;
|
||||
};
|
||||
|
||||
export const useGetDocumentUrl = () => {
|
||||
const { documentId } = useGetKnowledgeSearchParams();
|
||||
|
||||
const url = useMemo(() => {
|
||||
return `${api_host}/document/get/${documentId}`;
|
||||
}, [documentId]);
|
||||
|
||||
return url;
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
.documentContainer {
|
||||
width: 100%;
|
||||
height: calc(100vh - 284px);
|
||||
position: relative;
|
||||
:global(.PdfHighlighter) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
:global(.Highlight--scrolledTo .Highlight__part) {
|
||||
overflow-x: hidden;
|
||||
background-color: rgba(255, 226, 143, 1);
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import {
|
||||
AreaHighlight,
|
||||
Highlight,
|
||||
IHighlight,
|
||||
PdfHighlighter,
|
||||
PdfLoader,
|
||||
Popup,
|
||||
} from 'react-pdf-highlighter';
|
||||
import { useGetDocumentUrl } from './hooks';
|
||||
|
||||
import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
|
||||
import FileError from '@/pages/document-viewer/file-error';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
highlights: IHighlight[];
|
||||
setWidthAndHeight: (width: number, height: number) => void;
|
||||
}
|
||||
const HighlightPopup = ({
|
||||
comment,
|
||||
}: {
|
||||
comment: { text: string; emoji: string };
|
||||
}) =>
|
||||
comment.text ? (
|
||||
<div className="Highlight__popup">
|
||||
{comment.emoji} {comment.text}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// TODO: merge with DocumentPreviewer
|
||||
const Preview = ({ highlights: state, setWidthAndHeight }: IProps) => {
|
||||
const url = useGetDocumentUrl();
|
||||
|
||||
const ref = useRef<(highlight: IHighlight) => void>(() => {});
|
||||
const error = useCatchDocumentError(url);
|
||||
|
||||
const resetHash = () => {};
|
||||
|
||||
useEffect(() => {
|
||||
if (state.length > 0) {
|
||||
ref?.current(state[0]);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div className={styles.documentContainer}>
|
||||
<PdfLoader
|
||||
url={url}
|
||||
beforeLoad={<Skeleton active />}
|
||||
workerSrc="/pdfjs-dist/pdf.worker.min.js"
|
||||
errorMessage={<FileError>{error}</FileError>}
|
||||
>
|
||||
{(pdfDocument) => {
|
||||
pdfDocument.getPage(1).then((page) => {
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
setWidthAndHeight(width, height);
|
||||
});
|
||||
|
||||
return (
|
||||
<PdfHighlighter
|
||||
pdfDocument={pdfDocument}
|
||||
enableAreaSelection={(event) => event.altKey}
|
||||
onScrollChange={resetHash}
|
||||
scrollRef={(scrollTo) => {
|
||||
ref.current = scrollTo;
|
||||
}}
|
||||
onSelectionFinished={() => null}
|
||||
highlightTransform={(
|
||||
highlight,
|
||||
index,
|
||||
setTip,
|
||||
hideTip,
|
||||
viewportToScaled,
|
||||
screenshot,
|
||||
isScrolledTo,
|
||||
) => {
|
||||
const isTextHighlight = !Boolean(
|
||||
highlight.content && highlight.content.image,
|
||||
);
|
||||
|
||||
const component = isTextHighlight ? (
|
||||
<Highlight
|
||||
isScrolledTo={isScrolledTo}
|
||||
position={highlight.position}
|
||||
comment={highlight.comment}
|
||||
/>
|
||||
) : (
|
||||
<AreaHighlight
|
||||
isScrolledTo={isScrolledTo}
|
||||
highlight={highlight}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
popupContent={<HighlightPopup {...highlight} />}
|
||||
onMouseOver={(popupContent) =>
|
||||
setTip(highlight, () => popupContent)
|
||||
}
|
||||
onMouseOut={hideTip}
|
||||
key={index}
|
||||
>
|
||||
{component}
|
||||
</Popup>
|
||||
);
|
||||
}}
|
||||
highlights={state}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</PdfLoader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Preview);
|
||||
@ -1,4 +0,0 @@
|
||||
export enum ChunkTextMode {
|
||||
Full = 'full',
|
||||
Ellipse = 'ellipse',
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
import {
|
||||
useCreateChunk,
|
||||
useDeleteChunk,
|
||||
useSelectChunkList,
|
||||
} from '@/hooks/chunk-hooks';
|
||||
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { buildChunkHighlights } from '@/utils/document-util';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { IHighlight } from 'react-pdf-highlighter';
|
||||
import { ChunkTextMode } from './constant';
|
||||
|
||||
export const useHandleChunkCardClick = () => {
|
||||
const [selectedChunkId, setSelectedChunkId] = useState<string>('');
|
||||
|
||||
const handleChunkCardClick = useCallback((chunkId: string) => {
|
||||
setSelectedChunkId(chunkId);
|
||||
}, []);
|
||||
|
||||
return { handleChunkCardClick, selectedChunkId };
|
||||
};
|
||||
|
||||
export const useGetSelectedChunk = (selectedChunkId: string) => {
|
||||
const data = useSelectChunkList();
|
||||
return (
|
||||
data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk)
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetChunkHighlights = (selectedChunkId: string) => {
|
||||
const [size, setSize] = useState({ width: 849, height: 1200 });
|
||||
const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId);
|
||||
|
||||
const highlights: IHighlight[] = useMemo(() => {
|
||||
return buildChunkHighlights(selectedChunk, size);
|
||||
}, [selectedChunk, size]);
|
||||
|
||||
const setWidthAndHeight = useCallback((width: number, height: number) => {
|
||||
setSize((pre) => {
|
||||
if (pre.height !== height || pre.width !== width) {
|
||||
return { height, width };
|
||||
}
|
||||
return pre;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { highlights, setWidthAndHeight };
|
||||
};
|
||||
|
||||
// Switch chunk text to be fully displayed or ellipse
|
||||
export const useChangeChunkTextMode = () => {
|
||||
const [textMode, setTextMode] = useState<ChunkTextMode>(ChunkTextMode.Full);
|
||||
|
||||
const changeChunkTextMode = useCallback((mode: ChunkTextMode) => {
|
||||
setTextMode(mode);
|
||||
}, []);
|
||||
|
||||
return { textMode, changeChunkTextMode };
|
||||
};
|
||||
|
||||
export const useDeleteChunkByIds = (): {
|
||||
removeChunk: (chunkIds: string[], documentId: string) => Promise<number>;
|
||||
} => {
|
||||
const { deleteChunk } = useDeleteChunk();
|
||||
const showDeleteConfirm = useShowDeleteConfirm();
|
||||
|
||||
const removeChunk = useCallback(
|
||||
(chunkIds: string[], documentId: string) => () => {
|
||||
return deleteChunk({ chunkIds, doc_id: documentId });
|
||||
},
|
||||
[deleteChunk],
|
||||
);
|
||||
|
||||
const onRemoveChunk = useCallback(
|
||||
(chunkIds: string[], documentId: string): Promise<number> => {
|
||||
return showDeleteConfirm({ onOk: removeChunk(chunkIds, documentId) });
|
||||
},
|
||||
[removeChunk, showDeleteConfirm],
|
||||
);
|
||||
|
||||
return {
|
||||
removeChunk: onRemoveChunk,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdateChunk = () => {
|
||||
const [chunkId, setChunkId] = useState<string | undefined>('');
|
||||
const {
|
||||
visible: chunkUpdatingVisible,
|
||||
hideModal: hideChunkUpdatingModal,
|
||||
showModal,
|
||||
} = useSetModalState();
|
||||
const { createChunk, loading } = useCreateChunk();
|
||||
const { documentId } = useGetKnowledgeSearchParams();
|
||||
|
||||
const onChunkUpdatingOk = useCallback(
|
||||
async (params: IChunk) => {
|
||||
const code = await createChunk({
|
||||
...params,
|
||||
doc_id: documentId,
|
||||
chunk_id: chunkId,
|
||||
});
|
||||
|
||||
if (code === 0) {
|
||||
hideChunkUpdatingModal();
|
||||
}
|
||||
},
|
||||
[createChunk, hideChunkUpdatingModal, chunkId, documentId],
|
||||
);
|
||||
|
||||
const handleShowChunkUpdatingModal = useCallback(
|
||||
async (id?: string) => {
|
||||
setChunkId(id);
|
||||
showModal();
|
||||
},
|
||||
[showModal],
|
||||
);
|
||||
|
||||
return {
|
||||
chunkUpdatingLoading: loading,
|
||||
onChunkUpdatingOk,
|
||||
chunkUpdatingVisible,
|
||||
hideChunkUpdatingModal,
|
||||
showChunkUpdatingModal: handleShowChunkUpdatingModal,
|
||||
chunkId,
|
||||
documentId,
|
||||
};
|
||||
};
|
||||
@ -1,92 +0,0 @@
|
||||
.chunkPage {
|
||||
padding: 24px;
|
||||
|
||||
display: flex;
|
||||
// height: calc(100vh - 112px);
|
||||
flex-direction: column;
|
||||
|
||||
.filter {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pagePdfWrapper {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.pageWrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pageContent {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
.spin {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.documentPreview {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chunkContainer {
|
||||
display: flex;
|
||||
height: calc(100vh - 332px);
|
||||
}
|
||||
|
||||
.chunkOtherContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pageFooter {
|
||||
padding-top: 10px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.context {
|
||||
flex: 1;
|
||||
// width: 207px;
|
||||
height: 88px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 20px;
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
:global {
|
||||
.ant-card-body {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -1,202 +0,0 @@
|
||||
import { useFetchNextChunkList, useSwitchChunk } from '@/hooks/chunk-hooks';
|
||||
import type { PaginationProps } from 'antd';
|
||||
import { Divider, Flex, Pagination, Space, Spin, message } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ChunkCard from './components/chunk-card';
|
||||
import CreatingModal from './components/chunk-creating-modal';
|
||||
import ChunkToolBar from './components/chunk-toolbar';
|
||||
import DocumentPreview from './components/document-preview/preview';
|
||||
import {
|
||||
useChangeChunkTextMode,
|
||||
useDeleteChunkByIds,
|
||||
useGetChunkHighlights,
|
||||
useHandleChunkCardClick,
|
||||
useUpdateChunk,
|
||||
} from './hooks';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const Chunk = () => {
|
||||
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
|
||||
const { removeChunk } = useDeleteChunkByIds();
|
||||
const {
|
||||
data: { documentInfo, data = [], total },
|
||||
pagination,
|
||||
loading,
|
||||
searchString,
|
||||
handleInputChange,
|
||||
available,
|
||||
handleSetAvailable,
|
||||
} = useFetchNextChunkList();
|
||||
const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
|
||||
const isPdf = documentInfo?.type === 'pdf';
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { changeChunkTextMode, textMode } = useChangeChunkTextMode();
|
||||
const { switchChunk } = useSwitchChunk();
|
||||
const {
|
||||
chunkUpdatingLoading,
|
||||
onChunkUpdatingOk,
|
||||
showChunkUpdatingModal,
|
||||
hideChunkUpdatingModal,
|
||||
chunkId,
|
||||
chunkUpdatingVisible,
|
||||
documentId,
|
||||
} = useUpdateChunk();
|
||||
|
||||
const onPaginationChange: PaginationProps['onShowSizeChange'] = (
|
||||
page,
|
||||
size,
|
||||
) => {
|
||||
setSelectedChunkIds([]);
|
||||
pagination.onChange?.(page, size);
|
||||
};
|
||||
|
||||
const selectAllChunk = useCallback(
|
||||
(checked: boolean) => {
|
||||
setSelectedChunkIds(checked ? data.map((x) => x.chunk_id) : []);
|
||||
},
|
||||
[data],
|
||||
);
|
||||
|
||||
const handleSingleCheckboxClick = useCallback(
|
||||
(chunkId: string, checked: boolean) => {
|
||||
setSelectedChunkIds((previousIds) => {
|
||||
const idx = previousIds.findIndex((x) => x === chunkId);
|
||||
const nextIds = [...previousIds];
|
||||
if (checked && idx === -1) {
|
||||
nextIds.push(chunkId);
|
||||
} else if (!checked && idx !== -1) {
|
||||
nextIds.splice(idx, 1);
|
||||
}
|
||||
return nextIds;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const showSelectedChunkWarning = useCallback(() => {
|
||||
message.warning(t('message.pleaseSelectChunk'));
|
||||
}, [t]);
|
||||
|
||||
const handleRemoveChunk = useCallback(async () => {
|
||||
if (selectedChunkIds.length > 0) {
|
||||
const resCode: number = await removeChunk(selectedChunkIds, documentId);
|
||||
if (resCode === 0) {
|
||||
setSelectedChunkIds([]);
|
||||
}
|
||||
} else {
|
||||
showSelectedChunkWarning();
|
||||
}
|
||||
}, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]);
|
||||
|
||||
const handleSwitchChunk = useCallback(
|
||||
async (available?: number, chunkIds?: string[]) => {
|
||||
let ids = chunkIds;
|
||||
if (!chunkIds) {
|
||||
ids = selectedChunkIds;
|
||||
if (selectedChunkIds.length === 0) {
|
||||
showSelectedChunkWarning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const resCode: number = await switchChunk({
|
||||
chunk_ids: ids,
|
||||
available_int: available,
|
||||
doc_id: documentId,
|
||||
});
|
||||
if (!chunkIds && resCode === 0) {
|
||||
}
|
||||
},
|
||||
[switchChunk, documentId, selectedChunkIds, showSelectedChunkWarning],
|
||||
);
|
||||
|
||||
const { highlights, setWidthAndHeight } =
|
||||
useGetChunkHighlights(selectedChunkId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.chunkPage}>
|
||||
<ChunkToolBar
|
||||
selectAllChunk={selectAllChunk}
|
||||
createChunk={showChunkUpdatingModal}
|
||||
removeChunk={handleRemoveChunk}
|
||||
checked={selectedChunkIds.length === data.length}
|
||||
switchChunk={handleSwitchChunk}
|
||||
changeChunkTextMode={changeChunkTextMode}
|
||||
searchString={searchString}
|
||||
handleInputChange={handleInputChange}
|
||||
available={available}
|
||||
handleSetAvailable={handleSetAvailable}
|
||||
></ChunkToolBar>
|
||||
<Divider></Divider>
|
||||
<Flex flex={1} gap={'middle'}>
|
||||
<Flex
|
||||
vertical
|
||||
className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper}
|
||||
>
|
||||
<Spin spinning={loading} className={styles.spin} size="large">
|
||||
<div className={styles.pageContent}>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={'middle'}
|
||||
className={classNames(styles.chunkContainer, {
|
||||
[styles.chunkOtherContainer]: !isPdf,
|
||||
})}
|
||||
>
|
||||
{data.map((item) => (
|
||||
<ChunkCard
|
||||
item={item}
|
||||
key={item.chunk_id}
|
||||
editChunk={showChunkUpdatingModal}
|
||||
checked={selectedChunkIds.some(
|
||||
(x) => x === item.chunk_id,
|
||||
)}
|
||||
handleCheckboxClick={handleSingleCheckboxClick}
|
||||
switchChunk={handleSwitchChunk}
|
||||
clickChunkCard={handleChunkCardClick}
|
||||
selected={item.chunk_id === selectedChunkId}
|
||||
textMode={textMode}
|
||||
></ChunkCard>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Spin>
|
||||
<div className={styles.pageFooter}>
|
||||
<Pagination
|
||||
{...pagination}
|
||||
total={total}
|
||||
size={'small'}
|
||||
onChange={onPaginationChange}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
{isPdf && (
|
||||
<section className={styles.documentPreview}>
|
||||
<DocumentPreview
|
||||
highlights={highlights}
|
||||
setWidthAndHeight={setWidthAndHeight}
|
||||
></DocumentPreview>
|
||||
</section>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
{chunkUpdatingVisible && (
|
||||
<CreatingModal
|
||||
doc_id={documentId}
|
||||
chunkId={chunkId}
|
||||
hideModal={hideChunkUpdatingModal}
|
||||
visible={chunkUpdatingVisible}
|
||||
loading={chunkUpdatingLoading}
|
||||
onOk={onChunkUpdatingOk}
|
||||
parserId={documentInfo.parser_id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chunk;
|
||||
@ -1,24 +0,0 @@
|
||||
export type FormListItem = {
|
||||
frequency: number;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export function transformTagFeaturesArrayToObject(
|
||||
list: Array<FormListItem> = [],
|
||||
) {
|
||||
return list.reduce<Record<string, number>>((pre, cur) => {
|
||||
pre[cur.tag] = cur.frequency;
|
||||
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function transformTagFeaturesObjectToArray(
|
||||
object: Record<string, number> = {},
|
||||
) {
|
||||
return Object.keys(object).reduce<Array<FormListItem>>((pre, key) => {
|
||||
pre.push({ frequency: object[key], tag: key });
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { Outlet } from 'umi';
|
||||
|
||||
export const KnowledgeDataset = () => {
|
||||
return <Outlet></Outlet>;
|
||||
};
|
||||
|
||||
export default KnowledgeDataset;
|
||||
@ -1,17 +0,0 @@
|
||||
import { RunningStatus } from '@/constants/knowledge';
|
||||
|
||||
export const RunningStatusMap = {
|
||||
[RunningStatus.UNSTART]: {
|
||||
label: 'UNSTART',
|
||||
color: 'cyan',
|
||||
},
|
||||
[RunningStatus.RUNNING]: {
|
||||
label: 'Parsing',
|
||||
color: 'blue',
|
||||
},
|
||||
[RunningStatus.CANCEL]: { label: 'CANCEL', color: 'orange' },
|
||||
[RunningStatus.DONE]: { label: 'SUCCESS', color: 'geekblue' },
|
||||
[RunningStatus.FAIL]: { label: 'FAIL', color: 'red' },
|
||||
};
|
||||
|
||||
export * from '@/constants/knowledge';
|
||||
@ -1,49 +0,0 @@
|
||||
import { IModalManagerChildrenProps } from '@/components/modal-manager';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
type FieldType = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
|
||||
loading: boolean;
|
||||
onOk: (name: string) => void;
|
||||
showModal?(): void;
|
||||
}
|
||||
|
||||
const FileCreatingModal: React.FC<IProps> = ({ visible, hideModal, onOk }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
onOk(values.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="File Name"
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
name="validateOnly"
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="File Name"
|
||||
name="name"
|
||||
rules={[{ required: true, message: 'Please input name!' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default FileCreatingModal;
|
||||
@ -1,240 +0,0 @@
|
||||
import { ReactComponent as CancelIcon } from '@/assets/svg/cancel.svg';
|
||||
import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
|
||||
import { ReactComponent as DisableIcon } from '@/assets/svg/disable.svg';
|
||||
import { ReactComponent as EnableIcon } from '@/assets/svg/enable.svg';
|
||||
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
|
||||
import { useShowDeleteConfirm, useTranslate } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useRemoveNextDocument,
|
||||
useRunNextDocument,
|
||||
useSetNextDocumentStatus,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import {
|
||||
DownOutlined,
|
||||
FileOutlined,
|
||||
FileTextOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Dropdown, Flex, Input, MenuProps, Space } from 'antd';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { RunningStatus } from './constant';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
selectedRowKeys: string[];
|
||||
showCreateModal(): void;
|
||||
showWebCrawlModal(): void;
|
||||
showDocumentUploadModal(): void;
|
||||
searchString: string;
|
||||
handleInputChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||
documents: IDocumentInfo[];
|
||||
}
|
||||
|
||||
const DocumentToolbar = ({
|
||||
searchString,
|
||||
selectedRowKeys,
|
||||
showCreateModal,
|
||||
showDocumentUploadModal,
|
||||
handleInputChange,
|
||||
documents,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const { removeDocument } = useRemoveNextDocument();
|
||||
const showDeleteConfirm = useShowDeleteConfirm();
|
||||
const { runDocumentByIds } = useRunNextDocument();
|
||||
const { setDocumentStatus } = useSetNextDocumentStatus();
|
||||
|
||||
const actionItems: MenuProps['items'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: '1',
|
||||
onClick: showDocumentUploadModal,
|
||||
label: (
|
||||
<div>
|
||||
<Button type="link">
|
||||
<Space>
|
||||
<FileTextOutlined />
|
||||
{t('localFiles')}
|
||||
</Space>
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '3',
|
||||
onClick: showCreateModal,
|
||||
label: (
|
||||
<div>
|
||||
<Button type="link">
|
||||
<FileOutlined />
|
||||
{t('emptyFiles')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [showDocumentUploadModal, showCreateModal, t]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const deletedKeys = selectedRowKeys.filter(
|
||||
(x) =>
|
||||
!documents
|
||||
.filter((y) => y.run === RunningStatus.RUNNING)
|
||||
.some((y) => y.id === x),
|
||||
);
|
||||
if (deletedKeys.length === 0) {
|
||||
toast.error(t('theDocumentBeingParsedCannotBeDeleted'));
|
||||
return;
|
||||
}
|
||||
showDeleteConfirm({
|
||||
onOk: () => {
|
||||
removeDocument(deletedKeys);
|
||||
},
|
||||
});
|
||||
}, [selectedRowKeys, showDeleteConfirm, documents, t, removeDocument]);
|
||||
|
||||
const runDocument = useCallback(
|
||||
(run: number) => {
|
||||
runDocumentByIds({
|
||||
documentIds: selectedRowKeys,
|
||||
run,
|
||||
shouldDelete: false,
|
||||
});
|
||||
},
|
||||
[runDocumentByIds, selectedRowKeys],
|
||||
);
|
||||
|
||||
const handleRunClick = useCallback(() => {
|
||||
runDocument(1);
|
||||
}, [runDocument]);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
runDocument(2);
|
||||
}, [runDocument]);
|
||||
|
||||
const onChangeStatus = useCallback(
|
||||
(enabled: boolean) => {
|
||||
selectedRowKeys.forEach((id) => {
|
||||
setDocumentStatus({ status: enabled, documentId: id });
|
||||
});
|
||||
},
|
||||
[selectedRowKeys, setDocumentStatus],
|
||||
);
|
||||
|
||||
const handleEnableClick = useCallback(() => {
|
||||
onChangeStatus(true);
|
||||
}, [onChangeStatus]);
|
||||
|
||||
const handleDisableClick = useCallback(() => {
|
||||
onChangeStatus(false);
|
||||
}, [onChangeStatus]);
|
||||
|
||||
const disabled = selectedRowKeys.length === 0;
|
||||
|
||||
const items: MenuProps['items'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: '0',
|
||||
onClick: handleEnableClick,
|
||||
label: (
|
||||
<Flex gap={10}>
|
||||
<EnableIcon></EnableIcon>
|
||||
<b>{t('enabled')}</b>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
onClick: handleDisableClick,
|
||||
label: (
|
||||
<Flex gap={10}>
|
||||
<DisableIcon></DisableIcon>
|
||||
<b>{t('disabled')}</b>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '2',
|
||||
onClick: handleRunClick,
|
||||
label: (
|
||||
<Flex gap={10}>
|
||||
<RunIcon></RunIcon>
|
||||
<b>{t('run')}</b>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
onClick: handleCancelClick,
|
||||
label: (
|
||||
<Flex gap={10}>
|
||||
<CancelIcon />
|
||||
<b>{t('cancel')}</b>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '4',
|
||||
onClick: handleDelete,
|
||||
label: (
|
||||
<Flex gap={10}>
|
||||
<span className={styles.deleteIconWrapper}>
|
||||
<DeleteIcon width={18} />
|
||||
</span>
|
||||
<b>{t('delete', { keyPrefix: 'common' })}</b>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
handleDelete,
|
||||
handleRunClick,
|
||||
handleCancelClick,
|
||||
t,
|
||||
handleDisableClick,
|
||||
handleEnableClick,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.filter}>
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
placement="bottom"
|
||||
arrow={false}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button>
|
||||
<Space>
|
||||
<b> {t('bulk')}</b>
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Space>
|
||||
<Input
|
||||
placeholder={t('searchFiles')}
|
||||
value={searchString}
|
||||
style={{ width: 220 }}
|
||||
allowClear
|
||||
onChange={handleInputChange}
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
|
||||
<Dropdown menu={{ items: actionItems }} trigger={['click']}>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
{t('addFile')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentToolbar;
|
||||
@ -1,364 +0,0 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useCreateNextDocument,
|
||||
useNextWebCrawl,
|
||||
useRunNextDocument,
|
||||
useSaveNextDocumentName,
|
||||
useSetDocumentMeta,
|
||||
useSetNextDocumentParser,
|
||||
useUploadNextDocument,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
||||
import { UploadFile } from 'antd';
|
||||
import { TableRowSelection } from 'antd/es/table/interface';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'umi';
|
||||
import { KnowledgeRouteKey } from './constant';
|
||||
|
||||
export const useNavigateToOtherPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { knowledgeId } = useGetKnowledgeSearchParams();
|
||||
|
||||
const linkToUploadPage = useCallback(() => {
|
||||
navigate(`/knowledge/dataset/upload?id=${knowledgeId}`);
|
||||
}, [navigate, knowledgeId]);
|
||||
|
||||
const toChunk = useCallback(
|
||||
(id: string) => {
|
||||
navigate(
|
||||
`/knowledge/${KnowledgeRouteKey.Dataset}/chunk?id=${knowledgeId}&doc_id=${id}`,
|
||||
);
|
||||
},
|
||||
[navigate, knowledgeId],
|
||||
);
|
||||
|
||||
return { linkToUploadPage, toChunk };
|
||||
};
|
||||
|
||||
export const useRenameDocument = (documentId: string) => {
|
||||
const { saveName, loading } = useSaveNextDocumentName();
|
||||
|
||||
const {
|
||||
visible: renameVisible,
|
||||
hideModal: hideRenameModal,
|
||||
showModal: showRenameModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onRenameOk = useCallback(
|
||||
async (name: string) => {
|
||||
const ret = await saveName({ documentId, name });
|
||||
if (ret === 0) {
|
||||
hideRenameModal();
|
||||
}
|
||||
},
|
||||
[hideRenameModal, saveName, documentId],
|
||||
);
|
||||
|
||||
return {
|
||||
renameLoading: loading,
|
||||
onRenameOk,
|
||||
renameVisible,
|
||||
hideRenameModal,
|
||||
showRenameModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCreateEmptyDocument = () => {
|
||||
const { createDocument, loading } = useCreateNextDocument();
|
||||
|
||||
const {
|
||||
visible: createVisible,
|
||||
hideModal: hideCreateModal,
|
||||
showModal: showCreateModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onCreateOk = useCallback(
|
||||
async (name: string) => {
|
||||
const ret = await createDocument(name);
|
||||
if (ret === 0) {
|
||||
hideCreateModal();
|
||||
}
|
||||
},
|
||||
[hideCreateModal, createDocument],
|
||||
);
|
||||
|
||||
return {
|
||||
createLoading: loading,
|
||||
onCreateOk,
|
||||
createVisible,
|
||||
hideCreateModal,
|
||||
showCreateModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useChangeDocumentParser = (documentId: string) => {
|
||||
const { setDocumentParser, loading } = useSetNextDocumentParser();
|
||||
|
||||
const {
|
||||
visible: changeParserVisible,
|
||||
hideModal: hideChangeParserModal,
|
||||
showModal: showChangeParserModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onChangeParserOk = useCallback(
|
||||
async (parserId: string, parserConfig: IChangeParserConfigRequestBody) => {
|
||||
const ret = await setDocumentParser({
|
||||
parserId,
|
||||
documentId,
|
||||
parserConfig,
|
||||
});
|
||||
if (ret === 0) {
|
||||
hideChangeParserModal();
|
||||
}
|
||||
},
|
||||
[hideChangeParserModal, setDocumentParser, documentId],
|
||||
);
|
||||
|
||||
return {
|
||||
changeParserLoading: loading,
|
||||
onChangeParserOk,
|
||||
changeParserVisible,
|
||||
hideChangeParserModal,
|
||||
showChangeParserModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useGetRowSelection = () => {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const rowSelection: TableRowSelection<IDocumentInfo> = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys: React.Key[]) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys);
|
||||
},
|
||||
};
|
||||
|
||||
return rowSelection;
|
||||
};
|
||||
|
||||
export const useHandleUploadDocument = () => {
|
||||
const {
|
||||
visible: documentUploadVisible,
|
||||
hideModal: hideDocumentUploadModal,
|
||||
showModal: showDocumentUploadModal,
|
||||
} = useSetModalState();
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||
const { uploadDocument, loading } = useUploadNextDocument();
|
||||
const { runDocumentByIds } = useRunNextDocument();
|
||||
|
||||
const onDocumentUploadOk = useCallback(
|
||||
async ({
|
||||
parseOnCreation,
|
||||
directoryFileList,
|
||||
}: {
|
||||
directoryFileList: UploadFile[];
|
||||
parseOnCreation: boolean;
|
||||
}): Promise<number | undefined> => {
|
||||
const processFileGroup = async (filesPart: UploadFile[]) => {
|
||||
// set status to uploading on files
|
||||
setFileList(
|
||||
fileList.map((file) => {
|
||||
if (!filesPart.includes(file)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
let newFile = file;
|
||||
newFile.status = 'uploading';
|
||||
newFile.percent = 1;
|
||||
return newFile;
|
||||
}),
|
||||
);
|
||||
|
||||
const ret = await uploadDocument(filesPart);
|
||||
|
||||
const files = ret?.data || [];
|
||||
const successfulFilenames = files.map((file: any) => file.name);
|
||||
|
||||
// set status to done or error on files (based on response)
|
||||
setFileList(
|
||||
fileList.map((file) => {
|
||||
if (!filesPart.includes(file)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
let newFile = file;
|
||||
newFile.status = successfulFilenames.includes(file.name)
|
||||
? 'done'
|
||||
: 'error';
|
||||
newFile.percent = 100;
|
||||
newFile.response = ret.message;
|
||||
return newFile;
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
code: ret?.code,
|
||||
fileIds: files.map((file: any) => file.id),
|
||||
totalSuccess: successfulFilenames.length,
|
||||
};
|
||||
};
|
||||
const totalFiles = fileList.length;
|
||||
|
||||
if (directoryFileList.length > 0) {
|
||||
const ret = await uploadDocument(directoryFileList);
|
||||
if (ret?.code === 0) {
|
||||
hideDocumentUploadModal();
|
||||
}
|
||||
if (totalFiles === 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFiles === 0) {
|
||||
console.log('No files to upload');
|
||||
hideDocumentUploadModal();
|
||||
return 0;
|
||||
}
|
||||
|
||||
let totalSuccess = 0;
|
||||
let codes = [];
|
||||
let toRunFileIds: any[] = [];
|
||||
for (let i = 0; i < totalFiles; i += 10) {
|
||||
setUploadProgress(Math.floor((i / totalFiles) * 100));
|
||||
const files = fileList.slice(i, i + 10);
|
||||
const {
|
||||
code,
|
||||
totalSuccess: count,
|
||||
fileIds,
|
||||
} = await processFileGroup(files);
|
||||
codes.push(code);
|
||||
totalSuccess += count;
|
||||
toRunFileIds = toRunFileIds.concat(fileIds);
|
||||
}
|
||||
|
||||
const allSuccess = codes.every((code) => code === 0);
|
||||
const any500 = codes.some((code) => code === 500);
|
||||
|
||||
let code = 500;
|
||||
if (allSuccess || (any500 && totalSuccess === totalFiles)) {
|
||||
code = 0;
|
||||
hideDocumentUploadModal();
|
||||
}
|
||||
|
||||
if (parseOnCreation) {
|
||||
await runDocumentByIds({
|
||||
documentIds: toRunFileIds,
|
||||
run: 1,
|
||||
shouldDelete: false,
|
||||
});
|
||||
}
|
||||
|
||||
setUploadProgress(100);
|
||||
|
||||
return code;
|
||||
},
|
||||
[fileList, uploadDocument, hideDocumentUploadModal, runDocumentByIds],
|
||||
);
|
||||
|
||||
return {
|
||||
documentUploadLoading: loading,
|
||||
onDocumentUploadOk,
|
||||
documentUploadVisible,
|
||||
hideDocumentUploadModal,
|
||||
showDocumentUploadModal,
|
||||
uploadFileList: fileList,
|
||||
setUploadFileList: setFileList,
|
||||
uploadProgress,
|
||||
setUploadProgress,
|
||||
};
|
||||
};
|
||||
|
||||
export const useHandleWebCrawl = () => {
|
||||
const {
|
||||
visible: webCrawlUploadVisible,
|
||||
hideModal: hideWebCrawlUploadModal,
|
||||
showModal: showWebCrawlUploadModal,
|
||||
} = useSetModalState();
|
||||
const { webCrawl, loading } = useNextWebCrawl();
|
||||
|
||||
const onWebCrawlUploadOk = useCallback(
|
||||
async (name: string, url: string) => {
|
||||
const ret = await webCrawl({ name, url });
|
||||
if (ret === 0) {
|
||||
hideWebCrawlUploadModal();
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
[webCrawl, hideWebCrawlUploadModal],
|
||||
);
|
||||
|
||||
return {
|
||||
webCrawlUploadLoading: loading,
|
||||
onWebCrawlUploadOk,
|
||||
webCrawlUploadVisible,
|
||||
hideWebCrawlUploadModal,
|
||||
showWebCrawlUploadModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useHandleRunDocumentByIds = (id: string) => {
|
||||
const { runDocumentByIds, loading } = useRunNextDocument();
|
||||
const [currentId, setCurrentId] = useState<string>('');
|
||||
const isLoading = loading && currentId !== '' && currentId === id;
|
||||
|
||||
const handleRunDocumentByIds = async (
|
||||
documentId: string,
|
||||
isRunning: boolean,
|
||||
shouldDelete: boolean = false,
|
||||
) => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setCurrentId(documentId);
|
||||
try {
|
||||
await runDocumentByIds({
|
||||
documentIds: [documentId],
|
||||
run: isRunning ? 2 : 1,
|
||||
shouldDelete,
|
||||
});
|
||||
setCurrentId('');
|
||||
} catch (error) {
|
||||
setCurrentId('');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleRunDocumentByIds,
|
||||
loading: isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useShowMetaModal = (documentId: string) => {
|
||||
const { setDocumentMeta, loading } = useSetDocumentMeta();
|
||||
|
||||
const {
|
||||
visible: setMetaVisible,
|
||||
hideModal: hideSetMetaModal,
|
||||
showModal: showSetMetaModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onSetMetaModalOk = useCallback(
|
||||
async (meta: string) => {
|
||||
const ret = await setDocumentMeta({
|
||||
documentId,
|
||||
meta,
|
||||
});
|
||||
if (ret === 0) {
|
||||
hideSetMetaModal();
|
||||
}
|
||||
},
|
||||
[setDocumentMeta, documentId, hideSetMetaModal],
|
||||
);
|
||||
|
||||
return {
|
||||
setMetaLoading: loading,
|
||||
onSetMetaModalOk,
|
||||
setMetaVisible,
|
||||
hideSetMetaModal,
|
||||
showSetMetaModal,
|
||||
};
|
||||
};
|
||||
@ -1,54 +0,0 @@
|
||||
.datasetWrapper {
|
||||
padding: 30px 30px 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.documentTable {
|
||||
tbody {
|
||||
// height: calc(100vh - 508px);
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
margin: 10px 0;
|
||||
justify-content: space-between;
|
||||
padding: 24px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deleteIconWrapper {
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.column {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.toChunks {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pageInputNumber {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.questionIcon {
|
||||
margin-inline-start: 4px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
cursor: help;
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
|
||||
.nameText {
|
||||
color: #1677ff;
|
||||
}
|
||||
@ -1,275 +0,0 @@
|
||||
import ChunkMethodModal from '@/components/chunk-method-modal';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import {
|
||||
useFetchNextDocumentList,
|
||||
useSetNextDocumentStatus,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { Divider, Flex, Switch, Table, Tooltip, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateFileModal from './create-file-modal';
|
||||
import DocumentToolbar from './document-toolbar';
|
||||
import {
|
||||
useChangeDocumentParser,
|
||||
useCreateEmptyDocument,
|
||||
useGetRowSelection,
|
||||
useHandleUploadDocument,
|
||||
useHandleWebCrawl,
|
||||
useNavigateToOtherPage,
|
||||
useRenameDocument,
|
||||
useShowMetaModal,
|
||||
} from './hooks';
|
||||
import ParsingActionCell from './parsing-action-cell';
|
||||
import ParsingStatusCell from './parsing-status-cell';
|
||||
import RenameModal from './rename-modal';
|
||||
import WebCrawlModal from './web-crawl-modal';
|
||||
|
||||
import FileUploadModal from '@/components/file-upload-modal';
|
||||
import { RunningStatus } from '@/constants/knowledge';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { CircleHelp } from 'lucide-react';
|
||||
import styles from './index.less';
|
||||
import { SetMetaModal } from './set-meta-modal';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const KnowledgeFile = () => {
|
||||
const { searchString, documents, pagination, handleInputChange } =
|
||||
useFetchNextDocumentList();
|
||||
const parserList = useSelectParserList();
|
||||
const { setDocumentStatus } = useSetNextDocumentStatus();
|
||||
const { toChunk } = useNavigateToOtherPage();
|
||||
const { currentRecord, setRecord } = useSetSelectedRecord<IDocumentInfo>();
|
||||
const {
|
||||
renameLoading,
|
||||
onRenameOk,
|
||||
renameVisible,
|
||||
hideRenameModal,
|
||||
showRenameModal,
|
||||
} = useRenameDocument(currentRecord.id);
|
||||
const {
|
||||
createLoading,
|
||||
onCreateOk,
|
||||
createVisible,
|
||||
hideCreateModal,
|
||||
showCreateModal,
|
||||
} = useCreateEmptyDocument();
|
||||
const {
|
||||
changeParserLoading,
|
||||
onChangeParserOk,
|
||||
changeParserVisible,
|
||||
hideChangeParserModal,
|
||||
showChangeParserModal,
|
||||
} = useChangeDocumentParser(currentRecord.id);
|
||||
const {
|
||||
documentUploadVisible,
|
||||
hideDocumentUploadModal,
|
||||
showDocumentUploadModal,
|
||||
onDocumentUploadOk,
|
||||
documentUploadLoading,
|
||||
uploadFileList,
|
||||
setUploadFileList,
|
||||
uploadProgress,
|
||||
setUploadProgress,
|
||||
} = useHandleUploadDocument();
|
||||
const {
|
||||
webCrawlUploadVisible,
|
||||
hideWebCrawlUploadModal,
|
||||
showWebCrawlUploadModal,
|
||||
onWebCrawlUploadOk,
|
||||
webCrawlUploadLoading,
|
||||
} = useHandleWebCrawl();
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'knowledgeDetails',
|
||||
});
|
||||
|
||||
const {
|
||||
showSetMetaModal,
|
||||
hideSetMetaModal,
|
||||
setMetaVisible,
|
||||
setMetaLoading,
|
||||
onSetMetaModalOk,
|
||||
} = useShowMetaModal(currentRecord.id);
|
||||
|
||||
const rowSelection = useGetRowSelection();
|
||||
|
||||
const columns: ColumnsType<IDocumentInfo> = [
|
||||
{
|
||||
title: t('name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
fixed: 'left',
|
||||
render: (text: any, { id, thumbnail, name }) => (
|
||||
<div className={styles.toChunks} onClick={() => toChunk(id)}>
|
||||
<Flex gap={10} align="center">
|
||||
{thumbnail ? (
|
||||
<img className={styles.img} src={thumbnail} alt="" />
|
||||
) : (
|
||||
<SvgIcon
|
||||
name={`file-icon/${getExtension(name)}`}
|
||||
width={24}
|
||||
></SvgIcon>
|
||||
)}
|
||||
<Text ellipsis={{ tooltip: text }} className={styles.nameText}>
|
||||
{text}
|
||||
</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('chunkNumber'),
|
||||
dataIndex: 'chunk_num',
|
||||
key: 'chunk_num',
|
||||
},
|
||||
{
|
||||
title: t('uploadDate'),
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
render(value) {
|
||||
return formatDate(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('chunkMethod'),
|
||||
dataIndex: 'parser_id',
|
||||
key: 'parser_id',
|
||||
render: (text) => {
|
||||
return parserList.find((x) => x.value === text)?.label;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('enabled'),
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
render: (_, { status, id }) => (
|
||||
<>
|
||||
<Switch
|
||||
checked={status === '1'}
|
||||
onChange={(e) => {
|
||||
setDocumentStatus({ status: e, documentId: id });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span className="flex items-center gap-2">
|
||||
{t('parsingStatus')}
|
||||
<Tooltip title={t('parsingStatusTip')}>
|
||||
<CircleHelp className="size-3" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'run',
|
||||
key: 'run',
|
||||
filters: Object.values(RunningStatus).map((value) => ({
|
||||
text: t(`runningStatus${value}`),
|
||||
value: value,
|
||||
})),
|
||||
onFilter: (value, record: IDocumentInfo) => record.run === value,
|
||||
render: (text, record) => {
|
||||
return <ParsingStatusCell record={record}></ParsingStatusCell>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<ParsingActionCell
|
||||
setCurrentRecord={setRecord}
|
||||
showRenameModal={showRenameModal}
|
||||
showChangeParserModal={showChangeParserModal}
|
||||
showSetMetaModal={showSetMetaModal}
|
||||
record={record}
|
||||
></ParsingActionCell>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const finalColumns = columns.map((x) => ({
|
||||
...x,
|
||||
className: `${styles.column}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={styles.datasetWrapper}>
|
||||
<h3>{t('dataset')}</h3>
|
||||
<p>{t('datasetDescription')}</p>
|
||||
<Divider></Divider>
|
||||
<DocumentToolbar
|
||||
selectedRowKeys={rowSelection.selectedRowKeys as string[]}
|
||||
showCreateModal={showCreateModal}
|
||||
showWebCrawlModal={showWebCrawlUploadModal}
|
||||
showDocumentUploadModal={showDocumentUploadModal}
|
||||
searchString={searchString}
|
||||
handleInputChange={handleInputChange}
|
||||
documents={documents}
|
||||
></DocumentToolbar>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={finalColumns}
|
||||
dataSource={documents}
|
||||
pagination={pagination}
|
||||
rowSelection={rowSelection}
|
||||
className={styles.documentTable}
|
||||
scroll={{ scrollToFirstRowOnChange: true, x: 1300 }}
|
||||
/>
|
||||
<CreateFileModal
|
||||
visible={createVisible}
|
||||
hideModal={hideCreateModal}
|
||||
loading={createLoading}
|
||||
onOk={onCreateOk}
|
||||
/>
|
||||
<ChunkMethodModal
|
||||
documentId={currentRecord.id}
|
||||
parserId={currentRecord.parser_id}
|
||||
parserConfig={currentRecord.parser_config}
|
||||
documentExtension={getExtension(currentRecord.name)}
|
||||
onOk={onChangeParserOk}
|
||||
visible={changeParserVisible}
|
||||
hideModal={hideChangeParserModal}
|
||||
loading={changeParserLoading}
|
||||
/>
|
||||
<RenameModal
|
||||
visible={renameVisible}
|
||||
onOk={onRenameOk}
|
||||
loading={renameLoading}
|
||||
hideModal={hideRenameModal}
|
||||
initialName={currentRecord.name}
|
||||
></RenameModal>
|
||||
<FileUploadModal
|
||||
visible={documentUploadVisible}
|
||||
hideModal={hideDocumentUploadModal}
|
||||
loading={documentUploadLoading}
|
||||
onOk={onDocumentUploadOk}
|
||||
uploadFileList={uploadFileList}
|
||||
setUploadFileList={setUploadFileList}
|
||||
uploadProgress={uploadProgress}
|
||||
setUploadProgress={setUploadProgress}
|
||||
></FileUploadModal>
|
||||
<WebCrawlModal
|
||||
visible={webCrawlUploadVisible}
|
||||
hideModal={hideWebCrawlUploadModal}
|
||||
loading={webCrawlUploadLoading}
|
||||
onOk={onWebCrawlUploadOk}
|
||||
></WebCrawlModal>
|
||||
{setMetaVisible && (
|
||||
<SetMetaModal
|
||||
visible={setMetaVisible}
|
||||
hideModal={hideSetMetaModal}
|
||||
onOk={onSetMetaModalOk}
|
||||
loading={setMetaLoading}
|
||||
initialMetaData={currentRecord.meta_fields}
|
||||
></SetMetaModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeFile;
|
||||
@ -1,3 +0,0 @@
|
||||
.iconButton {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
import { useShowDeleteConfirm, useTranslate } from '@/hooks/common-hooks';
|
||||
import { useRemoveNextDocument } from '@/hooks/document-hooks';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { downloadDocument } from '@/utils/file-util';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
ToolOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps, Space, Tooltip } from 'antd';
|
||||
import { isParserRunning } from '../utils';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { DocumentType } from '../constant';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
record: IDocumentInfo;
|
||||
setCurrentRecord: (record: IDocumentInfo) => void;
|
||||
showRenameModal: () => void;
|
||||
showChangeParserModal: () => void;
|
||||
showSetMetaModal: () => void;
|
||||
}
|
||||
|
||||
const ParsingActionCell = ({
|
||||
record,
|
||||
setCurrentRecord,
|
||||
showRenameModal,
|
||||
showChangeParserModal,
|
||||
showSetMetaModal,
|
||||
}: IProps) => {
|
||||
const documentId = record.id;
|
||||
const isRunning = isParserRunning(record.run);
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const { removeDocument } = useRemoveNextDocument();
|
||||
const showDeleteConfirm = useShowDeleteConfirm();
|
||||
const isVirtualDocument = record.type === DocumentType.Virtual;
|
||||
|
||||
const onRmDocument = () => {
|
||||
if (!isRunning) {
|
||||
showDeleteConfirm({
|
||||
onOk: () => removeDocument([documentId]),
|
||||
content: record?.parser_config?.graphrag?.use_graphrag
|
||||
? t('deleteDocumentConfirmContent')
|
||||
: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadDocument = () => {
|
||||
downloadDocument({
|
||||
id: documentId,
|
||||
filename: record.name,
|
||||
});
|
||||
};
|
||||
|
||||
const setRecord = useCallback(() => {
|
||||
setCurrentRecord(record);
|
||||
}, [record, setCurrentRecord]);
|
||||
|
||||
const onShowRenameModal = () => {
|
||||
setRecord();
|
||||
showRenameModal();
|
||||
};
|
||||
const onShowChangeParserModal = () => {
|
||||
setRecord();
|
||||
showChangeParserModal();
|
||||
};
|
||||
|
||||
const onShowSetMetaModal = useCallback(() => {
|
||||
setRecord();
|
||||
showSetMetaModal();
|
||||
}, [setRecord, showSetMetaModal]);
|
||||
|
||||
const chunkItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="flex flex-col">
|
||||
<Button type="link" onClick={onShowChangeParserModal}>
|
||||
{t('chunkMethod')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<div className="flex flex-col">
|
||||
<Button type="link" onClick={onShowSetMetaModal}>
|
||||
{t('setMetaData')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Space size={0}>
|
||||
{isVirtualDocument || (
|
||||
<Dropdown
|
||||
menu={{ items: chunkItems }}
|
||||
trigger={['click']}
|
||||
disabled={isRunning || record.parser_id === 'tag'}
|
||||
>
|
||||
<Button type="text" className={styles.iconButton}>
|
||||
<ToolOutlined size={20} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
<Tooltip title={t('rename', { keyPrefix: 'common' })}>
|
||||
<Button
|
||||
type="text"
|
||||
disabled={isRunning}
|
||||
onClick={onShowRenameModal}
|
||||
className={styles.iconButton}
|
||||
>
|
||||
<EditOutlined size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
|
||||
<Button
|
||||
type="text"
|
||||
disabled={isRunning}
|
||||
onClick={onRmDocument}
|
||||
className={styles.iconButton}
|
||||
>
|
||||
<DeleteOutlined size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{isVirtualDocument || (
|
||||
<Tooltip title={t('download', { keyPrefix: 'common' })}>
|
||||
<Button
|
||||
type="text"
|
||||
disabled={isRunning}
|
||||
onClick={onDownloadDocument}
|
||||
className={styles.iconButton}
|
||||
>
|
||||
<DownloadOutlined size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParsingActionCell;
|
||||
@ -1,36 +0,0 @@
|
||||
.popoverContent {
|
||||
width: 40vw;
|
||||
|
||||
.popoverContentItem {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.popoverContentText {
|
||||
white-space: pre-line;
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
.popoverContentErrorLabel {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operationIcon {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.operationIconSpin {
|
||||
animation: spin 1s linear infinite;
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,143 +0,0 @@
|
||||
import { ReactComponent as CancelIcon } from '@/assets/svg/cancel.svg';
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/svg/refresh.svg';
|
||||
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import {
|
||||
Badge,
|
||||
DescriptionsProps,
|
||||
Flex,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import { DocumentType, RunningStatus, RunningStatusMap } from '../constant';
|
||||
import { useHandleRunDocumentByIds } from '../hooks';
|
||||
import { isParserRunning } from '../utils';
|
||||
import styles from './index.less';
|
||||
|
||||
const iconMap = {
|
||||
[RunningStatus.UNSTART]: RunIcon,
|
||||
[RunningStatus.RUNNING]: CancelIcon,
|
||||
[RunningStatus.CANCEL]: RefreshIcon,
|
||||
[RunningStatus.DONE]: RefreshIcon,
|
||||
[RunningStatus.FAIL]: RefreshIcon,
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
record: IDocumentInfo;
|
||||
}
|
||||
|
||||
const PopoverContent = ({ record }: IProps) => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
const replaceText = (text: string) => {
|
||||
// Remove duplicate \n
|
||||
const nextText = text.replace(/(\n)\1+/g, '$1');
|
||||
|
||||
const replacedText = reactStringReplace(
|
||||
nextText,
|
||||
/(\[ERROR\].+\s)/g,
|
||||
(match, i) => {
|
||||
return (
|
||||
<span key={i} className={styles.popoverContentErrorLabel}>
|
||||
{match}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return replacedText;
|
||||
};
|
||||
|
||||
const items: DescriptionsProps['items'] = [
|
||||
{
|
||||
key: 'process_begin_at',
|
||||
label: t('processBeginAt'),
|
||||
children: record.process_begin_at,
|
||||
},
|
||||
{
|
||||
key: 'process_duration',
|
||||
label: t('processDuration'),
|
||||
children: `${record.process_duration.toFixed(2)} s`,
|
||||
},
|
||||
{
|
||||
key: 'progress_msg',
|
||||
label: t('progressMsg'),
|
||||
children: replaceText(record.progress_msg.trim()),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex vertical className={styles.popoverContent}>
|
||||
{items.map((x, idx) => {
|
||||
return (
|
||||
<div key={x.key} className={idx < 2 ? styles.popoverContentItem : ''}>
|
||||
<b>{x.label}:</b>
|
||||
<div className={styles.popoverContentText}>{x.children}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const ParsingStatusCell = ({ record }: IProps) => {
|
||||
const text = record.run;
|
||||
const runningStatus = RunningStatusMap[text];
|
||||
const { t } = useTranslation();
|
||||
const { handleRunDocumentByIds } = useHandleRunDocumentByIds(record.id);
|
||||
|
||||
const isRunning = isParserRunning(text);
|
||||
|
||||
const OperationIcon = iconMap[text];
|
||||
|
||||
const label = t(`knowledgeDetails.runningStatus${text}`);
|
||||
|
||||
const handleOperationIconClick =
|
||||
(shouldDelete: boolean = false) =>
|
||||
() => {
|
||||
handleRunDocumentByIds(record.id, isRunning, shouldDelete);
|
||||
};
|
||||
|
||||
return record.type === DocumentType.Virtual ? null : (
|
||||
<Flex justify={'space-between'} align="center">
|
||||
<Popover content={<PopoverContent record={record}></PopoverContent>}>
|
||||
<Tag color={runningStatus.color}>
|
||||
{isRunning ? (
|
||||
<Space>
|
||||
<Badge color={runningStatus.color} />
|
||||
{label}
|
||||
<span>{(record.progress * 100).toFixed(2)}%</span>
|
||||
</Space>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</Tag>
|
||||
</Popover>
|
||||
<Popconfirm
|
||||
title={t(`knowledgeDetails.redo`, { chunkNum: record.chunk_num })}
|
||||
onConfirm={handleOperationIconClick(true)}
|
||||
onCancel={handleOperationIconClick(false)}
|
||||
disabled={record.chunk_num === 0}
|
||||
okText={t('common.yes')}
|
||||
cancelText={t('common.no')}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.operationIcon)}
|
||||
onClick={
|
||||
record.chunk_num === 0 ? handleOperationIconClick(false) : () => {}
|
||||
}
|
||||
>
|
||||
<OperationIcon />
|
||||
</div>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParsingStatusCell;
|
||||
@ -1,75 +0,0 @@
|
||||
import { IModalManagerChildrenProps } from '@/components/modal-manager';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
|
||||
loading: boolean;
|
||||
initialName: string;
|
||||
onOk: (name: string) => void;
|
||||
showModal?(): void;
|
||||
}
|
||||
|
||||
const RenameModal = ({
|
||||
visible,
|
||||
onOk,
|
||||
loading,
|
||||
initialName,
|
||||
hideModal,
|
||||
}: IProps) => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslate('common');
|
||||
type FieldType = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
const ret = await form.validateFields();
|
||||
onOk(ret.name);
|
||||
};
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
console.log('Success:', values);
|
||||
};
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.setFieldValue('name', initialName);
|
||||
}
|
||||
}, [initialName, form, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('rename')}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
okButtonProps={{ loading }}
|
||||
>
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label={t('name')}
|
||||
name="name"
|
||||
rules={[{ required: true, message: t('namePlaceholder') }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenameModal;
|
||||
@ -1,81 +0,0 @@
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import Editor, { loader } from '@monaco-editor/react';
|
||||
|
||||
import { Form, Modal } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
loader.config({ paths: { vs: '/vs' } });
|
||||
|
||||
type FieldType = {
|
||||
meta?: string;
|
||||
};
|
||||
|
||||
export function SetMetaModal({
|
||||
visible,
|
||||
hideModal,
|
||||
onOk,
|
||||
initialMetaData,
|
||||
}: IModalProps<any> & { initialMetaData?: IDocumentInfo['meta_fields'] }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = useCallback(async () => {
|
||||
const values = await form.validateFields();
|
||||
onOk?.(values.meta);
|
||||
}, [form, onOk]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldValue('meta', JSON.stringify(initialMetaData, null, 4));
|
||||
}, [form, initialMetaData]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('knowledgeDetails.setMetaData')}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
>
|
||||
<Form
|
||||
name="basic"
|
||||
initialValues={{ remember: true }}
|
||||
autoComplete="off"
|
||||
layout={'vertical'}
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label={t('knowledgeDetails.metaData')}
|
||||
name="meta"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator(rule, value) {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject(
|
||||
new Error(t('knowledgeDetails.pleaseInputJson')),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
t('knowledgeDetails.documentMetaTips'),
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
>
|
||||
<Editor height={200} defaultLanguage="json" theme="vs-dark" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import { RunningStatus } from './constant';
|
||||
|
||||
export const isParserRunning = (text: RunningStatus) => {
|
||||
const isRunning = text === RunningStatus.RUNNING;
|
||||
return isRunning;
|
||||
};
|
||||
@ -1,67 +0,0 @@
|
||||
import { IModalManagerChildrenProps } from '@/components/modal-manager';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
|
||||
loading: boolean;
|
||||
onOk: (name: string, url: string) => void;
|
||||
showModal?(): void;
|
||||
}
|
||||
|
||||
const WebCrawlModal: React.FC<IProps> = ({ visible, hideModal, onOk }) => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
onOk(values.name, values.url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('webCrawl')}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
name="validateOnly"
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label="Name"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true, message: 'Please input name!' },
|
||||
{
|
||||
max: 10,
|
||||
message: 'The maximum length of name is 128 characters',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Document name" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="URL"
|
||||
name="url"
|
||||
rules={[
|
||||
{ required: true, message: 'Please input url!' },
|
||||
{
|
||||
pattern: new RegExp(
|
||||
'(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]',
|
||||
),
|
||||
message: 'Please enter a valid URL!',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="https://www.baidu.com" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default WebCrawlModal;
|
||||
@ -1,241 +0,0 @@
|
||||
const nodes = [
|
||||
{
|
||||
type: '"ORGANIZATION"',
|
||||
description:
|
||||
'"厦门象屿是一家公司,其营业收入和市场占有率在2018年至2022年间有所变化。"',
|
||||
source_id: '0',
|
||||
id: '"厦门象屿"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2018年是一个时间点,标志着厦门象屿营业收入和市场占有率的记录开始。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2018"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2019年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2019"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2020年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2020"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2021年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2021"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2022年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2022"',
|
||||
},
|
||||
{
|
||||
type: '"ORGANIZATION"',
|
||||
description:
|
||||
'"厦门象屿股份有限公司是一家公司,中文简称为厦门象屿,外文名称为Xiamen Xiangyu Co.,Ltd.,外文名称缩写为Xiangyu,法定代表人为邓启东。"',
|
||||
source_id: '1',
|
||||
id: '"厦门象屿股份有限公司"',
|
||||
},
|
||||
{
|
||||
type: '"PERSON"',
|
||||
description: '"邓启东是厦门象屿股份有限公司的法定代表人。"',
|
||||
source_id: '1',
|
||||
entity_type: '"PERSON"',
|
||||
id: '"邓启东"',
|
||||
},
|
||||
{
|
||||
type: '"GEO"',
|
||||
description: '"厦门是一个地理位置,与厦门象屿股份有限公司相关。"',
|
||||
source_id: '1',
|
||||
entity_type: '"GEO"',
|
||||
id: '"厦门"',
|
||||
},
|
||||
{
|
||||
type: '"PERSON"',
|
||||
description:
|
||||
'"廖杰 is the Board Secretary, responsible for handling board-related matters and communications."',
|
||||
source_id: '2',
|
||||
id: '"廖杰"',
|
||||
},
|
||||
{
|
||||
type: '"PERSON"',
|
||||
description:
|
||||
'"史经洋 is the Securities Affairs Representative, responsible for handling securities-related matters and communications."',
|
||||
source_id: '2',
|
||||
entity_type: '"PERSON"',
|
||||
id: '"史经洋"',
|
||||
},
|
||||
{
|
||||
type: '"GEO"',
|
||||
description:
|
||||
'"A geographic location in Xiamen, specifically in the Free Trade Zone, where the company\'s office is situated."',
|
||||
source_id: '2',
|
||||
entity_type: '"GEO"',
|
||||
id: '"厦门市湖里区自由贸易试验区厦门片区"',
|
||||
},
|
||||
{
|
||||
type: '"GEO"',
|
||||
description:
|
||||
'"The building where the company\'s office is located, situated at Xiangyu Road, Xiamen."',
|
||||
source_id: '2',
|
||||
entity_type: '"GEO"',
|
||||
id: '"象屿集团大厦"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"Refers to the year 2021, used for comparing financial metrics with the year 2022."',
|
||||
source_id: '3',
|
||||
id: '"2021年"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"Refers to the year 2022, used for presenting current financial metrics and comparing them with the year 2021."',
|
||||
source_id: '3',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2022年"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"Indicates the focus on key financial metrics in the table, such as weighted averages and percentages."',
|
||||
source_id: '3',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"主要财务指标"',
|
||||
},
|
||||
].map(({ type, ...x }) => ({ ...x }));
|
||||
|
||||
const edges = [
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2018年的营业收入和市场占有率被记录。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2018"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2019年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2019"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2020年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2020"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2021年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2021"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2022年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2022"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿股份有限公司的法定代表人是邓启东。"',
|
||||
source_id: '1',
|
||||
source: '"厦门象屿股份有限公司"',
|
||||
target: '"邓启东"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿股份有限公司位于厦门。"',
|
||||
source_id: '1',
|
||||
source: '"厦门象屿股份有限公司"',
|
||||
target: '"厦门"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"廖杰\'s office is located in the Xiangyu Group Building, indicating his workplace."',
|
||||
source_id: '2',
|
||||
source: '"廖杰"',
|
||||
target: '"象屿集团大厦"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"廖杰 works in the Xiamen Free Trade Zone, a specific area within Xiamen."',
|
||||
source_id: '2',
|
||||
source: '"廖杰"',
|
||||
target: '"厦门市湖里区自由贸易试验区厦门片区"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"史经洋\'s office is also located in the Xiangyu Group Building, indicating his workplace."',
|
||||
source_id: '2',
|
||||
source: '"史经洋"',
|
||||
target: '"象屿集团大厦"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"史经洋 works in the Xiamen Free Trade Zone, a specific area within Xiamen."',
|
||||
source_id: '2',
|
||||
source: '"史经洋"',
|
||||
target: '"厦门市湖里区自由贸易试验区厦门片区"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"The years 2021 and 2022 are related as they are used for comparing financial metrics, showing changes and adjustments over time."',
|
||||
source_id: '3',
|
||||
source: '"2021年"',
|
||||
target: '"2022年"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"The \'主要财务指标\' is related to the year 2021 as it provides the basis for financial comparisons and adjustments."',
|
||||
source_id: '3',
|
||||
source: '"2021年"',
|
||||
target: '"主要财务指标"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"The \'主要财务指标\' is related to the year 2022 as it presents the current financial metrics and their changes compared to 2021."',
|
||||
source_id: '3',
|
||||
source: '"2022年"',
|
||||
target: '"主要财务指标"',
|
||||
},
|
||||
];
|
||||
|
||||
export const graphData = {
|
||||
directed: false,
|
||||
multigraph: false,
|
||||
graph: {},
|
||||
nodes,
|
||||
edges,
|
||||
combos: [],
|
||||
};
|
||||
@ -1,141 +0,0 @@
|
||||
import { ElementDatum, Graph, IElementEvent } from '@antv/g6';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { buildNodesAndCombos } from './util';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const TooltipColorMap = {
|
||||
combo: 'red',
|
||||
node: 'black',
|
||||
edge: 'blue',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
data: any;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const ForceGraph = ({ data, show }: IProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const graphRef = useRef<Graph | null>(null);
|
||||
|
||||
const nextData = useMemo(() => {
|
||||
if (!isEmpty(data)) {
|
||||
const graphData = data;
|
||||
const mi = buildNodesAndCombos(graphData.nodes);
|
||||
return { edges: graphData.edges, ...mi };
|
||||
}
|
||||
return { nodes: [], edges: [] };
|
||||
}, [data]);
|
||||
|
||||
const render = useCallback(() => {
|
||||
const graph = new Graph({
|
||||
container: containerRef.current!,
|
||||
autoFit: 'view',
|
||||
autoResize: true,
|
||||
behaviors: [
|
||||
'drag-element',
|
||||
'drag-canvas',
|
||||
'zoom-canvas',
|
||||
'collapse-expand',
|
||||
{
|
||||
type: 'hover-activate',
|
||||
degree: 1, // 👈🏻 Activate relations.
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
type: 'tooltip',
|
||||
enterable: true,
|
||||
getContent: (e: IElementEvent, items: ElementDatum) => {
|
||||
if (Array.isArray(items)) {
|
||||
if (items.some((x) => x?.isCombo)) {
|
||||
return `<p style="font-weight:600;color:red">${items?.[0]?.data?.label}</p>`;
|
||||
}
|
||||
let result = ``;
|
||||
items.forEach((item) => {
|
||||
result += `<section style="color:${TooltipColorMap[e['targetType'] as keyof typeof TooltipColorMap]};"><h3>${item?.id}</h3>`;
|
||||
if (item?.entity_type) {
|
||||
result += `<div style="padding-bottom: 6px;"><b>Entity type: </b>${item?.entity_type}</div>`;
|
||||
}
|
||||
if (item?.weight) {
|
||||
result += `<div><b>Weight: </b>${item?.weight}</div>`;
|
||||
}
|
||||
if (item?.description) {
|
||||
result += `<p>${item?.description}</p>`;
|
||||
}
|
||||
});
|
||||
return result + '</section>';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
type: 'combo-combined',
|
||||
preventOverlap: true,
|
||||
comboPadding: 1,
|
||||
spacing: 100,
|
||||
},
|
||||
node: {
|
||||
style: {
|
||||
size: 150,
|
||||
labelText: (d) => d.id,
|
||||
// labelPadding: 30,
|
||||
labelFontSize: 40,
|
||||
// labelOffsetX: 20,
|
||||
labelOffsetY: 20,
|
||||
labelPlacement: 'center',
|
||||
labelWordWrap: true,
|
||||
},
|
||||
palette: {
|
||||
type: 'group',
|
||||
field: (d) => {
|
||||
return d?.entity_type as string;
|
||||
},
|
||||
},
|
||||
},
|
||||
edge: {
|
||||
style: (model) => {
|
||||
const weight: number = Number(model?.weight) || 2;
|
||||
const lineWeight = weight * 4;
|
||||
return {
|
||||
stroke: '#99ADD1',
|
||||
lineWidth: lineWeight > 10 ? 10 : lineWeight,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (graphRef.current) {
|
||||
graphRef.current.destroy();
|
||||
}
|
||||
|
||||
graphRef.current = graph;
|
||||
|
||||
graph.setData(nextData);
|
||||
|
||||
graph.render();
|
||||
}, [nextData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(data)) {
|
||||
render();
|
||||
}
|
||||
}, [data, render]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.forceContainer}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: show ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForceGraph;
|
||||
@ -1,5 +0,0 @@
|
||||
.forceContainer {
|
||||
:global(.tooltip) {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useFetchKnowledgeGraph } from '@/hooks/knowledge-hooks';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ForceGraph from './force-graph';
|
||||
import { useDeleteKnowledgeGraph } from './use-delete-graph';
|
||||
|
||||
const KnowledgeGraph: React.FC = () => {
|
||||
const { data } = useFetchKnowledgeGraph();
|
||||
const { t } = useTranslation();
|
||||
const { handleDeleteKnowledgeGraph } = useDeleteKnowledgeGraph();
|
||||
|
||||
return (
|
||||
<section className={'w-full h-full relative'}>
|
||||
<ConfirmDeleteDialog onOk={handleDeleteKnowledgeGraph}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={'sm'}
|
||||
className="absolute right-0 top-0 z-50"
|
||||
>
|
||||
<Trash2 /> {t('common.delete')}
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
<ForceGraph data={data?.graph} show></ForceGraph>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeGraph;
|
||||
@ -1,21 +0,0 @@
|
||||
import {
|
||||
useKnowledgeBaseId,
|
||||
useRemoveKnowledgeGraph,
|
||||
} from '@/hooks/knowledge-hooks';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'umi';
|
||||
|
||||
export function useDeleteKnowledgeGraph() {
|
||||
const { removeKnowledgeGraph, loading } = useRemoveKnowledgeGraph();
|
||||
const navigate = useNavigate();
|
||||
const knowledgeBaseId = useKnowledgeBaseId();
|
||||
|
||||
const handleDeleteKnowledgeGraph = useCallback(async () => {
|
||||
const ret = await removeKnowledgeGraph();
|
||||
if (ret === 0) {
|
||||
navigate(`/knowledge/dataset?id=${knowledgeBaseId}`);
|
||||
}
|
||||
}, [knowledgeBaseId, navigate, removeKnowledgeGraph]);
|
||||
|
||||
return { handleDeleteKnowledgeGraph, loading };
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
class KeyGenerator {
|
||||
idx = 0;
|
||||
chars: string[] = [];
|
||||
constructor() {
|
||||
const chars = Array(26)
|
||||
.fill(1)
|
||||
.map((x, idx) => String.fromCharCode(97 + idx)); // 26 char
|
||||
this.chars = chars;
|
||||
}
|
||||
generateKey() {
|
||||
const key = this.chars[this.idx];
|
||||
this.idx++;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// Classify nodes based on edge relationships
|
||||
export class Converter {
|
||||
keyGenerator;
|
||||
dict: Record<string, string> = {}; // key is node id, value is combo
|
||||
constructor() {
|
||||
this.keyGenerator = new KeyGenerator();
|
||||
}
|
||||
buildDict(edges: { source: string; target: string }[]) {
|
||||
edges.forEach((x) => {
|
||||
if (this.dict[x.source] && !this.dict[x.target]) {
|
||||
this.dict[x.target] = this.dict[x.source];
|
||||
} else if (!this.dict[x.source] && this.dict[x.target]) {
|
||||
this.dict[x.source] = this.dict[x.target];
|
||||
} else if (!this.dict[x.source] && !this.dict[x.target]) {
|
||||
this.dict[x.source] = this.dict[x.target] =
|
||||
this.keyGenerator.generateKey();
|
||||
}
|
||||
});
|
||||
return this.dict;
|
||||
}
|
||||
buildNodesAndCombos(nodes: any[], edges: any[]) {
|
||||
this.buildDict(edges);
|
||||
const nextNodes = nodes.map((x) => ({ ...x, combo: this.dict[x.id] }));
|
||||
|
||||
const combos = Object.values(this.dict).reduce<any[]>((pre, cur) => {
|
||||
if (pre.every((x) => x.id !== cur)) {
|
||||
pre.push({
|
||||
id: cur,
|
||||
data: {
|
||||
label: `Combo ${cur}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
|
||||
return { nodes: nextNodes, combos };
|
||||
}
|
||||
}
|
||||
|
||||
export const isDataExist = (data: any) => {
|
||||
return (
|
||||
data?.data && typeof data?.data !== 'boolean' && !isEmpty(data?.data?.graph)
|
||||
);
|
||||
};
|
||||
|
||||
const findCombo = (communities: string[]) => {
|
||||
const combo = Array.isArray(communities) ? communities[0] : undefined;
|
||||
return combo;
|
||||
};
|
||||
|
||||
export const buildNodesAndCombos = (nodes: any[]) => {
|
||||
const combos: any[] = [];
|
||||
nodes.forEach((x) => {
|
||||
const combo = findCombo(x?.communities);
|
||||
if (combo && combos.every((y) => y.data.label !== combo)) {
|
||||
combos.push({
|
||||
isCombo: true,
|
||||
id: uuid(),
|
||||
data: {
|
||||
label: combo,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const nextNodes = nodes.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
combo: combos.find((y) => y.data.label === findCombo(x?.communities))?.id,
|
||||
};
|
||||
});
|
||||
|
||||
return { nodes: nextNodes, combos };
|
||||
};
|
||||
@ -1,77 +0,0 @@
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import { Col, Divider, Empty, Row, Typography } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useMemo } from 'react';
|
||||
import styles from './index.less';
|
||||
import { TagTabs } from './tag-tabs';
|
||||
import { ImageMap } from './utils';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => {
|
||||
const parserList = useSelectParserList();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
const item = useMemo(() => {
|
||||
const item = parserList.find((x) => x.value === chunkMethod);
|
||||
if (item) {
|
||||
return {
|
||||
title: item.label,
|
||||
description: t(camelCase(item.value)),
|
||||
};
|
||||
}
|
||||
return { title: '', description: '' };
|
||||
}, [parserList, chunkMethod, t]);
|
||||
|
||||
const imageList = useMemo(() => {
|
||||
if (chunkMethod in ImageMap) {
|
||||
return ImageMap[chunkMethod as keyof typeof ImageMap];
|
||||
}
|
||||
return [];
|
||||
}, [chunkMethod]);
|
||||
|
||||
return (
|
||||
<section className={styles.categoryPanelWrapper}>
|
||||
{imageList.length > 0 ? (
|
||||
<>
|
||||
<h5 className="font-semibold text-base mt-0 mb-1">
|
||||
{`"${item.title}" ${t('methodTitle')}`}
|
||||
</h5>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(item.description),
|
||||
}}
|
||||
></p>
|
||||
<h5 className="font-semibold text-base mt-4 mb-1">{`"${item.title}" ${t('methodExamples')}`}</h5>
|
||||
<Text>{t('methodExamplesDescription')}</Text>
|
||||
<Row gutter={[10, 10]} className={styles.imageRow}>
|
||||
{imageList.map((x) => (
|
||||
<Col span={12} key={x}>
|
||||
<SvgIcon
|
||||
name={x}
|
||||
width={'100%'}
|
||||
className={styles.image}
|
||||
></SvgIcon>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<h5 className="font-semibold text-base mt-4 mb-1">
|
||||
{item.title} {t('dialogueExamplesTitle')}
|
||||
</h5>
|
||||
<Divider></Divider>
|
||||
</>
|
||||
) : (
|
||||
<Empty description={''} image={null}>
|
||||
<p>{t('methodEmpty')}</p>
|
||||
<SvgIcon name={'chunk-method/chunk-empty'} width={'100%'}></SvgIcon>
|
||||
</Empty>
|
||||
)}
|
||||
{chunkMethod === 'tag' && <TagTabs></TagTabs>}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPanel;
|
||||
@ -1,31 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function AudioConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function BookConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useHandleChunkMethodSelectChange } from '@/hooks/logic-hooks';
|
||||
import { Form, Select } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import {
|
||||
useHasParsedDocument,
|
||||
useSelectChunkMethodList,
|
||||
useSelectEmbeddingModelOptions,
|
||||
} from '../hooks';
|
||||
|
||||
export const EmbeddingModelItem = memo(function EmbeddingModelItem() {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const embeddingModelOptions = useSelectEmbeddingModelOptions();
|
||||
const disabled = useHasParsedDocument();
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name="embd_id"
|
||||
label={t('embeddingModel')}
|
||||
rules={[{ required: true }]}
|
||||
tooltip={t('embeddingModelTip')}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('embeddingModelPlaceholder')}
|
||||
options={embeddingModelOptions}
|
||||
disabled={disabled}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
);
|
||||
});
|
||||
|
||||
export const ChunkMethodItem = memo(function ChunkMethodItem() {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = Form.useFormInstance();
|
||||
const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form);
|
||||
const parserList = useSelectChunkMethodList();
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name="parser_id"
|
||||
label={t('chunkMethod')}
|
||||
tooltip={t('chunkMethodTip')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('chunkMethodPlaceholder')}
|
||||
onChange={handleChunkMethodSelectChange}
|
||||
options={parserList}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
);
|
||||
});
|
||||
@ -1,31 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function EmailConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { normFile } from '@/utils/file-util';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Radio, Space, Upload } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
useFetchKnowledgeConfigurationOnMount,
|
||||
useSubmitKnowledgeConfiguration,
|
||||
} from '../hooks';
|
||||
import { AudioConfiguration } from './audio';
|
||||
import { BookConfiguration } from './book';
|
||||
import { EmailConfiguration } from './email';
|
||||
import { KnowledgeGraphConfiguration } from './knowledge-graph';
|
||||
import { LawsConfiguration } from './laws';
|
||||
import { ManualConfiguration } from './manual';
|
||||
import { NaiveConfiguration } from './naive';
|
||||
import { OneConfiguration } from './one';
|
||||
import { PaperConfiguration } from './paper';
|
||||
import { PictureConfiguration } from './picture';
|
||||
import { PresentationConfiguration } from './presentation';
|
||||
import { QAConfiguration } from './qa';
|
||||
import { ResumeConfiguration } from './resume';
|
||||
import { TableConfiguration } from './table';
|
||||
import { TagConfiguration } from './tag';
|
||||
|
||||
import styles from '../index.less';
|
||||
|
||||
const ConfigurationComponentMap = {
|
||||
[DocumentParserType.Naive]: NaiveConfiguration,
|
||||
[DocumentParserType.Qa]: QAConfiguration,
|
||||
[DocumentParserType.Resume]: ResumeConfiguration,
|
||||
[DocumentParserType.Manual]: ManualConfiguration,
|
||||
[DocumentParserType.Table]: TableConfiguration,
|
||||
[DocumentParserType.Paper]: PaperConfiguration,
|
||||
[DocumentParserType.Book]: BookConfiguration,
|
||||
[DocumentParserType.Laws]: LawsConfiguration,
|
||||
[DocumentParserType.Presentation]: PresentationConfiguration,
|
||||
[DocumentParserType.Picture]: PictureConfiguration,
|
||||
[DocumentParserType.One]: OneConfiguration,
|
||||
[DocumentParserType.Audio]: AudioConfiguration,
|
||||
[DocumentParserType.Email]: EmailConfiguration,
|
||||
[DocumentParserType.Tag]: TagConfiguration,
|
||||
[DocumentParserType.KnowledgeGraph]: KnowledgeGraphConfiguration,
|
||||
};
|
||||
|
||||
function EmptyComponent() {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export const ConfigurationForm = ({ form }: { form: FormInstance }) => {
|
||||
const { submitKnowledgeConfiguration, submitLoading, navigateToDataset } =
|
||||
useSubmitKnowledgeConfiguration(form);
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
const [finalParserId, setFinalParserId] = useState<DocumentParserType>();
|
||||
const knowledgeDetails = useFetchKnowledgeConfigurationOnMount(form);
|
||||
const parserId: DocumentParserType = Form.useWatch('parser_id', form);
|
||||
const ConfigurationComponent = useMemo(() => {
|
||||
return finalParserId
|
||||
? ConfigurationComponentMap[finalParserId]
|
||||
: EmptyComponent;
|
||||
}, [finalParserId]);
|
||||
|
||||
useEffect(() => {
|
||||
setFinalParserId(parserId);
|
||||
}, [parserId]);
|
||||
|
||||
useEffect(() => {
|
||||
setFinalParserId(knowledgeDetails.parser_id as DocumentParserType);
|
||||
}, [knowledgeDetails.parser_id]);
|
||||
|
||||
return (
|
||||
<Form form={form} name="validateOnly" layout="vertical" autoComplete="off">
|
||||
<Form.Item name="name" label={t('name')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="avatar"
|
||||
label={t('photo')}
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={normFile}
|
||||
>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
maxCount={1}
|
||||
beforeUpload={() => false}
|
||||
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
|
||||
>
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>{t('upload')}</div>
|
||||
</button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label={t('description')}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="permission"
|
||||
label={t('permissions')}
|
||||
tooltip={t('permissionsTip')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="me">{t('me')}</Radio>
|
||||
<Radio value="team">{t('team')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<ConfigurationComponent></ConfigurationComponent>
|
||||
|
||||
<Form.Item>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Space>
|
||||
<Button size={'middle'} onClick={navigateToDataset}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size={'middle'}
|
||||
loading={submitLoading}
|
||||
onClick={submitKnowledgeConfiguration}
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
import Delimiter from '@/components/delimiter';
|
||||
import EntityTypesItem from '@/components/entity-types-item';
|
||||
import MaxTokenNumber from '@/components/max-token-number';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function KnowledgeGraphConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<EntityTypesItem></EntityTypesItem>
|
||||
<MaxTokenNumber max={8192 * 2}></MaxTokenNumber>
|
||||
<Delimiter></Delimiter>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function LawsConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function ManualConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import { DatasetConfigurationContainer } from '@/components/dataset-configuration-container';
|
||||
import Delimiter from '@/components/delimiter';
|
||||
import ExcelToHtml from '@/components/excel-to-html';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import MaxTokenNumber from '@/components/max-token-number';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { Divider } from 'antd';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function NaiveConfiguration() {
|
||||
return (
|
||||
<section className="space-y-4 mb-4">
|
||||
<DatasetConfigurationContainer>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
<MaxTokenNumber></MaxTokenNumber>
|
||||
<Delimiter></Delimiter>
|
||||
</DatasetConfigurationContainer>
|
||||
<Divider></Divider>
|
||||
<DatasetConfigurationContainer>
|
||||
<PageRank></PageRank>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
<ExcelToHtml></ExcelToHtml>
|
||||
<TagItems></TagItems>
|
||||
</DatasetConfigurationContainer>
|
||||
<Divider></Divider>
|
||||
<DatasetConfigurationContainer>
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
</DatasetConfigurationContainer>
|
||||
<Divider></Divider>
|
||||
<GraphRagItems></GraphRagItems>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function OneConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function PaperConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function PictureConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import {
|
||||
AutoKeywordsItem,
|
||||
AutoQuestionsItem,
|
||||
} from '@/components/auto-keywords-item';
|
||||
import LayoutRecognize from '@/components/layout-recognize';
|
||||
import PageRank from '@/components/page-rank';
|
||||
import ParseConfiguration from '@/components/parse-configuration';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function PresentationConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<LayoutRecognize></LayoutRecognize>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<>
|
||||
<AutoKeywordsItem></AutoKeywordsItem>
|
||||
<AutoQuestionsItem></AutoQuestionsItem>
|
||||
</>
|
||||
|
||||
<ParseConfiguration></ParseConfiguration>
|
||||
|
||||
<GraphRagItems marginBottom></GraphRagItems>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import PageRank from '@/components/page-rank';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function QAConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import PageRank from '@/components/page-rank';
|
||||
import { TagItems } from '../tag-item';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function ResumeConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
import PageRank from '@/components/page-rank';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function TableConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
import PageRank from '@/components/page-rank';
|
||||
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
|
||||
|
||||
export function TagConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<ChunkMethodItem></ChunkMethodItem>
|
||||
|
||||
<PageRank></PageRank>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useFetchKnowledgeBaseConfiguration,
|
||||
useUpdateKnowledge,
|
||||
} from '@/hooks/knowledge-hooks';
|
||||
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
|
||||
import { useNavigateToDataset } from '@/hooks/route-hook';
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import {
|
||||
getBase64FromUploadFileList,
|
||||
getUploadFileListFromBase64,
|
||||
} from '@/utils/file-util';
|
||||
import { useIsFetching } from '@tanstack/react-query';
|
||||
import { Form, UploadFile } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import pick from 'lodash/pick';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const useSubmitKnowledgeConfiguration = (form: FormInstance) => {
|
||||
const { saveKnowledgeConfiguration, loading } = useUpdateKnowledge();
|
||||
const navigateToDataset = useNavigateToDataset();
|
||||
|
||||
const submitKnowledgeConfiguration = useCallback(async () => {
|
||||
const values = await form.validateFields();
|
||||
const avatar = await getBase64FromUploadFileList(values.avatar);
|
||||
saveKnowledgeConfiguration({
|
||||
...values,
|
||||
avatar,
|
||||
});
|
||||
navigateToDataset();
|
||||
}, [saveKnowledgeConfiguration, form, navigateToDataset]);
|
||||
|
||||
return {
|
||||
submitKnowledgeConfiguration,
|
||||
submitLoading: loading,
|
||||
navigateToDataset,
|
||||
};
|
||||
};
|
||||
|
||||
// The value that does not need to be displayed in the analysis method Select
|
||||
const HiddenFields = ['email', 'picture', 'audio'];
|
||||
|
||||
export function useSelectChunkMethodList() {
|
||||
const parserList = useSelectParserList();
|
||||
|
||||
return parserList.filter((x) => !HiddenFields.some((y) => y === x.value));
|
||||
}
|
||||
|
||||
export function useSelectEmbeddingModelOptions() {
|
||||
const allOptions = useSelectLlmOptionsByModelType();
|
||||
return allOptions[LlmModelType.Embedding];
|
||||
}
|
||||
|
||||
export function useHasParsedDocument() {
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
|
||||
return knowledgeDetails.chunk_num > 0;
|
||||
}
|
||||
|
||||
export const useFetchKnowledgeConfigurationOnMount = (form: FormInstance) => {
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
useEffect(() => {
|
||||
const fileList: UploadFile[] = getUploadFileListFromBase64(
|
||||
knowledgeDetails.avatar,
|
||||
);
|
||||
form.setFieldsValue({
|
||||
...pick(knowledgeDetails, [
|
||||
'description',
|
||||
'name',
|
||||
'permission',
|
||||
'embd_id',
|
||||
'parser_id',
|
||||
'language',
|
||||
'parser_config',
|
||||
'pagerank',
|
||||
]),
|
||||
avatar: fileList,
|
||||
});
|
||||
}, [form, knowledgeDetails]);
|
||||
|
||||
return knowledgeDetails;
|
||||
};
|
||||
|
||||
export const useSelectKnowledgeDetailsLoading = () =>
|
||||
useIsFetching({ queryKey: ['fetchKnowledgeDetail'] }) > 0;
|
||||
|
||||
export const useHandleChunkMethodChange = () => {
|
||||
const [form] = Form.useForm();
|
||||
const chunkMethod = Form.useWatch('parser_id', form);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🚀 ~ useHandleChunkMethodChange ~ chunkMethod:', chunkMethod);
|
||||
}, [chunkMethod]);
|
||||
|
||||
return { form, chunkMethod };
|
||||
};
|
||||
|
||||
export const useRenameKnowledgeTag = () => {
|
||||
const [tag, setTag] = useState<string>('');
|
||||
const {
|
||||
visible: tagRenameVisible,
|
||||
hideModal: hideTagRenameModal,
|
||||
showModal: showFileRenameModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const handleShowTagRenameModal = useCallback(
|
||||
(record: string) => {
|
||||
setTag(record);
|
||||
showFileRenameModal();
|
||||
},
|
||||
[showFileRenameModal],
|
||||
);
|
||||
|
||||
return {
|
||||
initialName: tag,
|
||||
tagRenameVisible,
|
||||
hideTagRenameModal,
|
||||
showTagRenameModal: handleShowTagRenameModal,
|
||||
};
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
.tags {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.preset {
|
||||
display: flex;
|
||||
height: 80px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 100px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.4);
|
||||
margin: 10px 0px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.configurationWrapper {
|
||||
padding: 0 52px;
|
||||
.buttonWrapper {
|
||||
text-align: right;
|
||||
}
|
||||
.variableSlider {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryPanelWrapper {
|
||||
.topTitle {
|
||||
margin-top: 0;
|
||||
}
|
||||
.imageRow {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { Col, Divider, Row, Spin, Typography } from 'antd';
|
||||
import CategoryPanel from './category-panel';
|
||||
import { ConfigurationForm } from './configuration';
|
||||
import {
|
||||
useHandleChunkMethodChange,
|
||||
useSelectKnowledgeDetailsLoading,
|
||||
} from './hooks';
|
||||
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Configuration = () => {
|
||||
const loading = useSelectKnowledgeDetailsLoading();
|
||||
const { form, chunkMethod } = useHandleChunkMethodChange();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
return (
|
||||
<div className={styles.configurationWrapper}>
|
||||
<Title level={5}>
|
||||
{t('configuration', { keyPrefix: 'knowledgeDetails' })}
|
||||
</Title>
|
||||
<p>{t('titleDescription')}</p>
|
||||
<Divider></Divider>
|
||||
<Spin spinning={loading}>
|
||||
<Row gutter={32}>
|
||||
<Col span={8}>
|
||||
<ConfigurationForm form={form}></ConfigurationForm>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<CategoryPanel chunkMethod={chunkMethod}></CategoryPanel>
|
||||
</Col>
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Configuration;
|
||||
@ -1,90 +0,0 @@
|
||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Flex, Form, InputNumber, Select, Slider, Space } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const TagSetItem = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||
|
||||
const knowledgeOptions = knowledgeList
|
||||
.filter((x) => x.parser_id === 'tag')
|
||||
.map((x) => ({
|
||||
label: (
|
||||
<Space>
|
||||
<Avatar size={20} icon={<UserOutlined />} src={x.avatar} />
|
||||
{x.name}
|
||||
</Space>
|
||||
),
|
||||
value: x.id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('knowledgeConfiguration.tagSet')}
|
||||
name={['parser_config', 'tag_kb_ids']}
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(t('knowledgeConfiguration.tagSetTip')),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
message: t('chat.knowledgeBasesMessage'),
|
||||
type: 'array',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={knowledgeOptions}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const TopNTagsItem = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Form.Item label={t('knowledgeConfiguration.topnTags')}>
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'topn_tags']}
|
||||
noStyle
|
||||
initialValue={3}
|
||||
>
|
||||
<Slider max={10} min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item name={['parser_config', 'topn_tags']} noStyle>
|
||||
<InputNumber max={10} min={1} />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export function TagItems() {
|
||||
return (
|
||||
<>
|
||||
<TagSetItem></TagSetItem>
|
||||
<Form.Item noStyle dependencies={[['parser_config', 'tag_kb_ids']]}>
|
||||
{({ getFieldValue }) => {
|
||||
const ids: string[] = getFieldValue(['parser_config', 'tag_kb_ids']);
|
||||
|
||||
return (
|
||||
Array.isArray(ids) &&
|
||||
ids.length > 0 && <TopNTagsItem></TopNTagsItem>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,307 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useDeleteTag, useFetchTagList } from '@/hooks/knowledge-hooks';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRenameKnowledgeTag } from '../hooks';
|
||||
import { RenameDialog } from './rename-dialog';
|
||||
|
||||
export type ITag = {
|
||||
tag: string;
|
||||
frequency: number;
|
||||
};
|
||||
|
||||
export function TagTable() {
|
||||
const { t } = useTranslation();
|
||||
const { list } = useFetchTagList();
|
||||
const [tagList, setTagList] = useState<ITag[]>([]);
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
const { deleteTag } = useDeleteTag();
|
||||
|
||||
useEffect(() => {
|
||||
setTagList(list.map((x) => ({ tag: x[0], frequency: x[1] })));
|
||||
}, [list]);
|
||||
|
||||
const handleDeleteTag = useCallback(
|
||||
(tags: string[]) => () => {
|
||||
deleteTag(tags);
|
||||
},
|
||||
[deleteTag],
|
||||
);
|
||||
|
||||
const {
|
||||
showTagRenameModal,
|
||||
hideTagRenameModal,
|
||||
tagRenameVisible,
|
||||
initialName,
|
||||
} = useRenameKnowledgeTag();
|
||||
|
||||
const columns: ColumnDef<ITag>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'tag',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('knowledgeConfiguration.tagName')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const value: string = row.getValue('tag');
|
||||
return <div>{value}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'frequency',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('knowledgeConfiguration.frequency')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="capitalize ">{row.getValue('frequency')}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableHiding: false,
|
||||
header: t('common.action'),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<Tooltip>
|
||||
<ConfirmDeleteDialog onOk={handleDeleteTag([row.original.tag])}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</ConfirmDeleteDialog>
|
||||
<TooltipContent>
|
||||
<p>{t('common.delete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => showTagRenameModal(row.original.tag)}
|
||||
>
|
||||
<Pencil />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('common.rename')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: tagList,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedRowLength = table.getFilteredSelectedRowModel().rows.length;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between py-4 ">
|
||||
<Input
|
||||
placeholder={t('knowledgeConfiguration.searchTags')}
|
||||
value={(table.getColumn('tag')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) =>
|
||||
table.getColumn('tag')?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="w-1/2"
|
||||
/>
|
||||
{selectedRowLength > 0 && (
|
||||
<ConfirmDeleteDialog
|
||||
onOk={handleDeleteTag(
|
||||
table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((x) => x.original.tag),
|
||||
)}
|
||||
>
|
||||
<Button variant="outline" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{selectedRowLength} of {table.getFilteredRowModel().rows.length}{' '}
|
||||
row(s) selected.
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{t('common.previousPage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
{t('common.nextPage')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tagRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideTagRenameModal}
|
||||
initialName={initialName}
|
||||
></RenameDialog>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { LoadingButton } from '@/components/ui/loading-button';
|
||||
import { useTagIsRenaming } from '@/hooks/knowledge-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RenameForm } from './rename-form';
|
||||
|
||||
export function RenameDialog({
|
||||
hideModal,
|
||||
initialName,
|
||||
}: IModalProps<any> & { initialName: string }) {
|
||||
const { t } = useTranslation();
|
||||
const loading = useTagIsRenaming();
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.rename')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<RenameForm
|
||||
initialName={initialName}
|
||||
hideModal={hideModal}
|
||||
></RenameForm>
|
||||
<DialogFooter>
|
||||
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useRenameTag } from '@/hooks/knowledge-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function RenameForm({
|
||||
initialName,
|
||||
hideModal,
|
||||
}: IModalProps<any> & { initialName: string }) {
|
||||
const { t } = useTranslation();
|
||||
const FormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t('common.namePlaceholder'),
|
||||
})
|
||||
.trim(),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { renameTag } = useRenameTag();
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
const ret = await renameTag({ fromTag: initialName, toTag: data.name });
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('name', initialName);
|
||||
}, [form, initialName]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
id={TagRenameId}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('common.namePlaceholder')}
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { Segmented } from 'antd';
|
||||
import { SegmentedLabeledOption } from 'antd/es/segmented';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TagTable } from './tag-table';
|
||||
import { TagWordCloud } from './tag-word-cloud';
|
||||
|
||||
enum TagType {
|
||||
Cloud = 'cloud',
|
||||
Table = 'table',
|
||||
}
|
||||
|
||||
const TagContentMap = {
|
||||
[TagType.Cloud]: <TagWordCloud></TagWordCloud>,
|
||||
[TagType.Table]: <TagTable></TagTable>,
|
||||
};
|
||||
|
||||
export function TagTabs() {
|
||||
const [value, setValue] = useState<TagType>(TagType.Cloud);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: SegmentedLabeledOption[] = [TagType.Cloud, TagType.Table].map(
|
||||
(x) => ({
|
||||
label: t(`knowledgeConfiguration.tag${upperFirst(x)}`),
|
||||
value: x,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="mt-4">
|
||||
<Segmented
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={(val) => setValue(val as TagType)}
|
||||
/>
|
||||
{TagContentMap[value]}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import { useFetchTagList } from '@/hooks/knowledge-hooks';
|
||||
import { Chart } from '@antv/g2';
|
||||
import { sumBy } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
export function TagWordCloud() {
|
||||
const domRef = useRef<HTMLDivElement>(null);
|
||||
let chartRef = useRef<Chart>();
|
||||
const { list } = useFetchTagList();
|
||||
|
||||
const { list: tagList } = useMemo(() => {
|
||||
const nextList = list.sort((a, b) => b[1] - a[1]).slice(0, 256);
|
||||
|
||||
return {
|
||||
list: nextList.map((x) => ({ text: x[0], value: x[1], name: x[0] })),
|
||||
sumValue: sumBy(nextList, (x: [string, number]) => x[1]),
|
||||
length: nextList.length,
|
||||
};
|
||||
}, [list]);
|
||||
|
||||
const renderWordCloud = useCallback(() => {
|
||||
if (domRef.current) {
|
||||
chartRef.current = new Chart({ container: domRef.current });
|
||||
|
||||
chartRef.current.options({
|
||||
type: 'wordCloud',
|
||||
autoFit: true,
|
||||
layout: {
|
||||
fontSize: [10, 50],
|
||||
// fontSize: (d: any) => {
|
||||
// if (d.value) {
|
||||
// return (d.value / sumValue) * 100 * (length / 10);
|
||||
// }
|
||||
// return 0;
|
||||
// },
|
||||
},
|
||||
data: {
|
||||
type: 'inline',
|
||||
value: tagList,
|
||||
},
|
||||
encode: { color: 'text' },
|
||||
legend: false,
|
||||
tooltip: {
|
||||
title: 'name', // title
|
||||
items: ['value'], // data item
|
||||
},
|
||||
});
|
||||
|
||||
chartRef.current.render();
|
||||
}
|
||||
}, [tagList]);
|
||||
|
||||
useEffect(() => {
|
||||
renderWordCloud();
|
||||
|
||||
return () => {
|
||||
chartRef.current?.destroy();
|
||||
};
|
||||
}, [renderWordCloud]);
|
||||
|
||||
return <div ref={domRef} className="w-full h-[38vh]"></div>;
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
const getImageName = (prefix: string, length: number) =>
|
||||
new Array(length)
|
||||
.fill(0)
|
||||
.map((x, idx) => `chunk-method/${prefix}-0${idx + 1}`);
|
||||
|
||||
export const ImageMap = {
|
||||
book: getImageName('book', 4),
|
||||
laws: getImageName('law', 2),
|
||||
manual: getImageName('manual', 4),
|
||||
picture: getImageName('media', 2),
|
||||
naive: getImageName('naive', 2),
|
||||
paper: getImageName('paper', 2),
|
||||
presentation: getImageName('presentation', 2),
|
||||
qa: getImageName('qa', 2),
|
||||
resume: getImageName('resume', 2),
|
||||
table: getImageName('table', 2),
|
||||
one: getImageName('one', 2),
|
||||
knowledge_graph: getImageName('knowledge-graph', 2),
|
||||
tag: getImageName('tag', 2),
|
||||
};
|
||||
@ -1,63 +0,0 @@
|
||||
.sidebarWrapper {
|
||||
max-width: 288px;
|
||||
padding: 32px 24px 24px 24px;
|
||||
flex-direction: column;
|
||||
|
||||
.sidebarTop {
|
||||
text-align: center;
|
||||
.knowledgeLogo {
|
||||
}
|
||||
.knowledgeTitle {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: @fontWeight700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.knowledgeDescription {
|
||||
font-size: 12px;
|
||||
font-weight: @fontWeight600;
|
||||
color: @gray8;
|
||||
margin: 0;
|
||||
}
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.divider {
|
||||
height: 2px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
@gray11 0%,
|
||||
@gray11 50%,
|
||||
transparent 50%
|
||||
);
|
||||
background-size: 10px 2px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.menuWrapper {
|
||||
padding-top: 10px;
|
||||
|
||||
.menu {
|
||||
border: none;
|
||||
font-size: @fontSize16;
|
||||
font-weight: @fontWeight600;
|
||||
:global(.ant-menu-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.defaultWidth {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.minWidth {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.menuText {
|
||||
color: @gray3;
|
||||
font-size: @fontSize14;
|
||||
font-weight: @fontWeight700;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,135 +0,0 @@
|
||||
import { ReactComponent as ConfigurationIcon } from '@/assets/svg/knowledge-configration.svg';
|
||||
import { ReactComponent as DatasetIcon } from '@/assets/svg/knowledge-dataset.svg';
|
||||
import { ReactComponent as TestingIcon } from '@/assets/svg/knowledge-testing.svg';
|
||||
import {
|
||||
useFetchKnowledgeBaseConfiguration,
|
||||
useFetchKnowledgeGraph,
|
||||
} from '@/hooks/knowledge-hooks';
|
||||
import {
|
||||
useGetKnowledgeSearchParams,
|
||||
useSecondPathName,
|
||||
} from '@/hooks/route-hook';
|
||||
import { getWidth } from '@/utils';
|
||||
import { Avatar, Menu, MenuProps, Space } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'umi';
|
||||
import { KnowledgeRouteKey } from '../../constant';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { GitGraph } from 'lucide-react';
|
||||
import styles from './index.less';
|
||||
|
||||
const KnowledgeSidebar = () => {
|
||||
let navigate = useNavigate();
|
||||
const activeKey = useSecondPathName();
|
||||
const { knowledgeId } = useGetKnowledgeSearchParams();
|
||||
|
||||
const [windowWidth, setWindowWidth] = useState(getWidth());
|
||||
const { t } = useTranslation();
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
const handleSelect: MenuProps['onSelect'] = (e) => {
|
||||
navigate(`/knowledge/${e.key}?id=${knowledgeId}`);
|
||||
};
|
||||
|
||||
const { data } = useFetchKnowledgeGraph();
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
|
||||
const getItem = useCallback(
|
||||
(
|
||||
label: string,
|
||||
key: React.Key,
|
||||
icon?: React.ReactNode,
|
||||
disabled?: boolean,
|
||||
children?: MenuItem[],
|
||||
type?: 'group',
|
||||
): MenuItem => {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label: t(`knowledgeDetails.${label}`),
|
||||
type,
|
||||
disabled,
|
||||
} as MenuItem;
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const list = [
|
||||
getItem(
|
||||
KnowledgeRouteKey.Dataset, // TODO: Change icon color when selected
|
||||
KnowledgeRouteKey.Dataset,
|
||||
<DatasetIcon />,
|
||||
),
|
||||
getItem(
|
||||
KnowledgeRouteKey.Testing,
|
||||
KnowledgeRouteKey.Testing,
|
||||
<TestingIcon />,
|
||||
),
|
||||
getItem(
|
||||
KnowledgeRouteKey.Configuration,
|
||||
KnowledgeRouteKey.Configuration,
|
||||
<ConfigurationIcon />,
|
||||
),
|
||||
];
|
||||
|
||||
if (!isEmpty(data?.graph)) {
|
||||
list.push(
|
||||
getItem(
|
||||
KnowledgeRouteKey.KnowledgeGraph,
|
||||
KnowledgeRouteKey.KnowledgeGraph,
|
||||
<GitGraph />,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [data, getItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const widthSize = () => {
|
||||
const width = getWidth();
|
||||
|
||||
setWindowWidth(width);
|
||||
};
|
||||
window.addEventListener('resize', widthSize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', widthSize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.sidebarWrapper}>
|
||||
<div className={styles.sidebarTop}>
|
||||
<Space size={8} direction="vertical">
|
||||
<Avatar size={64} src={knowledgeDetails.avatar} />
|
||||
<div className={styles.knowledgeTitle}>{knowledgeDetails.name}</div>
|
||||
</Space>
|
||||
<p className={styles.knowledgeDescription}>
|
||||
{knowledgeDetails.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.divider}></div>
|
||||
<div className={styles.menuWrapper}>
|
||||
<Menu
|
||||
selectedKeys={[activeKey]}
|
||||
// mode="inline"
|
||||
className={classNames(styles.menu, {
|
||||
[styles.defaultWidth]: windowWidth.width > 957,
|
||||
[styles.minWidth]: windowWidth.width <= 957,
|
||||
})}
|
||||
// inlineCollapsed={collapsed}
|
||||
items={items}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeSidebar;
|
||||
@ -1,4 +0,0 @@
|
||||
.testingWrapper {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
import {
|
||||
useTestChunkAllRetrieval,
|
||||
useTestChunkRetrieval,
|
||||
} from '@/hooks/knowledge-hooks';
|
||||
import { Flex, Form } from 'antd';
|
||||
import { useMemo, useState } from 'react';
|
||||
import TestingControl from './testing-control';
|
||||
import TestingResult from './testing-result';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const KnowledgeTesting = () => {
|
||||
const [form] = Form.useForm();
|
||||
const {
|
||||
data: retrievalData,
|
||||
testChunk,
|
||||
loading: retrievalLoading,
|
||||
} = useTestChunkRetrieval();
|
||||
const {
|
||||
data: allRetrievalData,
|
||||
testChunkAll,
|
||||
loading: allRetrievalLoading,
|
||||
} = useTestChunkAllRetrieval();
|
||||
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
|
||||
|
||||
const handleTesting = async (documentIds: string[] = []) => {
|
||||
const values = await form.validateFields();
|
||||
const params = {
|
||||
...values,
|
||||
vector_similarity_weight: 1 - values.vector_similarity_weight,
|
||||
};
|
||||
|
||||
if (Array.isArray(documentIds) && documentIds.length > 0) {
|
||||
testChunk({
|
||||
...params,
|
||||
doc_ids: documentIds,
|
||||
});
|
||||
} else {
|
||||
testChunkAll({
|
||||
...params,
|
||||
doc_ids: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const testingResult = useMemo(() => {
|
||||
return selectedDocumentIds.length > 0 ? retrievalData : allRetrievalData;
|
||||
}, [allRetrievalData, retrievalData, selectedDocumentIds.length]);
|
||||
|
||||
return (
|
||||
<Flex className={styles.testingWrapper} gap={16}>
|
||||
<TestingControl
|
||||
form={form}
|
||||
handleTesting={handleTesting}
|
||||
selectedDocumentIds={selectedDocumentIds}
|
||||
></TestingControl>
|
||||
<TestingResult
|
||||
data={testingResult}
|
||||
loading={retrievalLoading || allRetrievalLoading}
|
||||
handleTesting={handleTesting}
|
||||
selectedDocumentIds={selectedDocumentIds}
|
||||
setSelectedDocumentIds={setSelectedDocumentIds}
|
||||
></TestingResult>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeTesting;
|
||||
@ -1,29 +0,0 @@
|
||||
.testingControlWrapper {
|
||||
width: 350px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 30px 20px;
|
||||
overflow: auto;
|
||||
height: calc(100vh - 160px);
|
||||
|
||||
.historyTitle {
|
||||
padding: 30px 0 20px;
|
||||
}
|
||||
.historyIcon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.historyCardWrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.historyCard {
|
||||
width: 100%;
|
||||
:global(.ant-card-body) {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
.historyText {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
import Rerank from '@/components/rerank';
|
||||
import SimilaritySlider from '@/components/similarity-slider';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useChunkIsTesting } from '@/hooks/knowledge-hooks';
|
||||
import { Button, Card, Divider, Flex, Form, Input } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { LabelWordCloud } from './label-word-cloud';
|
||||
|
||||
import { CrossLanguageItem } from '@/components/cross-language-item';
|
||||
import { UseKnowledgeGraphItem } from '@/components/use-knowledge-graph-item';
|
||||
import styles from './index.less';
|
||||
|
||||
type FieldType = {
|
||||
similarity_threshold?: number;
|
||||
vector_similarity_weight?: number;
|
||||
question: string;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
form: FormInstance;
|
||||
handleTesting: (documentIds?: string[]) => Promise<any>;
|
||||
selectedDocumentIds: string[];
|
||||
}
|
||||
|
||||
const TestingControl = ({
|
||||
form,
|
||||
handleTesting,
|
||||
selectedDocumentIds,
|
||||
}: IProps) => {
|
||||
const question = Form.useWatch('question', { form, preserve: true });
|
||||
const loading = useChunkIsTesting();
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
|
||||
const buttonDisabled =
|
||||
!question || (typeof question === 'string' && question.trim() === '');
|
||||
|
||||
const onClick = () => {
|
||||
handleTesting(selectedDocumentIds);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.testingControlWrapper}>
|
||||
<div>
|
||||
<b>{t('testing')}</b>
|
||||
</div>
|
||||
<p>{t('testingDescription')}</p>
|
||||
<Divider></Divider>
|
||||
<section>
|
||||
<Form name="testing" layout="vertical" form={form}>
|
||||
<SimilaritySlider isTooltipShown></SimilaritySlider>
|
||||
<Rerank></Rerank>
|
||||
<UseKnowledgeGraphItem filedName={['use_kg']}></UseKnowledgeGraphItem>
|
||||
<CrossLanguageItem name={'cross_languages'}></CrossLanguageItem>
|
||||
<Card size="small" title={t('testText')}>
|
||||
<Form.Item<FieldType>
|
||||
name={'question'}
|
||||
rules={[{ required: true, message: t('testTextPlaceholder') }]}
|
||||
>
|
||||
<Input.TextArea autoSize={{ minRows: 8 }}></Input.TextArea>
|
||||
</Form.Item>
|
||||
<Flex justify={'end'}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={onClick}
|
||||
disabled={buttonDisabled}
|
||||
loading={loading}
|
||||
>
|
||||
{t('testingLabel')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Form>
|
||||
</section>
|
||||
<LabelWordCloud></LabelWordCloud>
|
||||
{/* <section>
|
||||
<div className={styles.historyTitle}>
|
||||
<Space size={'middle'}>
|
||||
<HistoryOutlined className={styles.historyIcon} />
|
||||
<b>Test history</b>
|
||||
</Space>
|
||||
</div>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={'middle'}
|
||||
className={styles.historyCardWrapper}
|
||||
>
|
||||
{list.map((x) => (
|
||||
<Card className={styles.historyCard} key={x}>
|
||||
<Flex justify={'space-between'} gap={'small'}>
|
||||
<span>{x}</span>
|
||||
<div className={styles.historyText}>
|
||||
content dcjsjl snldsh svnodvn svnodrfn svjdoghdtbnhdo
|
||||
sdvhodhbuid sldghdrlh
|
||||
</div>
|
||||
<Flex gap={'small'}>
|
||||
<span>time</span>
|
||||
<DeleteOutlined></DeleteOutlined>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</section> */}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestingControl;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user