Compare commits

...

6 Commits

Author SHA1 Message Date
8604c4f57c Feat: add GPT-5.1, GPT‑5.1 Instant and Claude-Opus-4.5 (#11559)
### What problem does this PR solve?

Add GPT-5.1, GPT‑5.1 Instant and Claude-Opus-4.5. #11548

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-11-27 17:59:17 +08:00
a674338c21 Fix: remove garbage filtering rules (#11567)
### What problem does this PR solve?
change:

remove garbage filtering rules

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-11-27 17:54:49 +08:00
89d82ff031 Feat: Delete useless knowledge base, chat, and search files. #10427 (#11568)
### What problem does this PR solve?

Feat: Delete useless knowledge base, chat, and search files.  #10427
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-11-27 17:54:27 +08:00
c71d25f744 Fix: enable structured output for agent with tool (#11558)
### What problem does this PR solve?

issue:
[#11541](https://github.com/infiniflow/ragflow/issues/11541)
change:
enable structured output for agent with tool

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-11-27 16:00:56 +08:00
f57f32cf3a Feat: Add loop operator node. #10427 (#11449)
### What problem does this PR solve?

Feat: Add loop operator node. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-11-27 15:55:46 +08:00
b6314164c5 Feat:new component Loop (#11447)
### What problem does this PR solve?
issue:
#10427
change: 
new component Loop

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-11-27 15:55:32 +08:00
232 changed files with 1810 additions and 12266 deletions

View File

@ -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:

View File

@ -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()

View 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 ""

View File

@ -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
View 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
View 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..."

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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):

View 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>
);
}

View File

@ -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';

View 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>
);
}

View File

@ -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';

View File

@ -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';

View File

@ -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`;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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 {

View File

@ -92,3 +92,5 @@ export enum DocumentParserType {
Tag = 'tag',
KnowledgeGraph = 'knowledge_graph',
}
export const TagRenameId = 'tagRename';

View File

@ -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,

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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[];
}

View File

@ -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',

View File

@ -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: '正在运行中...🕞',

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;
};

View File

@ -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);
}
}

View File

@ -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);

View File

@ -1,4 +0,0 @@
export enum ChunkTextMode {
Full = 'full',
Ellipse = 'ellipse',
}

View File

@ -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,
};
};

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}, []);
}

View File

@ -1,7 +0,0 @@
import { Outlet } from 'umi';
export const KnowledgeDataset = () => {
return <Outlet></Outlet>;
};
export default KnowledgeDataset;

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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,
};
};

View File

@ -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;
}

View File

@ -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;

View File

@ -1,3 +0,0 @@
.iconButton {
padding: 4px 8px;
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -1,6 +0,0 @@
import { RunningStatus } from './constant';
export const isParserRunning = (text: RunningStatus) => {
const isRunning = text === RunningStatus.RUNNING;
return isRunning;
};

View File

@ -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;

View File

@ -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: [],
};

View File

@ -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;

View File

@ -1,5 +0,0 @@
.forceContainer {
:global(.tooltip) {
border-radius: 10px !important;
}
}

View File

@ -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;

View File

@ -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 };
}

View File

@ -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 };
};

View File

@ -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;

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
});

View File

@ -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>
</>
);
}

View File

@ -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>
);
};

View File

@ -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>
</>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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,
};
};

View File

@ -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%;
}
}

View File

@ -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;

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>;
}

View File

@ -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),
};

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -1,4 +0,0 @@
.testingWrapper {
flex: 1;
height: 100%;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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