Merge branch 'feature/end_zy' of github.com:SuanmoSuanyangTechnology/MemoryBear into feature/end_zy
This commit is contained in:
@@ -81,6 +81,7 @@ class DifyConverter(BaseConverter):
|
|||||||
NodeType.START: self.convert_start_node_config,
|
NodeType.START: self.convert_start_node_config,
|
||||||
NodeType.LLM: self.convert_llm_node_config,
|
NodeType.LLM: self.convert_llm_node_config,
|
||||||
NodeType.END: self.convert_end_node_config,
|
NodeType.END: self.convert_end_node_config,
|
||||||
|
NodeType.OUTPUT: self.convert_output_node_config,
|
||||||
NodeType.IF_ELSE: self.convert_if_else_node_config,
|
NodeType.IF_ELSE: self.convert_if_else_node_config,
|
||||||
NodeType.LOOP: self.convert_loop_node_config,
|
NodeType.LOOP: self.convert_loop_node_config,
|
||||||
NodeType.ITERATION: self.convert_iteration_node_config,
|
NodeType.ITERATION: self.convert_iteration_node_config,
|
||||||
@@ -174,12 +175,20 @@ class DifyConverter(BaseConverter):
|
|||||||
"file": VariableType.FILE,
|
"file": VariableType.FILE,
|
||||||
"paragraph": VariableType.STRING,
|
"paragraph": VariableType.STRING,
|
||||||
"text-input": VariableType.STRING,
|
"text-input": VariableType.STRING,
|
||||||
|
"string": VariableType.STRING,
|
||||||
"number": VariableType.NUMBER,
|
"number": VariableType.NUMBER,
|
||||||
"checkbox": VariableType.BOOLEAN,
|
|
||||||
"file-list": VariableType.ARRAY_FILE,
|
|
||||||
"select": VariableType.STRING,
|
|
||||||
"integer": VariableType.NUMBER,
|
"integer": VariableType.NUMBER,
|
||||||
"float": VariableType.NUMBER,
|
"float": VariableType.NUMBER,
|
||||||
|
"checkbox": VariableType.BOOLEAN,
|
||||||
|
"boolean": VariableType.BOOLEAN,
|
||||||
|
"object": VariableType.OBJECT,
|
||||||
|
"file-list": VariableType.ARRAY_FILE,
|
||||||
|
"array[string]": VariableType.ARRAY_STRING,
|
||||||
|
"array[number]": VariableType.ARRAY_NUMBER,
|
||||||
|
"array[boolean]": VariableType.ARRAY_BOOLEAN,
|
||||||
|
"array[object]": VariableType.ARRAY_OBJECT,
|
||||||
|
"array[file]": VariableType.ARRAY_FILE,
|
||||||
|
"select": VariableType.STRING,
|
||||||
}
|
}
|
||||||
var_type = type_map.get(source_type, source_type)
|
var_type = type_map.get(source_type, source_type)
|
||||||
return var_type
|
return var_type
|
||||||
@@ -274,7 +283,18 @@ class DifyConverter(BaseConverter):
|
|||||||
def convert_start_node_config(self, node: dict) -> dict:
|
def convert_start_node_config(self, node: dict) -> dict:
|
||||||
node_data = node["data"]
|
node_data = node["data"]
|
||||||
start_vars = []
|
start_vars = []
|
||||||
for var in node_data["variables"]:
|
# workflow mode 用 user_input_form,advanced-chat 用 variables
|
||||||
|
raw_vars = node_data.get("variables") or []
|
||||||
|
if not raw_vars:
|
||||||
|
for form_item in node_data.get("user_input_form") or []:
|
||||||
|
# 每个 form_item 是 {"text-input": {...}} 或 {"paragraph": {...}} 等
|
||||||
|
for input_type, var in form_item.items():
|
||||||
|
var["type"] = input_type
|
||||||
|
var.setdefault("variable", var.get("variable", ""))
|
||||||
|
var.setdefault("required", var.get("required", False))
|
||||||
|
var.setdefault("label", var.get("label", ""))
|
||||||
|
raw_vars.append(var)
|
||||||
|
for var in raw_vars:
|
||||||
var_type = self.variable_type_map(var["type"])
|
var_type = self.variable_type_map(var["type"])
|
||||||
if not var_type:
|
if not var_type:
|
||||||
self.errors.append(
|
self.errors.append(
|
||||||
@@ -404,6 +424,19 @@ class DifyConverter(BaseConverter):
|
|||||||
self.config_validate(node["id"], node["data"]["title"], EndNodeConfig, result)
|
self.config_validate(node["id"], node["data"]["title"], EndNodeConfig, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def convert_output_node_config(self, node: dict) -> dict:
|
||||||
|
node_data = node["data"]
|
||||||
|
outputs = []
|
||||||
|
for item in node_data.get("outputs", []):
|
||||||
|
value_selector = item.get("value_selector") or []
|
||||||
|
var_type = self.variable_type_map(item.get("value_type", "string")) or VariableType.STRING
|
||||||
|
outputs.append({
|
||||||
|
"name": item.get("variable") or item.get("name", ""),
|
||||||
|
"type": var_type,
|
||||||
|
"value": self._process_list_variable_literal(value_selector) or "",
|
||||||
|
})
|
||||||
|
return {"outputs": outputs}
|
||||||
|
|
||||||
def convert_if_else_node_config(self, node: dict) -> dict:
|
def convert_if_else_node_config(self, node: dict) -> dict:
|
||||||
node_data = node["data"]
|
node_data = node["data"]
|
||||||
cases = []
|
cases = []
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
|
|||||||
"start": NodeType.START,
|
"start": NodeType.START,
|
||||||
"llm": NodeType.LLM,
|
"llm": NodeType.LLM,
|
||||||
"answer": NodeType.END,
|
"answer": NodeType.END,
|
||||||
|
"end": NodeType.OUTPUT,
|
||||||
"if-else": NodeType.IF_ELSE,
|
"if-else": NodeType.IF_ELSE,
|
||||||
"loop-start": NodeType.CYCLE_START,
|
"loop-start": NodeType.CYCLE_START,
|
||||||
"iteration-start": NodeType.CYCLE_START,
|
"iteration-start": NodeType.CYCLE_START,
|
||||||
@@ -86,13 +87,6 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
|
|||||||
require_fields = frozenset({'app', 'kind', 'version', 'workflow'})
|
require_fields = frozenset({'app', 'kind', 'version', 'workflow'})
|
||||||
if not all(field in self.config for field in require_fields):
|
if not all(field in self.config for field in require_fields):
|
||||||
return False
|
return False
|
||||||
if self.config.get("app", {}).get("mode") == "workflow":
|
|
||||||
self.errors.append(ExceptionDefinition(
|
|
||||||
type=ExceptionType.PLATFORM,
|
|
||||||
detail="workflow mode is not supported"
|
|
||||||
))
|
|
||||||
return False
|
|
||||||
|
|
||||||
for node in self.origin_nodes:
|
for node in self.origin_nodes:
|
||||||
if not self._valid_nodes(node):
|
if not self._valid_nodes(node):
|
||||||
return False
|
return False
|
||||||
@@ -114,7 +108,11 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
|
|||||||
if edge:
|
if edge:
|
||||||
self.edges.append(edge)
|
self.edges.append(edge)
|
||||||
|
|
||||||
for variable in self.config.get("workflow").get("conversation_variables"):
|
mode = self.config.get("app", {}).get("mode", "advanced-chat")
|
||||||
|
conv_variables = self.config.get("workflow").get("conversation_variables") or []
|
||||||
|
if mode == "workflow":
|
||||||
|
conv_variables = []
|
||||||
|
for variable in conv_variables:
|
||||||
con_var = self._convert_variable(variable)
|
con_var = self._convert_variable(variable)
|
||||||
if variable:
|
if variable:
|
||||||
self.conv_variables.append(con_var)
|
self.conv_variables.append(con_var)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from app.core.workflow.nodes.configs import (
|
|||||||
NoteNodeConfig,
|
NoteNodeConfig,
|
||||||
ListOperatorNodeConfig,
|
ListOperatorNodeConfig,
|
||||||
DocExtractorNodeConfig,
|
DocExtractorNodeConfig,
|
||||||
|
OutputNodeConfig,
|
||||||
)
|
)
|
||||||
from app.core.workflow.nodes.enums import NodeType
|
from app.core.workflow.nodes.enums import NodeType
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class MemoryBearConverter(BaseConverter):
|
|||||||
NodeType.START: StartNodeConfig,
|
NodeType.START: StartNodeConfig,
|
||||||
NodeType.END: EndNodeConfig,
|
NodeType.END: EndNodeConfig,
|
||||||
NodeType.ANSWER: EndNodeConfig,
|
NodeType.ANSWER: EndNodeConfig,
|
||||||
|
NodeType.OUTPUT: OutputNodeConfig,
|
||||||
NodeType.LLM: LLMNodeConfig,
|
NodeType.LLM: LLMNodeConfig,
|
||||||
NodeType.AGENT: AgentNodeConfig,
|
NodeType.AGENT: AgentNodeConfig,
|
||||||
NodeType.IF_ELSE: IfElseNodeConfig,
|
NodeType.IF_ELSE: IfElseNodeConfig,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.core.workflow.nodes import NodeFactory
|
|||||||
from app.core.workflow.nodes.enums import NodeType, BRANCH_NODES
|
from app.core.workflow.nodes.enums import NodeType, BRANCH_NODES
|
||||||
from app.core.workflow.utils.expression_evaluator import evaluate_condition
|
from app.core.workflow.utils.expression_evaluator import evaluate_condition
|
||||||
from app.core.workflow.validator import WorkflowValidator
|
from app.core.workflow.validator import WorkflowValidator
|
||||||
|
from app.core.workflow.variable.base_variable import VariableType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ class GraphBuilder:
|
|||||||
(node_info["id"], node_info["branch"])
|
(node_info["id"], node_info["branch"])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if self.get_node_type(node_info["id"]) == NodeType.END:
|
if self.get_node_type(node_info["id"]) in (NodeType.END, NodeType.OUTPUT):
|
||||||
output_nodes.append(node_info["id"])
|
output_nodes.append(node_info["id"])
|
||||||
non_branch_nodes.append(node_info["id"])
|
non_branch_nodes.append(node_info["id"])
|
||||||
|
|
||||||
@@ -187,7 +188,17 @@ class GraphBuilder:
|
|||||||
for end_node in self.end_nodes:
|
for end_node in self.end_nodes:
|
||||||
end_node_id = end_node.get("id")
|
end_node_id = end_node.get("id")
|
||||||
config = end_node.get("config", {})
|
config = end_node.get("config", {})
|
||||||
output = config.get("output")
|
node_type = end_node.get("type")
|
||||||
|
|
||||||
|
# Output node: STRING type items participate in streaming text output
|
||||||
|
if node_type == NodeType.OUTPUT:
|
||||||
|
outputs_list = config.get("outputs", [])
|
||||||
|
output = "\n".join(
|
||||||
|
item.get("value", "") for item in outputs_list
|
||||||
|
if item.get("value") and item.get("type", VariableType.STRING) == VariableType.STRING
|
||||||
|
) or None
|
||||||
|
else:
|
||||||
|
output = config.get("output")
|
||||||
|
|
||||||
# Skip End nodes without output configuration
|
# Skip End nodes without output configuration
|
||||||
if not output:
|
if not output:
|
||||||
@@ -515,7 +526,7 @@ class GraphBuilder:
|
|||||||
self.end_nodes = [
|
self.end_nodes = [
|
||||||
node
|
node
|
||||||
for node in self.nodes
|
for node in self.nodes
|
||||||
if node.get("type") == "end" and node.get("id") in self.reachable_nodes
|
if node.get("type") in ("end", "output") and node.get("id") in self.reachable_nodes
|
||||||
]
|
]
|
||||||
self._build_adj()
|
self._build_adj()
|
||||||
self._find_upstream_activation_dep: Callable = lru_cache(
|
self._find_upstream_activation_dep: Callable = lru_cache(
|
||||||
|
|||||||
@@ -258,6 +258,21 @@ class WorkflowExecutor:
|
|||||||
end_time = datetime.datetime.now()
|
end_time = datetime.datetime.now()
|
||||||
elapsed_time = (end_time - start_time).total_seconds()
|
elapsed_time = (end_time - start_time).total_seconds()
|
||||||
|
|
||||||
|
# For output nodes, collect structured results from variable_pool and serialize to JSON
|
||||||
|
output_node_ids = [
|
||||||
|
node["id"] for node in self.workflow_config.get("nodes", [])
|
||||||
|
if node.get("type") == "output"
|
||||||
|
]
|
||||||
|
if output_node_ids:
|
||||||
|
structured_output = {}
|
||||||
|
for node_id in output_node_ids:
|
||||||
|
node_output = self.variable_pool.get_node_output(node_id, default=None, strict=False)
|
||||||
|
if node_output:
|
||||||
|
structured_output.update(node_output)
|
||||||
|
final_output = structured_output if structured_output else full_content
|
||||||
|
else:
|
||||||
|
final_output = full_content
|
||||||
|
|
||||||
# Append messages for user and assistant
|
# Append messages for user and assistant
|
||||||
if input_data.get("files"):
|
if input_data.get("files"):
|
||||||
result["messages"].extend(
|
result["messages"].extend(
|
||||||
@@ -301,7 +316,7 @@ class WorkflowExecutor:
|
|||||||
self.execution_context,
|
self.execution_context,
|
||||||
self.variable_pool,
|
self.variable_pool,
|
||||||
elapsed_time,
|
elapsed_time,
|
||||||
full_content,
|
final_output,
|
||||||
success=True)
|
success=True)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from app.core.workflow.nodes.variable_aggregator.config import VariableAggregato
|
|||||||
from app.core.workflow.nodes.notes.config import NoteNodeConfig
|
from app.core.workflow.nodes.notes.config import NoteNodeConfig
|
||||||
from app.core.workflow.nodes.list_operator.config import ListOperatorNodeConfig
|
from app.core.workflow.nodes.list_operator.config import ListOperatorNodeConfig
|
||||||
from app.core.workflow.nodes.document_extractor.config import DocExtractorNodeConfig
|
from app.core.workflow.nodes.document_extractor.config import DocExtractorNodeConfig
|
||||||
|
from app.core.workflow.nodes.output.config import OutputNodeConfig
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# 基础类
|
# 基础类
|
||||||
@@ -54,4 +55,5 @@ __all__ = [
|
|||||||
"NoteNodeConfig",
|
"NoteNodeConfig",
|
||||||
"ListOperatorNodeConfig",
|
"ListOperatorNodeConfig",
|
||||||
"DocExtractorNodeConfig",
|
"DocExtractorNodeConfig",
|
||||||
|
"OutputNodeConfig"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class NodeType(StrEnum):
|
|||||||
MEMORY_WRITE = "memory-write"
|
MEMORY_WRITE = "memory-write"
|
||||||
DOCUMENT_EXTRACTOR = "document-extractor"
|
DOCUMENT_EXTRACTOR = "document-extractor"
|
||||||
LIST_OPERATOR = "list-operator"
|
LIST_OPERATOR = "list-operator"
|
||||||
|
OUTPUT = "output"
|
||||||
|
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
NOTES = "notes"
|
NOTES = "notes"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ LLM 节点实现
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage
|
from langchain_core.messages import AIMessage
|
||||||
@@ -81,7 +80,7 @@ class LLMNode(BaseNode):
|
|||||||
|
|
||||||
def _render_context(self, message: str, variable_pool: VariablePool):
|
def _render_context(self, message: str, variable_pool: VariablePool):
|
||||||
context = f"<context>{self._render_template(self.typed_config.context, variable_pool)}</context>"
|
context = f"<context>{self._render_template(self.typed_config.context, variable_pool)}</context>"
|
||||||
return re.sub(r"{{context}}", context, message)
|
return message.replace("{{context}}", context)
|
||||||
|
|
||||||
async def _prepare_llm(
|
async def _prepare_llm(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from app.core.workflow.nodes.breaker import BreakNode
|
|||||||
from app.core.workflow.nodes.tool import ToolNode
|
from app.core.workflow.nodes.tool import ToolNode
|
||||||
from app.core.workflow.nodes.document_extractor import DocExtractorNode
|
from app.core.workflow.nodes.document_extractor import DocExtractorNode
|
||||||
from app.core.workflow.nodes.list_operator import ListOperatorNode
|
from app.core.workflow.nodes.list_operator import ListOperatorNode
|
||||||
|
from app.core.workflow.nodes.output import OutputNode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -53,7 +54,8 @@ WorkflowNode = Union[
|
|||||||
MemoryWriteNode,
|
MemoryWriteNode,
|
||||||
CodeNode,
|
CodeNode,
|
||||||
DocExtractorNode,
|
DocExtractorNode,
|
||||||
ListOperatorNode
|
ListOperatorNode,
|
||||||
|
OutputNode
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +88,8 @@ class NodeFactory:
|
|||||||
NodeType.MEMORY_WRITE: MemoryWriteNode,
|
NodeType.MEMORY_WRITE: MemoryWriteNode,
|
||||||
NodeType.CODE: CodeNode,
|
NodeType.CODE: CodeNode,
|
||||||
NodeType.DOCUMENT_EXTRACTOR: DocExtractorNode,
|
NodeType.DOCUMENT_EXTRACTOR: DocExtractorNode,
|
||||||
NodeType.LIST_OPERATOR: ListOperatorNode
|
NodeType.LIST_OPERATOR: ListOperatorNode,
|
||||||
|
NodeType.OUTPUT: OutputNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
4
api/app/core/workflow/nodes/output/__init__.py
Normal file
4
api/app/core/workflow/nodes/output/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from app.core.workflow.nodes.output.node import OutputNode
|
||||||
|
from app.core.workflow.nodes.output.config import OutputNodeConfig
|
||||||
|
|
||||||
|
__all__ = ["OutputNode", "OutputNodeConfig"]
|
||||||
14
api/app/core/workflow/nodes/output/config.py
Normal file
14
api/app/core/workflow/nodes/output/config.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from typing import Any
|
||||||
|
from pydantic import Field
|
||||||
|
from app.core.workflow.nodes.base_config import BaseNodeConfig
|
||||||
|
from app.core.workflow.variable.base_variable import VariableType
|
||||||
|
|
||||||
|
|
||||||
|
class OutputItemConfig(BaseNodeConfig):
|
||||||
|
name: str
|
||||||
|
type: VariableType = VariableType.STRING
|
||||||
|
value: Any = ""
|
||||||
|
|
||||||
|
|
||||||
|
class OutputNodeConfig(BaseNodeConfig):
|
||||||
|
outputs: list[OutputItemConfig] = Field(default_factory=list)
|
||||||
49
api/app/core/workflow/nodes/output/node.py
Normal file
49
api/app/core/workflow/nodes/output/node.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Output 节点实现
|
||||||
|
|
||||||
|
工作流的输出节点(类似 Dify workflow 的 end 节点),
|
||||||
|
用于定义工作流的最终输出变量,不产生流式输出。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.core.workflow.engine.state_manager import WorkflowState
|
||||||
|
from app.core.workflow.engine.variable_pool import VariablePool
|
||||||
|
from app.core.workflow.nodes.base_node import BaseNode
|
||||||
|
from app.core.workflow.variable.base_variable import VariableType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OutputNode(BaseNode):
|
||||||
|
"""
|
||||||
|
Output 节点
|
||||||
|
|
||||||
|
工作流的输出节点,收集并输出指定变量的值。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _output_types(self) -> dict[str, VariableType]:
|
||||||
|
outputs = self.config.get("outputs", [])
|
||||||
|
return {
|
||||||
|
item["name"]: VariableType(item.get("type", VariableType.STRING))
|
||||||
|
for item in outputs if item.get("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, state: WorkflowState, variable_pool: VariablePool) -> dict[str, Any]:
|
||||||
|
outputs = self.config.get("outputs", [])
|
||||||
|
result = {}
|
||||||
|
for item in outputs:
|
||||||
|
name = item.get("name")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
var_type = VariableType(item.get("type", VariableType.STRING))
|
||||||
|
value = item.get("value", "")
|
||||||
|
if var_type == VariableType.STRING:
|
||||||
|
result[name] = self._render_template(str(value), variable_pool, strict=False)
|
||||||
|
elif isinstance(value, str) and value.strip().startswith("{{") and value.strip().endswith("}}"):
|
||||||
|
selector = value.strip()[2:-2].strip()
|
||||||
|
result[name] = variable_pool.get_value(selector, default=None, strict=False)
|
||||||
|
else:
|
||||||
|
result[name] = value
|
||||||
|
return result
|
||||||
@@ -132,10 +132,10 @@ class WorkflowValidator:
|
|||||||
errors.append(f"工作流只能有一个 start 节点,当前有 {len(start_nodes)} 个")
|
errors.append(f"工作流只能有一个 start 节点,当前有 {len(start_nodes)} 个")
|
||||||
|
|
||||||
if index == len(graphs) - 1:
|
if index == len(graphs) - 1:
|
||||||
# 2. 验证 主图end 节点(至少一个)
|
# 2. 验证 主图end 节点(至少一个,output 节点也可作为终止节点)
|
||||||
end_nodes = [n for n in nodes if n.get("type") == NodeType.END]
|
end_nodes = [n for n in nodes if n.get("type") in [NodeType.END, NodeType.OUTPUT]]
|
||||||
if len(end_nodes) == 0:
|
if len(end_nodes) == 0:
|
||||||
errors.append("工作流必须至少有一个 end 节点")
|
errors.append("工作流必须至少有一个 end 节点 或 output 节点")
|
||||||
|
|
||||||
# 3. 验证节点 ID 唯一性
|
# 3. 验证节点 ID 唯一性
|
||||||
node_ids = [n.get("id") for n in nodes if n.get("type") != NodeType.NOTES]
|
node_ids = [n.get("id") for n in nodes if n.get("type") != NodeType.NOTES]
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
|
|||||||
return request.put(`/apps/${app_id}/workflow`, values)
|
return request.put(`/apps/${app_id}/workflow`, values)
|
||||||
}
|
}
|
||||||
// Model comparison test run
|
// Model comparison test run
|
||||||
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void, onAbort?: (abort: () => void) => void) => {
|
||||||
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
|
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage, undefined, onAbort)
|
||||||
}
|
}
|
||||||
// Test run
|
// Test run
|
||||||
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void, onAbort?: (abort: () => void) => void) => {
|
||||||
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
|
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage, undefined, onAbort)
|
||||||
}
|
}
|
||||||
// Delete application
|
// Delete application
|
||||||
export const deleteApplication = (app_id: string) => {
|
export const deleteApplication = (app_id: string) => {
|
||||||
@@ -93,12 +93,12 @@ export const getConversationHistory = (share_token: string, data: { page: number
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Send conversation
|
// Send conversation
|
||||||
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
|
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string, onAbort?: (abort: () => void) => void) => {
|
||||||
return handleSSE(`/public/share/chat`, values, onMessage, {
|
return handleSSE(`/public/share/chat`, values, onMessage, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${shareToken}`
|
'Authorization': `Bearer ${shareToken}`
|
||||||
}
|
}
|
||||||
})
|
}, onAbort)
|
||||||
}
|
}
|
||||||
// Get conversation details
|
// Get conversation details
|
||||||
export const getConversationDetail = (share_token: string, conversation_id: string) => {
|
export const getConversationDetail = (share_token: string, conversation_id: string) => {
|
||||||
|
|||||||
@@ -274,8 +274,8 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
|
|||||||
return request.post('/memory-storage/update_config_extracted', values)
|
return request.post('/memory-storage/update_config_extracted', values)
|
||||||
}
|
}
|
||||||
// Memory Extraction Engine - Pilot run
|
// Memory Extraction Engine - Pilot run
|
||||||
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; custom_text?: string; }, onMessage?: (data: SSEMessage[]) => void) => {
|
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; custom_text?: string; }, onMessage?: (data: SSEMessage[]) => void, onAbort?: (abort: () => void) => void) => {
|
||||||
return handleSSE('/memory-storage/pilot_run', values, onMessage)
|
return handleSSE('/memory-storage/pilot_run', values, onMessage, undefined, onAbort)
|
||||||
}
|
}
|
||||||
// Emotion Engine - Get configuration
|
// Emotion Engine - Get configuration
|
||||||
export const getMemoryEmotionConfig = (config_id: number | string) => {
|
export const getMemoryEmotionConfig = (config_id: number | string) => {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export const createPromptSessions = () => {
|
|||||||
return request.post(`/prompt/sessions`)
|
return request.post(`/prompt/sessions`)
|
||||||
}
|
}
|
||||||
// Get prompt optimization
|
// Get prompt optimization
|
||||||
export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void) => {
|
export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void, config?: any, onAbort?: (abort: () => void) => void) => {
|
||||||
return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage)
|
return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage, config, onAbort)
|
||||||
}
|
}
|
||||||
// Prompt release list
|
// Prompt release list
|
||||||
export const getPromptReleaseListUrl = '/prompt/releases/list'
|
export const getPromptReleaseListUrl = '/prompt/releases/list'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 15:29:46
|
* @Date: 2026-02-02 15:29:46
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-26 14:52:23
|
* @Last Modified time: 2026-04-14 17:55:15
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* RbTable Component
|
* RbTable Component
|
||||||
@@ -27,7 +27,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { request } from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
import Empty from '@/components/Empty';
|
import Empty from '@/components/Empty';
|
||||||
|
|
||||||
interface TablePaginationConfig { pagesize: number; page: number; }
|
interface TablePaginationConfig { pagesize?: number; page?: number; }
|
||||||
|
|
||||||
/** Props interface for Table component */
|
/** Props interface for Table component */
|
||||||
interface TableComponentProps<T = Record<string, unknown>, Q = Record<string, unknown>> extends Omit<TableProps<T>, 'pagination'> {
|
interface TableComponentProps<T = Record<string, unknown>, Q = Record<string, unknown>> extends Omit<TableProps<T>, 'pagination'> {
|
||||||
@@ -102,7 +102,7 @@ const RbTable = forwardRef(<T = Record<string, unknown>, Q = Record<string, unkn
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [currentPagination, setCurrentPagination] = useState({
|
const [currentPagination, setCurrentPagination] = useState({
|
||||||
page: 1,
|
page: 1,
|
||||||
pagesize: 20,
|
pagesize: typeof pagination === 'object' ? (pagination.pagesize || 20) : 20,
|
||||||
});
|
});
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
|||||||
@@ -2519,6 +2519,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
arrange: 'Arrange',
|
arrange: 'Arrange',
|
||||||
redo: 'Redo',
|
redo: 'Redo',
|
||||||
undo: 'Undo',
|
undo: 'Undo',
|
||||||
|
fit: 'Fit View',
|
||||||
|
|
||||||
input_result: 'Input',
|
input_result: 'Input',
|
||||||
output_result: 'Output',
|
output_result: 'Output',
|
||||||
|
|||||||
@@ -2483,6 +2483,7 @@ export const zh = {
|
|||||||
arrange: '整理',
|
arrange: '整理',
|
||||||
redo: '重做',
|
redo: '重做',
|
||||||
undo: '撤销',
|
undo: '撤销',
|
||||||
|
fit: '自适应',
|
||||||
|
|
||||||
input_result: '输入',
|
input_result: '输入',
|
||||||
output_result: '输出',
|
output_result: '输出',
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
*/
|
*/
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
|
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
|
||||||
|
import type { ChatItem } from '@/components/Chat/types'
|
||||||
|
|
||||||
interface WorkflowState {
|
interface WorkflowState {
|
||||||
checkResults: Record<string, NodeCheckResult[]>
|
checkResults: Record<string, NodeCheckResult[]>
|
||||||
setCheckResults: (appId: string, results: NodeCheckResult[]) => void
|
setCheckResults: (appId: string, results: NodeCheckResult[]) => void
|
||||||
getCheckResults: (appId: string) => NodeCheckResult[]
|
getCheckResults: (appId: string) => NodeCheckResult[]
|
||||||
|
chatHistoryMap: Record<string, ChatItem[]>
|
||||||
|
setChatHistory: (conversationId: string, history: ChatItem[]) => void
|
||||||
|
getChatHistory: (conversationId: string) => ChatItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
@@ -18,4 +22,8 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
setCheckResults: (appId, results) =>
|
setCheckResults: (appId, results) =>
|
||||||
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
|
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
|
||||||
getCheckResults: (appId) => get().checkResults[appId] ?? [],
|
getCheckResults: (appId) => get().checkResults[appId] ?? [],
|
||||||
|
chatHistoryMap: {},
|
||||||
|
setChatHistory: (conversationId, history) =>
|
||||||
|
set(state => ({ chatHistoryMap: { ...state.chatHistoryMap, [conversationId]: history } })),
|
||||||
|
getChatHistory: (conversationId) => get().chatHistoryMap[conversationId] ?? [],
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 16:35:43
|
* @Date: 2026-02-02 16:35:43
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-18 14:32:40
|
* @Last Modified time: 2026-04-21 14:20:39
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Server-Sent Events (SSE) Stream Utility Module
|
* Server-Sent Events (SSE) Stream Utility Module
|
||||||
@@ -148,7 +148,7 @@ function parseDataContent(dataContent: string): string | object {
|
|||||||
* @param config - Additional request configuration
|
* @param config - Additional request configuration
|
||||||
* @returns Fetch response
|
* @returns Fetch response
|
||||||
*/
|
*/
|
||||||
const makeSSERequest = async (url: string, data: any, token: string, config = { headers: {} }) => {
|
const makeSSERequest = async (url: string, data: any, token: string, config = { headers: {} }, signal?: AbortSignal) => {
|
||||||
return fetch(`${API_PREFIX}${url}`, {
|
return fetch(`${API_PREFIX}${url}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -156,7 +156,8 @@ const makeSSERequest = async (url: string, data: any, token: string, config = {
|
|||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
...config.headers,
|
...config.headers,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
|
signal,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,10 +168,14 @@ const makeSSERequest = async (url: string, data: any, token: string, config = {
|
|||||||
* @param onMessage - Callback for each parsed message
|
* @param onMessage - Callback for each parsed message
|
||||||
* @param config - Additional request configuration
|
* @param config - Additional request configuration
|
||||||
*/
|
*/
|
||||||
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => {
|
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }, onAbort?: (abort: () => void) => void) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const abort = () => controller.abort();
|
||||||
|
onAbort?.(abort);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let token = cookieUtils.get('authToken');
|
let token = cookieUtils.get('authToken');
|
||||||
let response = await makeSSERequest(url, data, token || '', config);
|
let response = await makeSSERequest(url, data, token || '', config, controller.signal);
|
||||||
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 500:
|
case 500:
|
||||||
@@ -199,7 +204,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const newToken = await refreshTokenForSSE();
|
const newToken = await refreshTokenForSSE();
|
||||||
response = await makeSSERequest(url, data, newToken, config);
|
response = await makeSSERequest(url, data, newToken, config, controller.signal);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -211,30 +216,37 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
|||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = ''; // Buffer for handling incomplete messages
|
let buffer = ''; // Buffer for handling incomplete messages
|
||||||
|
|
||||||
while (true) {
|
try {
|
||||||
const { done, value } = await reader.read();
|
while (true) {
|
||||||
if (done) break;
|
const { done, value } = await reader.read();
|
||||||
|
if (done || controller.signal.aborted) break;
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
buffer += chunk;
|
buffer += chunk;
|
||||||
|
|
||||||
// Process complete events
|
// Process complete events
|
||||||
const events = buffer.split('\n\n');
|
const events = buffer.split('\n\n');
|
||||||
buffer = events.pop() || ''; // Keep last potentially incomplete event
|
buffer = events.pop() || ''; // Keep last potentially incomplete event
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.trim() && onMessage) {
|
if (event.trim() && onMessage) {
|
||||||
onMessage(parseSSEToJSON(event) ?? {});
|
onMessage(parseSSEToJSON(event) ?? {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Process remaining buffer content
|
// Process remaining buffer content
|
||||||
if (buffer.trim() && onMessage) {
|
if (!controller.signal.aborted && buffer.trim() && onMessage) {
|
||||||
onMessage(parseSSEToJSON(buffer) ?? {});
|
onMessage(parseSSEToJSON(buffer) ?? {});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.cancel();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.name !== 'AbortError') {
|
||||||
|
console.error('Request failed:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Request failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -92,6 +92,7 @@ const TestChat: FC<TestChatProps> = ({
|
|||||||
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
||||||
const streamLoadingRef = useRef(false)
|
const streamLoadingRef = useRef(false)
|
||||||
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
|
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVariables()
|
getVariables()
|
||||||
@@ -99,6 +100,8 @@ const TestChat: FC<TestChatProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
audioPollingRef.current.forEach(timer => clearInterval(timer))
|
audioPollingRef.current.forEach(timer => clearInterval(timer))
|
||||||
audioPollingRef.current.clear()
|
audioPollingRef.current.clear()
|
||||||
}
|
}
|
||||||
@@ -262,7 +265,8 @@ const TestChat: FC<TestChatProps> = ({
|
|||||||
draftRun(
|
draftRun(
|
||||||
application.id,
|
application.id,
|
||||||
formatParams((msg || message) as string, conversationId, files, params),
|
formatParams((msg || message) as string, conversationId, files, params),
|
||||||
handleStreamMessage
|
handleStreamMessage,
|
||||||
|
(abort) => { abortRef.current = abort }
|
||||||
)
|
)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
updateErrorAssistantMessage(0)
|
updateErrorAssistantMessage(0)
|
||||||
@@ -373,7 +377,8 @@ const TestChat: FC<TestChatProps> = ({
|
|||||||
draftRun(
|
draftRun(
|
||||||
application.id,
|
application.id,
|
||||||
formatParams((msg || message) as string, conversationId, files, params),
|
formatParams((msg || message) as string, conversationId, files, params),
|
||||||
handleWorkflowStreamMessage
|
handleWorkflowStreamMessage,
|
||||||
|
(abort) => { abortRef.current = abort }
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorInfo = JSON.parse(error.message)
|
const errorInfo = JSON.parse(error.message)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:26:44
|
* @Date: 2026-02-03 16:26:44
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-20 13:53:05
|
* @Last Modified time: 2026-04-21 14:50:21
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* AI Prompt Assistant Modal
|
* AI Prompt Assistant Modal
|
||||||
@@ -61,11 +61,14 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null)
|
const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null)
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<any>(null)
|
||||||
const currentPromptValueRef = useRef<string>('')
|
const currentPromptValueRef = useRef<string>('')
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
const values = Form.useWatch([], form)
|
const values = Form.useWatch([], form)
|
||||||
|
|
||||||
/** Close modal and reset state */
|
/** Close modal and reset state */
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setChatList([])
|
setChatList([])
|
||||||
@@ -148,7 +151,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
updatePromptMessages(promptSession, {
|
updatePromptMessages(promptSession, {
|
||||||
...values,
|
...values,
|
||||||
skill: source === 'skills'
|
skill: source === 'skills'
|
||||||
}, handleStreamMessage)
|
}, handleStreamMessage, undefined, abort => { abortRef.current = abort })
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
@@ -221,7 +224,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<ChatContent
|
<ChatContent
|
||||||
classNames="rb:h-105.5 rb:pb-[15px]!"
|
classNames="rb:h-[calc(100vh-330px)] rb:pb-[15px]!"
|
||||||
contentClassNames="rb:max-w-75!"
|
contentClassNames="rb:max-w-75!"
|
||||||
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
|
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
|
||||||
data={chatList || []}
|
data={chatList || []}
|
||||||
@@ -292,10 +295,10 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
{values?.current_prompt
|
{values?.current_prompt
|
||||||
? <Editor
|
? <Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className="rb:h-119 rb:bg-white! rb:border-none! rb:p-0!"
|
className="rb:h-[calc(100vh-278px)] rb:bg-white! rb:border-none! rb:p-0!"
|
||||||
onChange={(value) => form.setFieldValue('current_prompt', value)}
|
onChange={(value) => form.setFieldValue('current_prompt', value)}
|
||||||
/>
|
/>
|
||||||
: <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-119 rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
|
: <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-[calc(100vh-278px)] rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
|
||||||
}
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,11 +73,14 @@ const Chat: FC<ChatProps> = ({
|
|||||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||||
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
||||||
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
|
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCompareLoading(false)
|
setCompareLoading(false)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return () => {
|
return () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
audioPollingRef.current.forEach(timer => clearInterval(timer))
|
audioPollingRef.current.forEach(timer => clearInterval(timer))
|
||||||
audioPollingRef.current.clear()
|
audioPollingRef.current.clear()
|
||||||
}
|
}
|
||||||
@@ -85,6 +88,8 @@ const Chat: FC<ChatProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
audioPollingRef.current.forEach(timer => clearInterval(timer))
|
audioPollingRef.current.forEach(timer => clearInterval(timer))
|
||||||
audioPollingRef.current.clear()
|
audioPollingRef.current.clear()
|
||||||
}
|
}
|
||||||
@@ -393,7 +398,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
parallel: true,
|
parallel: true,
|
||||||
stream: true,
|
stream: true,
|
||||||
timeout: 60,
|
timeout: 60,
|
||||||
}, handleStreamMessage)
|
}, handleStreamMessage, (abort) => { abortRef.current = abort })
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setCompareLoading(false)
|
setCompareLoading(false)
|
||||||
@@ -537,7 +542,8 @@ const Chat: FC<ChatProps> = ({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
handleStreamMessage
|
handleStreamMessage,
|
||||||
|
(abort) => { abortRef.current = abort }
|
||||||
)
|
)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:58:03
|
* @Date: 2026-02-03 16:58:03
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-13 18:32:58
|
* @Last Modified time: 2026-04-21 14:27:15
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Conversation Page
|
* Conversation Page
|
||||||
@@ -53,6 +53,7 @@ const Conversation: FC = () => {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||||
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
||||||
const [fileList, setFileList] = useState<any[]>([])
|
const [fileList, setFileList] = useState<any[]>([])
|
||||||
const [webSearch, setWebSearch] = useState(false)
|
const [webSearch, setWebSearch] = useState(false)
|
||||||
@@ -67,6 +68,8 @@ const Conversation: FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
audioPollingRef.current.forEach((timer) => clearInterval(timer))
|
audioPollingRef.current.forEach((timer) => clearInterval(timer))
|
||||||
audioPollingRef.current.clear()
|
audioPollingRef.current.clear()
|
||||||
}
|
}
|
||||||
@@ -150,6 +153,8 @@ const Conversation: FC = () => {
|
|||||||
const handleChangeHistory = (id: string | null) => {
|
const handleChangeHistory = (id: string | null) => {
|
||||||
if (id !== conversation_id) setConversationId(id)
|
if (id !== conversation_id) setConversationId(id)
|
||||||
if (!id) setMessage('')
|
if (!id) setMessage('')
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -406,7 +411,7 @@ const Conversation: FC = () => {
|
|||||||
}),
|
}),
|
||||||
variables: params,
|
variables: params,
|
||||||
thinking,
|
thinking,
|
||||||
}, handleStreamMessage, shareToken)
|
}, handleStreamMessage, shareToken, (abort) => { abortRef.current = abort })
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
streamLoadingRef.current = false
|
streamLoadingRef.current = false
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ const Index = () => {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
scrollY="100%"
|
scrollY="100%"
|
||||||
|
pagination={{pagesize: 10}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 17:30:11
|
* @Date: 2026-02-03 17:30:11
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-26 15:46:30
|
* @Last Modified time: 2026-04-21 14:54:14
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Result Component
|
* Result Component
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
* Shows text preprocessing, knowledge extraction, node/edge creation, and deduplication
|
* Shows text preprocessing, knowledge extraction, node/edge creation, and deduplication
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type FC, useState } from 'react'
|
import { type FC, useState, useRef, useEffect } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Space, Button, Progress, Form, Input, Flex } from 'antd'
|
import { Space, Button, Progress, Form, Input, Flex } from 'antd'
|
||||||
@@ -105,7 +105,14 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
|||||||
|
|
||||||
const [runForm] = Form.useForm()
|
const [runForm] = Form.useForm()
|
||||||
const customText = Form.useWatch(['custom_text'], runForm)
|
const customText = Form.useWatch(['custom_text'], runForm)
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null;
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
/** Run pilot test */
|
/** Run pilot test */
|
||||||
const handleRun = () => {
|
const handleRun = () => {
|
||||||
if(!id) return
|
if(!id) return
|
||||||
@@ -229,11 +236,13 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
setRunLoading(true)
|
setRunLoading(true)
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null;
|
||||||
pilotRunMemoryExtractionConfig({
|
pilotRunMemoryExtractionConfig({
|
||||||
config_id: id,
|
config_id: id,
|
||||||
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
||||||
custom_text: runForm.getFieldValue('custom_text')
|
custom_text: runForm.getFieldValue('custom_text')
|
||||||
}, handleStreamMessage)
|
}, handleStreamMessage, (abort) => { abortRef.current = abort })
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setRunLoading(false)
|
setRunLoading(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 17:44:15
|
* @Date: 2026-02-03 17:44:15
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-27 15:14:58
|
* @Last Modified time: 2026-04-21 14:24:00
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Prompt Editor Component
|
* Prompt Editor Component
|
||||||
@@ -46,9 +46,17 @@ const Prompt: FC = () => {
|
|||||||
const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
|
const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
|
||||||
const editorRef = useRef<any>(null)
|
const editorRef = useRef<any>(null)
|
||||||
const currentPromptValueRef = useRef<string>(undefined)
|
const currentPromptValueRef = useRef<string>(undefined)
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
const values = Form.useWatch([], form)
|
const values = Form.useWatch([], form)
|
||||||
const [editVo, setEditVo] = useState<HistoryItem | null>(null)
|
const [editVo, setEditVo] = useState<HistoryItem | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditVo(state)
|
setEditVo(state)
|
||||||
}, [state])
|
}, [state])
|
||||||
@@ -126,7 +134,7 @@ const Prompt: FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
updatePromptMessages((promptSession) as string, values, handleStreamMessage)
|
updatePromptMessages((promptSession) as string, values, handleStreamMessage, undefined, (abort) => { abortRef.current = abort })
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Select, Divider } from 'antd';
|
import { Select, Divider, Tooltip } from 'antd';
|
||||||
import { PlusOutlined, MinusOutlined, FileAddOutlined } from '@ant-design/icons'
|
import { PlusOutlined, MinusOutlined, FileAddOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Node } from '@antv/x6';
|
import { Node } from '@antv/x6';
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import type { GraphRef } from '../types'
|
import type { GraphRef } from '../types'
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ interface CanvasToolbarProps {
|
|||||||
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
addNotes: () => void;
|
addNotes: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
canRedo: boolean;
|
||||||
|
onUndo: () => void;
|
||||||
|
onRedo: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||||
@@ -22,12 +27,13 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
|||||||
miniMapRef,
|
miniMapRef,
|
||||||
graphRef,
|
graphRef,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
// canUndo,
|
canUndo,
|
||||||
// canRedo,
|
canRedo,
|
||||||
// onUndo,
|
onUndo,
|
||||||
// onRedo,
|
onRedo,
|
||||||
addNotes,
|
addNotes,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 小地图 */}
|
{/* 小地图 */}
|
||||||
@@ -63,13 +69,16 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
|||||||
{ label: '125%', value: 125 },
|
{ label: '125%', value: 125 },
|
||||||
{ label: '150%', value: 150 },
|
{ label: '150%', value: 150 },
|
||||||
{ label: '200%', value: 200 },
|
{ label: '200%', value: 200 },
|
||||||
{ label: '自适应', value: 'fit' },
|
{ label: t('workflow.fit'), value: 'fit' },
|
||||||
]}
|
]}
|
||||||
variant='borderless'
|
variant='borderless'
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
|
<PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
|
||||||
<Divider type="vertical" className="rb:h-4" />
|
<Divider type="vertical" className="rb:h-4" />
|
||||||
|
<Tooltip title={`${t('workflow.undo')} (Ctrl+Z)`}><UndoOutlined className={clsx('rb:text-[16px]', canUndo ? 'rb:cursor-pointer' : 'rb:opacity-30 rb:cursor-not-allowed')} onClick={onUndo} /></Tooltip>
|
||||||
|
<Tooltip title={`${t('workflow.redo')} (Ctrl+Y)`}><RedoOutlined className={clsx('rb:text-[16px]', canRedo ? 'rb:cursor-pointer' : 'rb:opacity-30 rb:cursor-not-allowed')} onClick={onRedo} /></Tooltip>
|
||||||
|
<Divider type="vertical" className="rb:h-4" />
|
||||||
<FileAddOutlined onClick={addNotes} />
|
<FileAddOutlined onClick={addNotes} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-06 21:10:56
|
* @Date: 2026-02-06 21:10:56
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-07 18:07:38
|
* @Last Modified time: 2026-04-21 14:59:13
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Workflow Chat Component
|
* Workflow Chat Component
|
||||||
@@ -41,13 +41,17 @@ import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
|||||||
import Runtime from './Runtime';
|
import Runtime from './Runtime';
|
||||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||||
import { replaceVariables } from '@/views/ApplicationConfig/Agent';
|
import { replaceVariables } from '@/views/ApplicationConfig/Agent';
|
||||||
|
import { useWorkflowStore } from '@/store/workflow';
|
||||||
|
|
||||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({
|
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({ // eslint-disable-line
|
||||||
appId, graphRef, features
|
appId, graphRef, features
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { message: messageApi } = App.useApp()
|
const { message: messageApi } = App.useApp()
|
||||||
|
const { setChatHistory } = useWorkflowStore()
|
||||||
|
const conversationIdRef = useRef<string>('draft')
|
||||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||||
|
const abortRef = useRef<(() => void) | null>(null)
|
||||||
const [toolbarReady, setToolbarReady] = useState(false)
|
const [toolbarReady, setToolbarReady] = useState(false)
|
||||||
const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => {
|
const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => {
|
||||||
(toolbarRef as React.MutableRefObject<ChatToolbarRef | null>).current = node
|
(toolbarRef as React.MutableRefObject<ChatToolbarRef | null>).current = node
|
||||||
@@ -62,6 +66,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
const [fileList, setFileList] = useState<any[]>([])
|
const [fileList, setFileList] = useState<any[]>([])
|
||||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
console.log('abortRef', abortRef)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the chat drawer and loads workflow variables from the start node
|
* Opens the chat drawer and loads workflow variables from the start node
|
||||||
*/
|
*/
|
||||||
@@ -113,11 +119,14 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
* Closes the drawer and resets all state
|
* Closes the drawer and resets all state
|
||||||
*/
|
*/
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
abortRef.current?.()
|
||||||
|
abortRef.current = null;
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
setToolbarReady(false)
|
setToolbarReady(false)
|
||||||
setChatList([])
|
setChatList([])
|
||||||
setVariables([])
|
setVariables([])
|
||||||
setConversationId(null)
|
setConversationId(null)
|
||||||
|
conversationIdRef.current = 'draft'
|
||||||
setMessage(undefined)
|
setMessage(undefined)
|
||||||
toolbarRef.current?.setFiles([])
|
toolbarRef.current?.setFiles([])
|
||||||
toolbarRef.current?.setVariables([])
|
toolbarRef.current?.setVariables([])
|
||||||
@@ -189,7 +198,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
elapsed_time?: string;
|
elapsed_time?: string;
|
||||||
error?: any;
|
error?: any;
|
||||||
state: Record<string, any>;
|
state: Record<string, any>;
|
||||||
status?: 'completed' | 'failed',
|
status?: 'completed' | 'failed' | 'running',
|
||||||
citations?: {
|
citations?: {
|
||||||
document_id: string;
|
document_id: string;
|
||||||
file_name: string;
|
file_name: string;
|
||||||
@@ -231,6 +240,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
node_name: name,
|
node_name: name,
|
||||||
node_type: type,
|
node_type: type,
|
||||||
icon,
|
icon,
|
||||||
|
status: 'running',
|
||||||
content: {},
|
content: {},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -240,6 +250,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
node_name: name,
|
node_name: name,
|
||||||
node_type: type,
|
node_type: type,
|
||||||
icon,
|
icon,
|
||||||
|
status: 'running',
|
||||||
content: {},
|
content: {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -344,6 +355,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (conversation_id && conversationId !== conversation_id) {
|
if (conversation_id && conversationId !== conversation_id) {
|
||||||
|
conversationIdRef.current = conversation_id
|
||||||
setConversationId(conversation_id)
|
setConversationId(conversation_id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -388,7 +400,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
])
|
])
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setStreamLoading(true)
|
setStreamLoading(true)
|
||||||
draftRun(appId, data, handleStreamMessage)
|
draftRun(appId, data, handleStreamMessage, abort => { abortRef.current = abort })
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorInfo = JSON.parse(error.message)
|
const errorInfo = JSON.parse(error.message)
|
||||||
setChatList(prev => {
|
setChatList(prev => {
|
||||||
@@ -440,6 +452,10 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
}
|
}
|
||||||
}, [chatList.length, features?.opening_statement, variables])
|
}, [chatList.length, features?.opening_statement, variables])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChatHistory(conversationIdRef.current, chatList)
|
||||||
|
}, [chatList])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbDrawer
|
<RbDrawer
|
||||||
title={<Flex align="center" gap={10}>
|
title={<Flex align="center" gap={10}>
|
||||||
|
|||||||
@@ -34,29 +34,24 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
|
|||||||
>
|
>
|
||||||
<Flex vertical align={collapsed ? 'center' : undefined} gap={collapsed ? 8 : 16}>
|
<Flex vertical align={collapsed ? 'center' : undefined} gap={collapsed ? 8 : 16}>
|
||||||
{collapsed
|
{collapsed
|
||||||
? <>
|
? nodeLibrary.flatMap(category =>
|
||||||
{nodeLibrary.map(category => (
|
category.nodes
|
||||||
<>
|
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||||
{category.nodes
|
.map(node => (
|
||||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
<Tooltip key={node.type} title={t(`workflow.${node.type}`)} placement="right">
|
||||||
.map((node, nodeIndex) => (
|
<div
|
||||||
<Tooltip key={nodeIndex} title={t(`workflow.${node.type}`)} placement="right">
|
className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]"
|
||||||
<div
|
draggable
|
||||||
className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]"
|
onDragStart={(e) => {
|
||||||
draggable
|
e.dataTransfer.setData('application/reactflow', node.type);
|
||||||
onDragStart={(e) => {
|
e.dataTransfer.setData('application/json', JSON.stringify(node));
|
||||||
e.dataTransfer.setData('application/reactflow', node.type);
|
}}
|
||||||
e.dataTransfer.setData('application/json', JSON.stringify(node));
|
>
|
||||||
}}
|
<div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} />
|
||||||
>
|
</div>
|
||||||
<div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} />
|
</Tooltip>
|
||||||
</div>
|
))
|
||||||
</Tooltip>
|
)
|
||||||
))
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
: nodeLibrary.map(category => (
|
: nodeLibrary.map(category => (
|
||||||
<div
|
<div
|
||||||
key={category.category}
|
key={category.category}
|
||||||
@@ -65,9 +60,9 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
|
|||||||
<Flex gap={6} vertical>
|
<Flex gap={6} vertical>
|
||||||
{category.nodes
|
{category.nodes
|
||||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||||
.map((node, nodeIndex) => (
|
.map((node) => (
|
||||||
<Flex
|
<Flex
|
||||||
key={nodeIndex}
|
key={node.type}
|
||||||
align="center"
|
align="center"
|
||||||
gap={8}
|
gap={8}
|
||||||
className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"
|
className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import NodeTools from './NodeTools'
|
import NodeTools from './NodeTools'
|
||||||
import { useVariableList } from '../Properties/hooks/useVariableList'
|
import { useVariableList } from '../Properties/hooks/useVariableList'
|
||||||
@@ -64,13 +65,23 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||||
'rb:border-[#171719]': data.isSelected,
|
'rb:border-[#171719]!': data.isSelected,
|
||||||
'rb:border-[#FCFCFD]': !data.isSelected
|
'rb:border-[#FCFCFD]': !data.isSelected,
|
||||||
|
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
|
||||||
|
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
|
||||||
})}>
|
})}>
|
||||||
<NodeTools node={node} />
|
<NodeTools node={node} />
|
||||||
<Flex align="center" gap={8} className="rb:flex-1">
|
<Flex align="center" gap={8} className="rb:flex-1">
|
||||||
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
||||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
<div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||||
|
{data.executionStatus === 'completed'
|
||||||
|
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
|
||||||
|
: data.executionStatus === 'failed'
|
||||||
|
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
|
||||||
|
: data.executionStatus === 'running'
|
||||||
|
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{data.type === 'question-classifier' &&
|
{data.type === 'question-classifier' &&
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import { graphNodeLibrary, edgeAttrs } from '../../constant';
|
import { graphNodeLibrary, edgeAttrs } from '../../constant';
|
||||||
import NodeTools from './NodeTools'
|
import NodeTools from './NodeTools'
|
||||||
@@ -131,12 +132,22 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||||
'rb:border-[#171719]': data.isSelected,
|
'rb:border-[#171719]': data.isSelected,
|
||||||
'rb:border-[#FCFCFD]': !data.isSelected
|
'rb:border-[#FCFCFD]': !data.isSelected,
|
||||||
|
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
|
||||||
|
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
|
||||||
})}>
|
})}>
|
||||||
<NodeTools node={node} />
|
<NodeTools node={node} />
|
||||||
<Flex align="center" gap={8} className="rb:flex-1">
|
<Flex align="center" gap={8} className="rb:flex-1">
|
||||||
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
||||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
<div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||||
|
{data.executionStatus === 'completed'
|
||||||
|
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
|
||||||
|
: data.executionStatus === 'failed'
|
||||||
|
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
|
||||||
|
: data.executionStatus === 'running'
|
||||||
|
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div>
|
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import NodeTools from './NodeTools'
|
import NodeTools from './NodeTools'
|
||||||
|
|
||||||
@@ -11,13 +12,23 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||||
'rb:border-[#171719]': data.isSelected,
|
'rb:border-[#171719]!': data.isSelected,
|
||||||
'rb:border-[#FCFCFD]': !data.isSelected
|
'rb:border-[#FCFCFD]': !data.isSelected,
|
||||||
|
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
|
||||||
|
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
|
||||||
})}>
|
})}>
|
||||||
<NodeTools node={node} />
|
<NodeTools node={node} />
|
||||||
<Flex align="center" gap={8} className="rb:flex-1">
|
<Flex align="center" gap={8} className="rb:flex-1">
|
||||||
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
||||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
<div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||||
|
{data.executionStatus === 'completed'
|
||||||
|
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
|
||||||
|
: data.executionStatus === 'failed'
|
||||||
|
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
|
||||||
|
: data.executionStatus === 'running'
|
||||||
|
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-3">{t('workflow.clickToConfigure')}</div>
|
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-3">{t('workflow.clickToConfigure')}</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:39:59
|
* @Date: 2026-02-03 15:39:59
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-21 18:36:42
|
* @Last Modified time: 2026-04-21 18:44:15
|
||||||
*/
|
*/
|
||||||
import { type FC, useEffect, useState, useMemo } from "react";
|
import { type FC, useEffect, useState, useMemo } from "react";
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -154,7 +154,9 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
selectedNode?.setData({
|
selectedNode?.setData({
|
||||||
...nodeData,
|
...nodeData,
|
||||||
...allRest,
|
...allRest,
|
||||||
})
|
},
|
||||||
|
// { deep: false }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [values, selectedNode, form])
|
}, [values, selectedNode, form])
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:17:48
|
* @Date: 2026-02-03 15:17:48
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-14 17:43:14
|
* @Last Modified time: 2026-04-20 16:00:26
|
||||||
*/
|
*/
|
||||||
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, type Edge } from '@antv/x6';
|
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6';
|
||||||
|
import type { HistoryCommand as Command } from '@antv/x6/lib/plugin/history/type';
|
||||||
import { register } from '@antv/x6-react-shape';
|
import { register } from '@antv/x6-react-shape';
|
||||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
@@ -18,6 +19,7 @@ import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
|||||||
import { conditionNodeHeight, conditionNodeItemHeight, conditionNodePortItemArgsY, defaultAbsolutePortGroups, defaultPortItems, edgeAttrs, edgeHoverTool, edge_color, edge_selected_color, edge_width, graphNodeLibrary, nodeLibrary, nodeRegisterLibrary, nodeWidth, notesConfig, portAttrs, portItemArgsY, portMarkup, portTextAttrs, unknownNode } from '../constant';
|
import { conditionNodeHeight, conditionNodeItemHeight, conditionNodePortItemArgsY, defaultAbsolutePortGroups, defaultPortItems, edgeAttrs, edgeHoverTool, edge_color, edge_selected_color, edge_width, graphNodeLibrary, nodeLibrary, nodeRegisterLibrary, nodeWidth, notesConfig, portAttrs, portItemArgsY, portMarkup, portTextAttrs, unknownNode } from '../constant';
|
||||||
import type { ChatVariable, NodeProperties, WorkflowConfig } from '../types';
|
import type { ChatVariable, NodeProperties, WorkflowConfig } from '../types';
|
||||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils';
|
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils';
|
||||||
|
import { useWorkflowStore } from '@/store/workflow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for useWorkflowGraph hook
|
* Props for useWorkflowGraph hook
|
||||||
@@ -63,6 +65,14 @@ export interface UseWorkflowGraphReturn {
|
|||||||
copyEvent: () => boolean | void;
|
copyEvent: () => boolean | void;
|
||||||
/** Handler for paste keyboard event */
|
/** Handler for paste keyboard event */
|
||||||
parseEvent: () => boolean | void;
|
parseEvent: () => boolean | void;
|
||||||
|
/** Whether undo is available */
|
||||||
|
canUndo: boolean;
|
||||||
|
/** Whether redo is available */
|
||||||
|
canRedo: boolean;
|
||||||
|
/** Undo last action */
|
||||||
|
undo: () => void;
|
||||||
|
/** Redo last undone action */
|
||||||
|
redo: () => void;
|
||||||
/** Function to save workflow configuration */
|
/** Function to save workflow configuration */
|
||||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||||
/** Chat variables for workflow */
|
/** Chat variables for workflow */
|
||||||
@@ -94,6 +104,8 @@ export const useWorkflowGraph = ({
|
|||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
const { chatHistoryMap } = useWorkflowStore()
|
||||||
|
const chatHistory = Object.values(chatHistoryMap).at(-1) ?? []
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const graphRef = useRef<Graph>();
|
const graphRef = useRef<Graph>();
|
||||||
@@ -105,6 +117,8 @@ export const useWorkflowGraph = ({
|
|||||||
const [config, setConfig] = useState<WorkflowConfig | null>(null);
|
const [config, setConfig] = useState<WorkflowConfig | null>(null);
|
||||||
const [chatVariables, setChatVariables] = useState<ChatVariable[]>([])
|
const [chatVariables, setChatVariables] = useState<ChatVariable[]>([])
|
||||||
const featuresRef = useRef<FeaturesConfigForm | undefined>(undefined)
|
const featuresRef = useRef<FeaturesConfigForm | undefined>(undefined)
|
||||||
|
const [canUndo, setCanUndo] = useState(false)
|
||||||
|
const [canRedo, setCanRedo] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!graphRef.current) return
|
if (!graphRef.current) return
|
||||||
@@ -470,6 +484,8 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.getNodes().forEach(node => {
|
graphRef.current.getNodes().forEach(node => {
|
||||||
if (node.getData()?.cycle) node.toFront();
|
if (node.getData()?.cycle) node.toFront();
|
||||||
});
|
});
|
||||||
|
graphRef.current.enableHistory()
|
||||||
|
graphRef.current.cleanHistory()
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
@@ -505,6 +521,22 @@ export const useWorkflowGraph = ({
|
|||||||
global: true,
|
global: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
graphRef.current.use(
|
||||||
|
new History({
|
||||||
|
enabled: false,
|
||||||
|
beforeAddCommand(_event, args: any) {
|
||||||
|
const event = args?.key ? `cell:change:${args.key}` : _event;
|
||||||
|
if (event.startsWith('cell:change:') &&
|
||||||
|
event !== 'cell:change:position' &&
|
||||||
|
event !== 'cell:change:source' &&
|
||||||
|
event !== 'cell:change:target') return false;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
graphRef.current.on('history:change', ({ cmds }: { cmds: Command[] }) => {
|
||||||
|
setCanUndo(graphRef.current?.canUndo() ?? false)
|
||||||
|
setCanRedo(graphRef.current?.canRedo() ?? false)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
// 显示/隐藏连接桩
|
// 显示/隐藏连接桩
|
||||||
// const showPorts = (show: boolean) => {
|
// const showPorts = (show: boolean) => {
|
||||||
@@ -1093,6 +1125,9 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.bindKey(['ctrl+v', 'cmd+v'], parseEvent);
|
graphRef.current.bindKey(['ctrl+v', 'cmd+v'], parseEvent);
|
||||||
// Delete selected nodes and edges
|
// Delete selected nodes and edges
|
||||||
graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent);
|
graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent);
|
||||||
|
// Undo / Redo
|
||||||
|
graphRef.current.bindKey(['ctrl+z', 'cmd+z'], () => { graphRef.current?.undo(); return false; });
|
||||||
|
graphRef.current.bindKey(['ctrl+y', 'cmd+y', 'ctrl+shift+z', 'cmd+shift+z'], () => { graphRef.current?.redo(); return false; });
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1411,6 +1446,9 @@ export const useWorkflowGraph = ({
|
|||||||
return userVars
|
return userVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const undo = () => graphRef.current?.undo()
|
||||||
|
const redo = () => graphRef.current?.redo()
|
||||||
|
|
||||||
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
|
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
|
||||||
const { statement = '' } = value?.opening_statement || {}
|
const { statement = '' } = value?.opening_statement || {}
|
||||||
featuresRef.current = value
|
featuresRef.current = value
|
||||||
@@ -1446,6 +1484,31 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (!graphRef.current) return;
|
||||||
|
const nodes = graphRef.current.getNodes();
|
||||||
|
|
||||||
|
const lastWithSub = [...chatHistory].reverse().find(item => item.subContent?.length);
|
||||||
|
// Reset all node execution status first
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const data = node.getData();
|
||||||
|
if (typeof data.status === 'string') {
|
||||||
|
node.setData({ ...data, executionStatus: undefined });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!lastWithSub?.subContent) return;
|
||||||
|
// Build a nodeId -> status map first
|
||||||
|
const statusMap: Record<string, string> = {};
|
||||||
|
lastWithSub.subContent.forEach(sub => {
|
||||||
|
if (typeof sub.status === 'string') {
|
||||||
|
statusMap[sub.node_id] = sub.status;
|
||||||
|
const node = nodes.find(n => n.getData()?.id === sub.node_id);
|
||||||
|
if (node) {
|
||||||
|
node.setData({ ...node.getData(), executionStatus: sub.status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [chatHistory, graphRef.current]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
@@ -1470,5 +1533,9 @@ export const useWorkflowGraph = ({
|
|||||||
handleSaveFeaturesConfig,
|
handleSaveFeaturesConfig,
|
||||||
features: featuresRef.current,
|
features: featuresRef.current,
|
||||||
getStartNodeVariables,
|
getStartNodeVariables,
|
||||||
|
canUndo,
|
||||||
|
canRedo,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
handleSaveFeaturesConfig,
|
handleSaveFeaturesConfig,
|
||||||
features,
|
features,
|
||||||
getStartNodeVariables,
|
getStartNodeVariables,
|
||||||
|
canUndo,
|
||||||
|
canRedo,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
|
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
|
||||||
|
|
||||||
const onDragOver = (event: React.DragEvent) => {
|
const onDragOver = (event: React.DragEvent) => {
|
||||||
@@ -96,6 +100,10 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
setIsHandMode={setIsHandMode}
|
setIsHandMode={setIsHandMode}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
addNotes={handleAddNotes}
|
addNotes={handleAddNotes}
|
||||||
|
canUndo={canUndo}
|
||||||
|
canRedo={canRedo}
|
||||||
|
onUndo={undo}
|
||||||
|
onRedo={redo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user