Compare commits
35 Commits
v0.2.10
...
hotfix/v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068e2bfb7e | ||
|
|
4ce6fede67 | ||
|
|
3e0f59adc6 | ||
|
|
660cd2fadb | ||
|
|
61b5627505 | ||
|
|
af6392fb09 | ||
|
|
84b1a95313 | ||
|
|
8b21dab255 | ||
|
|
5226c5b79d | ||
|
|
27e9f9968d | ||
|
|
d38612a10d | ||
|
|
32c71dcd89 | ||
|
|
428e7ebaa5 | ||
|
|
57833689d9 | ||
|
|
384a67482c | ||
|
|
ca4f7aa65d | ||
|
|
130684cac0 | ||
|
|
62e0b2730b | ||
|
|
a7b8ba0c66 | ||
|
|
0516822d42 | ||
|
|
b598171a3d | ||
|
|
a4ea7f0385 | ||
|
|
32ae60fc65 | ||
|
|
6b272c5b44 | ||
|
|
2782d0661f | ||
|
|
ea2f5e61c9 | ||
|
|
5975d70bf9 | ||
|
|
70aab94fc3 | ||
|
|
b7c1ce261b | ||
|
|
edac6a164e | ||
|
|
1503b242ea | ||
|
|
18fd48505d | ||
|
|
807ddce5cd | ||
|
|
62fb6c79a0 | ||
|
|
cc373b2864 |
@@ -23,6 +23,7 @@ from app.models.user_model import User
|
|||||||
from app.schemas import chunk_schema
|
from app.schemas import chunk_schema
|
||||||
from app.schemas.response_schema import ApiResponse
|
from app.schemas.response_schema import ApiResponse
|
||||||
from app.services import knowledge_service, document_service, file_service, knowledgeshare_service
|
from app.services import knowledge_service, document_service, file_service, knowledgeshare_service
|
||||||
|
from app.services.model_service import ModelApiKeyService
|
||||||
|
|
||||||
# Obtain a dedicated API logger
|
# Obtain a dedicated API logger
|
||||||
api_logger = get_api_logger()
|
api_logger = get_api_logger()
|
||||||
@@ -460,18 +461,20 @@ async def retrieve_chunks(
|
|||||||
if retrieve_data.retrieve_type == chunk_schema.RetrieveType.Graph:
|
if retrieve_data.retrieve_type == chunk_schema.RetrieveType.Graph:
|
||||||
kb_ids = [str(kb_id) for kb_id in private_kb_ids]
|
kb_ids = [str(kb_id) for kb_id in private_kb_ids]
|
||||||
workspace_ids = [str(workspace_id) for workspace_id in private_workspace_ids]
|
workspace_ids = [str(workspace_id) for workspace_id in private_workspace_ids]
|
||||||
|
llm_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.llm_id)
|
||||||
|
emb_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.embedding_id)
|
||||||
# Prepare to configure chat_mdl、embedding_model、vision_model information
|
# Prepare to configure chat_mdl、embedding_model、vision_model information
|
||||||
chat_model = Base(
|
chat_model = Base(
|
||||||
key=db_knowledge.llm.api_keys[0].api_key,
|
key=llm_key.api_key,
|
||||||
model_name=db_knowledge.llm.api_keys[0].model_name,
|
model_name=llm_key.model_name,
|
||||||
base_url=db_knowledge.llm.api_keys[0].api_base
|
base_url=llm_key.api_base
|
||||||
)
|
)
|
||||||
embedding_model = OpenAIEmbed(
|
embedding_model = OpenAIEmbed(
|
||||||
key=db_knowledge.embedding.api_keys[0].api_key,
|
key=emb_key.api_key,
|
||||||
model_name=db_knowledge.embedding.api_keys[0].model_name,
|
model_name=emb_key.model_name,
|
||||||
base_url=db_knowledge.embedding.api_keys[0].api_base
|
base_url=emb_key.api_base
|
||||||
)
|
)
|
||||||
doc = kg_retriever.retrieval(question=retrieve_data.query, workspace_ids=workspace_ids, kb_ids= kb_ids, emb_mdl=embedding_model, llm=chat_model)
|
doc = kg_retriever.retrieval(question=retrieve_data.query, workspace_ids=workspace_ids, kb_ids=kb_ids, emb_mdl=embedding_model, llm=chat_model)
|
||||||
if doc:
|
if doc:
|
||||||
rs.insert(0, doc)
|
rs.insert(0, doc)
|
||||||
return success(data=jsonable_encoder(rs), msg="retrieval successful")
|
return success(data=jsonable_encoder(rs), msg="retrieval successful")
|
||||||
@@ -292,9 +292,10 @@ class MinerUParser(RAGPdfParser):
|
|||||||
self.page_from = page_from
|
self.page_from = page_from
|
||||||
self.page_to = page_to
|
self.page_to = page_to
|
||||||
try:
|
try:
|
||||||
with pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm)) as pdf:
|
with sys.modules[LOCK_KEY_pdfplumber]: # ← 加这一行,获取全局锁
|
||||||
self.pdf = pdf
|
with pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm)) as pdf:
|
||||||
self.page_images = [p.to_image(resolution=72 * zoomin, antialias=True).original for _, p in enumerate(self.pdf.pages[page_from:page_to])]
|
self.pdf = pdf
|
||||||
|
self.page_images = [p.to_image(resolution=72 * zoomin, antialias=True).original for _, p in enumerate(self.pdf.pages[page_from:page_to])]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.page_images = None
|
self.page_images = None
|
||||||
self.total_page = 0
|
self.total_page = 0
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from app.core.rag.common.float_utils import get_float
|
|||||||
from app.core.rag.common.constants import PAGERANK_FLD, TAG_FLD
|
from app.core.rag.common.constants import PAGERANK_FLD, TAG_FLD
|
||||||
from app.core.rag.llm.chat_model import Base
|
from app.core.rag.llm.chat_model import Base
|
||||||
from app.core.rag.llm.embedding_model import OpenAIEmbed
|
from app.core.rag.llm.embedding_model import OpenAIEmbed
|
||||||
|
from app.services.model_service import ModelApiKeyService
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -114,9 +115,8 @@ def knowledge_retrieval(
|
|||||||
# Use the specified reranker for re-ranking
|
# Use the specified reranker for re-ranking
|
||||||
if reranker_id:
|
if reranker_id:
|
||||||
try:
|
try:
|
||||||
return rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k)
|
all_results = rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k)
|
||||||
except Exception as rerank_error:
|
except Exception as rerank_error:
|
||||||
# If reranker fails, log warning and continue with original results
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Reranker failed, falling back to original results",
|
"Reranker failed, falling back to original results",
|
||||||
extra={
|
extra={
|
||||||
@@ -132,7 +132,10 @@ def knowledge_retrieval(
|
|||||||
from app.core.rag.common.settings import kg_retriever
|
from app.core.rag.common.settings import kg_retriever
|
||||||
doc = kg_retriever.retrieval(question=query, workspace_ids=workspace_ids, kb_ids=kb_ids, emb_mdl=embedding_model, llm=chat_model)
|
doc = kg_retriever.retrieval(question=query, workspace_ids=workspace_ids, kb_ids=kb_ids, emb_mdl=embedding_model, llm=chat_model)
|
||||||
if doc:
|
if doc:
|
||||||
all_results.insert(0, doc)
|
all_results.insert(0, DocumentChunk(
|
||||||
|
page_content=doc.get("page_content", ""),
|
||||||
|
metadata=doc.get("metadata", {})
|
||||||
|
))
|
||||||
except Exception as graph_error:
|
except Exception as graph_error:
|
||||||
print(f"Failed to retrieve from knowledge graph: {str(graph_error)}")
|
print(f"Failed to retrieve from knowledge graph: {str(graph_error)}")
|
||||||
|
|
||||||
@@ -198,16 +201,18 @@ def _retrieve_for_knowledge(
|
|||||||
workspace_ids.append(str(db_knowledge.workspace_id))
|
workspace_ids.append(str(db_knowledge.workspace_id))
|
||||||
|
|
||||||
if not chat_model:
|
if not chat_model:
|
||||||
|
llm_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.llm_id)
|
||||||
chat_model = Base(
|
chat_model = Base(
|
||||||
key=db_knowledge.llm.api_keys[0].api_key,
|
key=llm_key.api_key,
|
||||||
model_name=db_knowledge.llm.api_keys[0].model_name,
|
model_name=llm_key.model_name,
|
||||||
base_url=db_knowledge.llm.api_keys[0].api_base,
|
base_url=llm_key.api_base,
|
||||||
)
|
)
|
||||||
if not embedding_model:
|
if not embedding_model:
|
||||||
|
emb_key = ModelApiKeyService.get_available_api_key(db, db_knowledge.embedding_id)
|
||||||
embedding_model = OpenAIEmbed(
|
embedding_model = OpenAIEmbed(
|
||||||
key=db_knowledge.embedding.api_keys[0].api_key,
|
key=emb_key.api_key,
|
||||||
model_name=db_knowledge.embedding.api_keys[0].model_name,
|
model_name=emb_key.model_name,
|
||||||
base_url=db_knowledge.embedding.api_keys[0].api_base,
|
base_url=emb_key.api_base,
|
||||||
)
|
)
|
||||||
|
|
||||||
vector_service = ElasticSearchVectorFactory().init_vector(knowledge=db_knowledge)
|
vector_service = ElasticSearchVectorFactory().init_vector(knowledge=db_knowledge)
|
||||||
@@ -248,6 +253,29 @@ def _retrieve_for_knowledge(
|
|||||||
seen_ids.add(doc.metadata["doc_id"])
|
seen_ids.add(doc.metadata["doc_id"])
|
||||||
unique_rs.append(doc)
|
unique_rs.append(doc)
|
||||||
rs = unique_rs
|
rs = unique_rs
|
||||||
|
if unique_rs:
|
||||||
|
rs = vector_service.rerank(
|
||||||
|
query=kb_config["query"],
|
||||||
|
docs=unique_rs,
|
||||||
|
top_k=kb_config["top_k"]
|
||||||
|
)
|
||||||
|
if kb_config["retrieve_type"] == "graph":
|
||||||
|
try:
|
||||||
|
from app.core.rag.common.settings import kg_retriever
|
||||||
|
graph_doc = kg_retriever.retrieval(
|
||||||
|
question=kb_config["query"],
|
||||||
|
workspace_ids=[str(db_knowledge.workspace_id)],
|
||||||
|
kb_ids=[str(db_knowledge.id)],
|
||||||
|
emb_mdl=embedding_model,
|
||||||
|
llm=chat_model,
|
||||||
|
)
|
||||||
|
if graph_doc:
|
||||||
|
rs.insert(0, DocumentChunk(
|
||||||
|
page_content=graph_doc.get("page_content", ""),
|
||||||
|
metadata=graph_doc.get("metadata", {})
|
||||||
|
))
|
||||||
|
except Exception as graph_error:
|
||||||
|
logger.warning(f"Graph retrieval failed for kb {db_knowledge.id}: {graph_error}")
|
||||||
|
|
||||||
results.extend(rs)
|
results.extend(rs)
|
||||||
return results, chat_model, embedding_model
|
return results, chat_model, embedding_model
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class DateTimeTool(BuiltinTool):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _datetime_to_timestamp(kwargs) -> dict:
|
def _datetime_to_timestamp(kwargs) -> dict:
|
||||||
"""日期时间转时间戳"""
|
"""日期时间转时间戳"""
|
||||||
input_value = kwargs.get("input_value")
|
input_value = kwargs.get("input_value").strip()
|
||||||
input_format = kwargs.get("input_format", "%Y-%m-%d %H:%M:%S")
|
input_format = kwargs.get("input_format", "%Y-%m-%d %H:%M:%S")
|
||||||
timezone_str = kwargs.get("from_timezone", "Asia/Shanghai")
|
timezone_str = kwargs.get("from_timezone", "Asia/Shanghai")
|
||||||
|
|
||||||
@@ -253,9 +253,9 @@ class DateTimeTool(BuiltinTool):
|
|||||||
return {
|
return {
|
||||||
"datetime": input_value,
|
"datetime": input_value,
|
||||||
"timezone": timezone_str,
|
"timezone": timezone_str,
|
||||||
"timestamp": int(dt.timestamp()),
|
"timestamp": int(dt.timestamp()) * 1000,
|
||||||
"iso_format": dt.isoformat(),
|
"iso_format": dt.isoformat(),
|
||||||
"result_data": int(dt.timestamp())
|
"result_data": int(dt.timestamp()) * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
def _calculate_datetime(self, kwargs) -> dict:
|
def _calculate_datetime(self, kwargs) -> dict:
|
||||||
|
|||||||
@@ -138,6 +138,29 @@ class OperationTool(BaseTool):
|
|||||||
default="Asia/Shanghai"
|
default="Asia/Shanghai"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
elif self.operation == "datetime_to_timestamp":
|
||||||
|
return [
|
||||||
|
ToolParameter(
|
||||||
|
name="input_value",
|
||||||
|
type=ParameterType.STRING,
|
||||||
|
description="输入值(时间字符串,如:2026-04-07 10:30:25)",
|
||||||
|
required=True
|
||||||
|
),
|
||||||
|
ToolParameter(
|
||||||
|
name="input_format",
|
||||||
|
type=ParameterType.STRING,
|
||||||
|
description="输入时间格式(如:%Y-%m-%d %H:%M:%S)",
|
||||||
|
required=False,
|
||||||
|
default="%Y-%m-%d %H:%M:%S"
|
||||||
|
),
|
||||||
|
ToolParameter(
|
||||||
|
name="from_timezone",
|
||||||
|
type=ParameterType.STRING,
|
||||||
|
description="源时区(如:UTC, Asia/Shanghai)",
|
||||||
|
required=False,
|
||||||
|
default="Asia/Shanghai"
|
||||||
|
)
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Example:
|
# Example:
|
||||||
# "Hello {{user.name}}!" ->
|
# "Hello {{user.name}}!" ->
|
||||||
# ["Hello ", "{{user.name}}", "!"]
|
# ["Hello ", "{{user.name}}", "!"]
|
||||||
_OUTPUT_PATTERN = re.compile(r'\{\{.*?}}|[^{}]+')
|
_OUTPUT_PATTERN = re.compile(r'\{\{.*?}}|[^{]+|{')
|
||||||
# Strict variable format: {{ node_id.field_name }}
|
# Strict variable format: {{ node_id.field_name }}
|
||||||
_VARIABLE_PATTERN = re.compile(r'\{\{\s*[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+\s*}}')
|
_VARIABLE_PATTERN = re.compile(r'\{\{\s*[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+\s*}}')
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ class CycleGraphNode(BaseNode):
|
|||||||
if config.output_type in [
|
if config.output_type in [
|
||||||
VariableType.ARRAY_FILE,
|
VariableType.ARRAY_FILE,
|
||||||
VariableType.ARRAY_STRING,
|
VariableType.ARRAY_STRING,
|
||||||
VariableType.NUMBER,
|
VariableType.ARRAY_NUMBER,
|
||||||
VariableType.ARRAY_OBJECT,
|
VariableType.ARRAY_OBJECT,
|
||||||
VariableType.BOOLEAN
|
VariableType.ARRAY_BOOLEAN
|
||||||
]:
|
]:
|
||||||
if config.flatten:
|
if config.flatten:
|
||||||
outputs['output'] = config.output_type
|
outputs['output'] = config.output_type
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from langchain_core.documents import Document
|
|||||||
from app.core.error_codes import BizCode
|
from app.core.error_codes import BizCode
|
||||||
from app.core.exceptions import BusinessException
|
from app.core.exceptions import BusinessException
|
||||||
from app.core.models import RedBearRerank, RedBearModelConfig
|
from app.core.models import RedBearRerank, RedBearModelConfig
|
||||||
|
from app.core.rag.llm.chat_model import Base
|
||||||
|
from app.core.rag.llm.embedding_model import OpenAIEmbed
|
||||||
from app.core.rag.models.chunk import DocumentChunk
|
from app.core.rag.models.chunk import DocumentChunk
|
||||||
from app.core.rag.vdb.elasticsearch.elasticsearch_vector import ElasticSearchVectorFactory
|
from app.core.rag.vdb.elasticsearch.elasticsearch_vector import ElasticSearchVectorFactory
|
||||||
from app.core.workflow.engine.state_manager import WorkflowState
|
from app.core.workflow.engine.state_manager import WorkflowState
|
||||||
@@ -39,8 +41,9 @@ class KnowledgeRetrievalNode(BaseNode):
|
|||||||
if isinstance(business_result, dict) and "chunks" in business_result:
|
if isinstance(business_result, dict) and "chunks" in business_result:
|
||||||
return business_result["chunks"]
|
return business_result["chunks"]
|
||||||
return business_result
|
return business_result
|
||||||
|
|
||||||
def _extract_citations(self, business_result: Any) -> list:
|
@staticmethod
|
||||||
|
def _extract_citations(business_result: Any) -> list:
|
||||||
if isinstance(business_result, dict):
|
if isinstance(business_result, dict):
|
||||||
return business_result.get("citations", [])
|
return business_result.get("citations", [])
|
||||||
return []
|
return []
|
||||||
@@ -230,23 +233,23 @@ class KnowledgeRetrievalNode(BaseNode):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case RetrieveType.HYBRID:
|
case retrieve_type if retrieve_type in (RetrieveType.HYBRID, RetrieveType.Graph):
|
||||||
rs1_task = asyncio.to_thread(
|
rs1_task = asyncio.to_thread(
|
||||||
vector_service.search_by_vector, **{
|
vector_service.search_by_vector, **{
|
||||||
"query": query,
|
"query": query,
|
||||||
"top_k": kb_config.top_k,
|
"top_k": kb_config.top_k,
|
||||||
"indices": indices,
|
"indices": indices,
|
||||||
"score_threshold": kb_config.vector_similarity_weight
|
"score_threshold": kb_config.vector_similarity_weight
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rs2_task = asyncio.to_thread(
|
rs2_task = asyncio.to_thread(
|
||||||
vector_service.search_by_full_text, **{
|
vector_service.search_by_full_text, **{
|
||||||
"query": query,
|
"query": query,
|
||||||
"top_k": kb_config.top_k,
|
"top_k": kb_config.top_k,
|
||||||
"indices": indices,
|
"indices": indices,
|
||||||
"score_threshold": kb_config.similarity_threshold
|
"score_threshold": kb_config.similarity_threshold
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rs1, rs2 = await asyncio.gather(rs1_task, rs2_task)
|
rs1, rs2 = await asyncio.gather(rs1_task, rs2_task)
|
||||||
|
|
||||||
# Deduplicate hybrid retrieval results
|
# Deduplicate hybrid retrieval results
|
||||||
@@ -266,6 +269,33 @@ class KnowledgeRetrievalNode(BaseNode):
|
|||||||
key=lambda d: d.metadata.get("score", 0),
|
key=lambda d: d.metadata.get("score", 0),
|
||||||
reverse=True
|
reverse=True
|
||||||
)[:kb_config.top_k])
|
)[:kb_config.top_k])
|
||||||
|
if kb_config.retrieve_type == RetrieveType.Graph:
|
||||||
|
from app.core.rag.common.settings import kg_retriever
|
||||||
|
llm_key = self.model_balance(db_knowledge.llm)
|
||||||
|
emb_key = self.model_balance(db_knowledge.embedding)
|
||||||
|
chat_model = Base(
|
||||||
|
key=llm_key.api_key,
|
||||||
|
model_name=llm_key.model_name,
|
||||||
|
base_url=llm_key.api_base
|
||||||
|
)
|
||||||
|
embedding_model = OpenAIEmbed(
|
||||||
|
key=emb_key.api_key,
|
||||||
|
model_name=emb_key.model_name,
|
||||||
|
base_url=emb_key.api_base
|
||||||
|
)
|
||||||
|
doc = await asyncio.to_thread(
|
||||||
|
kg_retriever.retrieval,
|
||||||
|
question=query,
|
||||||
|
workspace_ids=[str(db_knowledge.workspace_id)],
|
||||||
|
kb_ids=[str(kb_config.kb_id)],
|
||||||
|
emb_mdl=embedding_model,
|
||||||
|
llm=chat_model
|
||||||
|
)
|
||||||
|
if doc:
|
||||||
|
rs.insert(0, DocumentChunk(
|
||||||
|
page_content=doc.get("page_content", ""),
|
||||||
|
metadata=doc.get("metadata", {})
|
||||||
|
))
|
||||||
case _:
|
case _:
|
||||||
raise RuntimeError("Unknown retrieval type")
|
raise RuntimeError("Unknown retrieval type")
|
||||||
return rs
|
return rs
|
||||||
|
|||||||
@@ -574,6 +574,29 @@ class ToolService:
|
|||||||
"default": "Asia/Shanghai"
|
"default": "Asia/Shanghai"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
elif operation == "datetime_to_timestamp":
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "input_value",
|
||||||
|
"type": "string",
|
||||||
|
"description": "输入值(时间字符串,如:2026-04-07 10:30:25)",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "input_format",
|
||||||
|
"type": "string",
|
||||||
|
"description": "输入时间格式(如:%Y-%m-%d %H:%M:%S)",
|
||||||
|
"required": False,
|
||||||
|
"default": "%Y-%m-%d %H:%M:%S"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_timezone",
|
||||||
|
"type": "string",
|
||||||
|
"description": "源时区(如:UTC, Asia/Shanghai)",
|
||||||
|
"required": False,
|
||||||
|
"default": "Asia/Shanghai"
|
||||||
|
}
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
# 默认返回所有参数(除了operation)
|
# 默认返回所有参数(除了operation)
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -229,7 +229,11 @@ const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigF
|
|||||||
...knowledgeRest,
|
...knowledgeRest,
|
||||||
knowledge_bases: knowledge_bases.map(item => ({
|
knowledge_bases: knowledge_bases.map(item => ({
|
||||||
kb_id: item.kb_id || item.id,
|
kb_id: item.kb_id || item.id,
|
||||||
...(item.config || {})
|
retrieve_type: item.retrieve_type,
|
||||||
|
top_k: item.top_k,
|
||||||
|
similarity_threshold: item.similarity_threshold,
|
||||||
|
vector_similarity_weight: item.vector_similarity_weight,
|
||||||
|
// ...(item.config || {})
|
||||||
}))
|
}))
|
||||||
} as KnowledgeConfig : null,
|
} as KnowledgeConfig : null,
|
||||||
tools: tools.map(vo => {
|
tools: tools.map(vo => {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
|||||||
const list = [...knowledgeList]
|
const list = [...knowledgeList]
|
||||||
list[index] = {
|
list[index] = {
|
||||||
...list[index],
|
...list[index],
|
||||||
|
...values,
|
||||||
config: {...values as KnowledgeConfigForm}
|
config: {...values as KnowledgeConfigForm}
|
||||||
}
|
}
|
||||||
setKnowledgeList([...list])
|
setKnowledgeList([...list])
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface KnowledgeConfigModalProps {
|
|||||||
* Available retrieval types
|
* Available retrieval types
|
||||||
*/
|
*/
|
||||||
const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid',
|
const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid',
|
||||||
// 'graph'
|
'graph'
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
refresh(selectedRows.map(item => ({
|
refresh(selectedRows.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
|
similarity_threshold: 0.7,
|
||||||
|
retrieve_type: "hybrid",
|
||||||
|
top_k: 3,
|
||||||
|
weight: 1,
|
||||||
config: {
|
config: {
|
||||||
similarity_threshold: 0.7,
|
similarity_threshold: 0.7,
|
||||||
retrieve_type: "hybrid",
|
retrieve_type: "hybrid",
|
||||||
|
|||||||
@@ -155,12 +155,10 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
{['model', 'chat'].includes(source) && <>
|
{['model', 'chat'].includes(source) && <>
|
||||||
<FormItem name="capability" hidden />
|
<FormItem name="capability" hidden />
|
||||||
{(values?.deep_thinking || values?.capability?.includes('thinking')) && (
|
|
||||||
<FormItem name="deep_thinking" valuePropName="checked">
|
|
||||||
<Checkbox>{t('application.deep_thinking')}</Checkbox>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
</>}
|
</>}
|
||||||
|
<FormItem name="deep_thinking" valuePropName="checked" hidden={!['model', 'chat'].includes(source) || !(values?.deep_thinking || values?.capability?.includes('thinking'))}>
|
||||||
|
<Checkbox>{t('application.deep_thinking')}</Checkbox>
|
||||||
|
</FormItem>
|
||||||
{source === 'chat' && <FormItem name="label" hidden />}
|
{source === 'chat' && <FormItem name="label" hidden />}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const CreateDataset = () => {
|
|||||||
const [processingMethod, setProcessingMethod] = useState<ProcessingMethod>('directBlock');
|
const [processingMethod, setProcessingMethod] = useState<ProcessingMethod>('directBlock');
|
||||||
const [parameterSettings, setParameterSettings] = useState<ParameterSettings>('defaultSettings');
|
const [parameterSettings, setParameterSettings] = useState<ParameterSettings>('defaultSettings');
|
||||||
const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState<boolean>(true);
|
const [pdfEnhancementEnabled, setPdfEnhancementEnabled] = useState<boolean>(true);
|
||||||
const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState<string>('deepdoc');
|
const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState<string>('mineru');
|
||||||
const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt','png','jpg','mp3','mp4','mov','wav']
|
const fileType = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'md', 'htm', 'html', 'json', 'ppt', 'pptx', 'txt','png','jpg','mp3','mp4','mov','wav']
|
||||||
const steps = useMemo(
|
const steps = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@@ -89,14 +89,6 @@ const WordCloud: FC = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
color: ['#155EEF'],
|
color: ['#155EEF'],
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: (params: any) => {
|
|
||||||
const dataIndex = params.dataIndex
|
|
||||||
const item = radarData[dataIndex]
|
|
||||||
return `${item.name}<br/>${item.percentage.toFixed(1)}%`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
radar: {
|
radar: {
|
||||||
indicator: radarData.map(item => ({
|
indicator: radarData.map(item => ({
|
||||||
name: t(`statementDetail.${item.name}`),
|
name: t(`statementDetail.${item.name}`),
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
<AutocompletePlugin options={options} />
|
<AutocompletePlugin options={options} />
|
||||||
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
|
<CharacterCountPlugin setCount={setCount} />
|
||||||
<InitialValuePlugin value={value} options={options} />
|
<InitialValuePlugin value={value} options={options} onChange={onChange} />
|
||||||
<BlurPlugin />
|
<BlurPlugin />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
|
|||||||
@@ -1,41 +1,22 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { $getRoot, $isParagraphNode } from 'lexical';
|
import { $getRoot, $isParagraphNode } from 'lexical';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
|
||||||
import { $isVariableNode } from '../nodes/VariableNode';
|
const CharacterCountPlugin = ({ setCount }: { setCount: (count: number) => void }) => {
|
||||||
|
|
||||||
const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const onChangeRef = useRef(onChange);
|
|
||||||
onChangeRef.current = onChange;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState, tags }) => {
|
return editor.registerUpdateListener(({ editorState, tags }) => {
|
||||||
if (tags.has('programmatic')) return;
|
if (tags.has('programmatic')) return;
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
let serializedContent = '';
|
|
||||||
|
|
||||||
// Traverse all nodes and serialize properly
|
|
||||||
const paragraphs: string[] = [];
|
const paragraphs: string[] = [];
|
||||||
root.getChildren().forEach(child => {
|
root.getChildren().forEach(child => {
|
||||||
if ($isParagraphNode(child)) {
|
if ($isParagraphNode(child)) {
|
||||||
let paragraphContent = '';
|
paragraphs.push(child.getChildren().map(n => n.getTextContent()).join(''));
|
||||||
child.getChildren().forEach(node => {
|
|
||||||
if ($isVariableNode(node)) {
|
|
||||||
paragraphContent += node.getTextContent();
|
|
||||||
} else {
|
|
||||||
paragraphContent += node.getTextContent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
paragraphs.push(paragraphContent);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
setCount(paragraphs.join('\n').length);
|
||||||
serializedContent = paragraphs.join('\n');
|
|
||||||
|
|
||||||
setCount(serializedContent.length);
|
|
||||||
onChangeRef.current?.(serializedContent);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [editor, setCount]);
|
}, [editor, setCount]);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const CommandPlugin = () => {
|
|||||||
|
|
||||||
// Create and insert the variable node
|
// Create and insert the variable node
|
||||||
const tagNode = $createVariableNode(payload.data);
|
const tagNode = $createVariableNode(payload.data);
|
||||||
const spaceNode = $createTextNode(' ');
|
const spaceNode = $createTextNode('');
|
||||||
|
|
||||||
anchorNode.insertAfter(tagNode);
|
anchorNode.insertAfter(tagNode);
|
||||||
tagNode.insertAfter(spaceNode);
|
tagNode.insertAfter(spaceNode);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
import { $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode } from 'lexical';
|
||||||
|
|
||||||
import { $createVariableNode } from '../nodes/VariableNode';
|
import { $createVariableNode } from '../nodes/VariableNode';
|
||||||
import { type Suggestion } from '../plugin/AutocompletePlugin'
|
import { type Suggestion } from '../plugin/AutocompletePlugin'
|
||||||
@@ -14,24 +14,34 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
|||||||
interface InitialValuePluginProps {
|
interface InitialValuePluginProps {
|
||||||
value: string;
|
value: string;
|
||||||
options?: Suggestion[];
|
options?: Suggestion[];
|
||||||
|
onChange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
|
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], onChange }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const prevValueRef = useRef<string>('');
|
const prevValueRef = useRef<string>('');
|
||||||
const isUserInputRef = useRef(false);
|
const isUserInputRef = useRef(false);
|
||||||
const optionsRef = useRef(options);
|
const optionsRef = useRef(options);
|
||||||
optionsRef.current = options;
|
optionsRef.current = options;
|
||||||
|
const onChangeRef = useRef(onChange);
|
||||||
|
onChangeRef.current = onChange;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState, tags }) => {
|
return editor.registerUpdateListener(({ editorState, tags }) => {
|
||||||
if (tags.has('programmatic')) return;
|
if (tags.has('programmatic')) return;
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
const textContent = root.getTextContent();
|
const paragraphs: string[] = [];
|
||||||
if (textContent !== prevValueRef.current) {
|
root.getChildren().forEach(child => {
|
||||||
|
if ($isParagraphNode(child)) {
|
||||||
|
paragraphs.push(child.getChildren().map(n => n.getTextContent()).join(''));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const text = paragraphs.join('\n');
|
||||||
|
if (text !== prevValueRef.current) {
|
||||||
isUserInputRef.current = true;
|
isUserInputRef.current = true;
|
||||||
prevValueRef.current = textContent;
|
prevValueRef.current = text;
|
||||||
|
onChangeRef.current?.(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,23 +49,24 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
|
|
||||||
const incomingEdges = graph.getIncomingEdges(node);
|
const incomingEdges = graph.getIncomingEdges(node);
|
||||||
const outgoingEdges = graph.getOutgoingEdges(node);
|
const outgoingEdges = graph.getOutgoingEdges(node);
|
||||||
|
const addedEdges: any[] = [];
|
||||||
incomingEdges?.forEach(edge => {
|
|
||||||
graph.addEdge({
|
incomingEdges?.forEach((edge: any) => {
|
||||||
|
addedEdges.push(graph.addEdge({
|
||||||
source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() },
|
source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() },
|
||||||
target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' },
|
target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
outgoingEdges?.forEach(edge => {
|
outgoingEdges?.forEach((edge: any) => {
|
||||||
const targetCell = graph.getCellById(edge.getTargetCellId()) as any;
|
const targetCell = graph.getCellById(edge.getTargetCellId()) as any;
|
||||||
const targetPortId = targetCell?.getPorts?.()?.find((port: any) => port.group === 'left')?.id || edge.getTargetPortId();
|
const targetPortId = targetCell?.getPorts?.()?.find((port: any) => port.group === 'left')?.id || edge.getTargetPortId();
|
||||||
graph.addEdge({
|
addedEdges.push(graph.addEdge({
|
||||||
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
|
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
|
||||||
target: { cell: edge.getTargetCellId(), port: targetPortId },
|
target: { cell: edge.getTargetCellId(), port: targetPortId },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove all add-node type nodes
|
// Remove all add-node type nodes
|
||||||
@@ -75,6 +76,15 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
addedEdges.forEach(e => {
|
||||||
|
const src = graph.getCellById(e.getSourceCellId());
|
||||||
|
const tgt = graph.getCellById(e.getTargetCellId());
|
||||||
|
if (src?.isNode()) src.toFront();
|
||||||
|
if (tgt?.isNode()) tgt.toFront();
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
|
||||||
// Automatically adjust loop node size
|
// Automatically adjust loop node size
|
||||||
const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||||
if (loopNode) {
|
if (loopNode) {
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
target: { cell: addNode.id, port: targetPort },
|
target: { cell: addNode.id, port: targetPort },
|
||||||
...edgeAttrs,
|
...edgeAttrs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cycleStartNode.toFront()
|
||||||
|
addNode.toFront()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +120,12 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
}
|
}
|
||||||
graph.addEdge(edgeConfig)
|
graph.addEdge(edgeConfig)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
|
||||||
|
cycleStartNode.toFront()
|
||||||
|
addNode.toFront()
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('port:click', handlePortClick as EventListener);
|
window.addEventListener('port:click', handlePortClick as EventListener);
|
||||||
|
const handleBlankClick = () => handlePopoverClose();
|
||||||
|
window.addEventListener('blank:click', handleBlankClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('port:click', handlePortClick as EventListener);
|
window.removeEventListener('port:click', handlePortClick as EventListener);
|
||||||
|
window.removeEventListener('blank:click', handleBlankClick);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -188,38 +191,39 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const newPorts = newNode.getPorts();
|
const newPorts = newNode.getPorts();
|
||||||
|
|
||||||
|
const addedEdges: any[] = [];
|
||||||
if (edgeInsertion) {
|
if (edgeInsertion) {
|
||||||
// Edge insertion: create source→new and new→target edges
|
// Edge insertion: create source→new and new→target edges
|
||||||
const { targetCell, targetPort: origTargetPort } = edgeInsertion;
|
const { targetCell, targetPort: origTargetPort } = edgeInsertion;
|
||||||
const newLeftPort = newPorts.find((p: any) => p.group === 'left')?.id || 'left';
|
const newLeftPort = newPorts.find((p: any) => p.group === 'left')?.id || 'left';
|
||||||
const newRightPort = newPorts.find((p: any) => p.group === 'right')?.id || 'right';
|
const newRightPort = newPorts.find((p: any) => p.group === 'right')?.id || 'right';
|
||||||
graph.addEdge({
|
addedEdges.push(graph.addEdge({
|
||||||
source: { cell: sourceNode.id, port: sourcePort },
|
source: { cell: sourceNode.id, port: sourcePort },
|
||||||
target: { cell: newNode.id, port: newLeftPort },
|
target: { cell: newNode.id, port: newLeftPort },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
}));
|
||||||
graph.addEdge({
|
addedEdges.push(graph.addEdge({
|
||||||
source: { cell: newNode.id, port: newRightPort },
|
source: { cell: newNode.id, port: newRightPort },
|
||||||
target: { cell: targetCell.id, port: origTargetPort },
|
target: { cell: targetCell.id, port: origTargetPort },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
}));
|
||||||
setEdgeInsertion(null);
|
setEdgeInsertion(null);
|
||||||
} else if (sourcePortGroup === 'left') {
|
} else if (sourcePortGroup === 'left') {
|
||||||
// Connect from left port to new node's right side
|
// Connect from left port to new node's right side
|
||||||
const targetPort = newPorts.find((port: any) => port.group === 'right')?.id || 'right';
|
const targetPort = newPorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||||
graph.addEdge({
|
addedEdges.push(graph.addEdge({
|
||||||
source: { cell: newNode.id, port: targetPort },
|
source: { cell: newNode.id, port: targetPort },
|
||||||
target: { cell: sourceNode.id, port: sourcePort },
|
target: { cell: sourceNode.id, port: sourcePort },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Connect from right port to new node's left side
|
// Connect from right port to new node's left side
|
||||||
const targetPort = newPorts.find((port: any) => port.group === 'left')?.id || 'left';
|
const targetPort = newPorts.find((port: any) => port.group === 'left')?.id || 'left';
|
||||||
graph.addEdge({
|
addedEdges.push(graph.addEdge({
|
||||||
source: { cell: sourceNode.id, port: sourcePort },
|
source: { cell: sourceNode.id, port: sourcePort },
|
||||||
target: { cell: newNode.id, port: targetPort },
|
target: { cell: newNode.id, port: targetPort },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust loop node size when child node is added via port within loop node
|
// Adjust loop node size when child node is added via port within loop node
|
||||||
@@ -266,6 +270,44 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCycleContainer = (type: string) => type === 'loop' || type === 'iteration';
|
||||||
|
const newNodeType = selectedNodeType.type;
|
||||||
|
|
||||||
|
// Helper: bring all child nodes and their edges of a cycle container to front
|
||||||
|
const bringCycleChildrenToFront = (cycleContainerId: string) => {
|
||||||
|
|
||||||
|
graph.getEdges().forEach((e: any) => {
|
||||||
|
const src = graph.getCellById(e.getSourceCellId());
|
||||||
|
const tgt = graph.getCellById(e.getTargetCellId());
|
||||||
|
if (src?.getData()?.cycle === cycleContainerId || tgt?.getData()?.cycle === cycleContainerId) e.toFront();
|
||||||
|
});
|
||||||
|
graph.getNodes().forEach((n: any) => {
|
||||||
|
if (n.getData()?.cycle === cycleContainerId) n.toFront();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCycleContainer(sourceNodeType)) {
|
||||||
|
console.log('isCycleContainer(sourceNodeType)')
|
||||||
|
// Case 4: source is a loop/iteration node — bring new node to front, then its children
|
||||||
|
newNode.toFront();
|
||||||
|
sourceNode.toFront();
|
||||||
|
bringCycleChildrenToFront(sourceNodeData.id);
|
||||||
|
} else if (isCycleContainer(newNodeType)) {
|
||||||
|
console.log('isCycleContainer(newNodeType)')
|
||||||
|
// Case 3: adding a loop/iteration node from a normal node — bring new node to front, then its children
|
||||||
|
newNode.toFront();
|
||||||
|
sourceNode.toFront()
|
||||||
|
bringCycleChildrenToFront(id);
|
||||||
|
} else {
|
||||||
|
// Case 2: normal node → normal node
|
||||||
|
addedEdges.forEach(e => {
|
||||||
|
const src = graph.getCellById(e.getSourceCellId());
|
||||||
|
const tgt = graph.getCellById(e.getTargetCellId());
|
||||||
|
if (src?.isNode()) src.toFront();
|
||||||
|
if (tgt?.isNode()) tgt.toFront();
|
||||||
|
});
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
// Clean up temporary element
|
// Clean up temporary element
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
const bringLoopChildrenToFront = (cell: any) => {
|
||||||
|
const type = cell?.getData()?.type;
|
||||||
|
if ((type !== 'loop' && type !== 'iteration') || !graphRef?.current) return;
|
||||||
|
const cycleId = cell.getData().id;
|
||||||
|
graphRef.current.getEdges().forEach((edge: any) => {
|
||||||
|
const src = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||||
|
const tgt = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||||
|
if (src?.getData()?.cycle === cycleId || tgt?.getData()?.cycle === cycleId) edge.toFront();
|
||||||
|
});
|
||||||
|
graphRef.current.getNodes().forEach((n: any) => {
|
||||||
|
if (n.getData()?.cycle === cycleId) n.toFront();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Recalculate node height and port Y positions without rebuilding ports
|
// Recalculate node height and port Y positions without rebuilding ports
|
||||||
const updateNodeLayout = (cases: any[]) => {
|
const updateNodeLayout = (cases: any[]) => {
|
||||||
if (!selectedNode || !graphRef?.current) return;
|
if (!selectedNode || !graphRef?.current) return;
|
||||||
@@ -139,6 +153,10 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
...edgeAttrs,
|
...edgeAttrs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
sourceCell.toFront()
|
||||||
|
selectedNode.toFront()
|
||||||
|
bringLoopChildrenToFront(sourceCell)
|
||||||
|
bringLoopChildrenToFront(selectedNode)
|
||||||
graphRef.current?.removeCell(edge);
|
graphRef.current?.removeCell(edge);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,6 +201,10 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
target: { cell: targetCellId, port: targetPortId },
|
target: { cell: targetCellId, port: targetPortId },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
});
|
||||||
|
selectedNode.toFront()
|
||||||
|
bringLoopChildrenToFront(selectedNode)
|
||||||
|
targetCell.toFront()
|
||||||
|
bringLoopChildrenToFront(targetCell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
|||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
const formValues = Form.useWatch([parentName], form);
|
const formValues = Form.useWatch([parentName], form);
|
||||||
|
|
||||||
|
const bringLoopChildrenToFront = (cell: any) => {
|
||||||
|
const type = cell?.getData()?.type;
|
||||||
|
if ((type !== 'loop' && type !== 'iteration') || !graphRef?.current) return;
|
||||||
|
const cycleId = cell.getData().id;
|
||||||
|
graphRef.current.getEdges().forEach((edge: any) => {
|
||||||
|
const src = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||||
|
const tgt = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||||
|
if (src?.getData()?.cycle === cycleId || tgt?.getData()?.cycle === cycleId) edge.toFront();
|
||||||
|
});
|
||||||
|
graphRef.current.getNodes().forEach((n: any) => {
|
||||||
|
if (n.getData()?.cycle === cycleId) n.toFront();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Update node ports based on category count changes (add/remove categories)
|
// Update node ports based on category count changes (add/remove categories)
|
||||||
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
||||||
if (!selectedNode || !graphRef?.current) return;
|
if (!selectedNode || !graphRef?.current) return;
|
||||||
@@ -88,6 +102,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
|||||||
target: { cell: selectedNode.id, port: targetPortId },
|
target: { cell: selectedNode.id, port: targetPortId },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
});
|
||||||
|
sourceCell.toFront()
|
||||||
|
bringLoopChildrenToFront(sourceCell)
|
||||||
|
selectedNode.toFront()
|
||||||
|
bringLoopChildrenToFront(selectedNode)
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,6 +137,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
|||||||
target: { cell: targetCellId, port: targetPortId },
|
target: { cell: targetCellId, port: targetPortId },
|
||||||
...edgeAttrs
|
...edgeAttrs
|
||||||
});
|
});
|
||||||
|
selectedNode.toFront()
|
||||||
|
bringLoopChildrenToFront(selectedNode)
|
||||||
|
targetCell.toFront()
|
||||||
|
bringLoopChildrenToFront(targetCell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface KnowledgeConfigModalProps {
|
|||||||
refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void;
|
refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void;
|
||||||
}
|
}
|
||||||
const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid',
|
const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid',
|
||||||
// 'graph'
|
'graph'
|
||||||
]
|
]
|
||||||
|
|
||||||
const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfigModalProps>(({
|
const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfigModalProps>(({
|
||||||
|
|||||||
@@ -730,7 +730,7 @@ const defaultPortGroup = {
|
|||||||
stroke: port_color,
|
stroke: port_color,
|
||||||
strokeWidth: edge_width,
|
strokeWidth: edge_width,
|
||||||
fill: port_color,
|
fill: port_color,
|
||||||
opacity: 0,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
text: '+',
|
text: '+',
|
||||||
@@ -741,7 +741,7 @@ const defaultPortGroup = {
|
|||||||
textVerticalAnchor: 'middle',
|
textVerticalAnchor: 'middle',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
y: '0.15em',
|
y: '0.15em',
|
||||||
opacity: 0,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ export const useWorkflowGraph = ({
|
|||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const graphRef = useRef<Graph>();
|
const graphRef = useRef<Graph>();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||||
const [zoomLevel, setZoomLevel] = useState(1);
|
const [zoomLevel, setZoomLevel] = useState(1);
|
||||||
@@ -134,7 +134,7 @@ export const useWorkflowGraph = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initWorkflow()
|
initWorkflow()
|
||||||
}, [config, graphRef.current])
|
}, [config, graphRef.current])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize workflow graph with nodes and edges from configuration
|
* Initialize workflow graph with nodes and edges from configuration
|
||||||
*/
|
*/
|
||||||
@@ -211,7 +211,7 @@ export const useWorkflowGraph = ({
|
|||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
data: { ...node, ...nodeLibraryConfig},
|
data: { ...node, ...nodeLibraryConfig },
|
||||||
...position,
|
...position,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,11 +221,11 @@ export const useWorkflowGraph = ({
|
|||||||
if (w) nodeConfig.width = w as number;
|
if (w) nodeConfig.width = w as number;
|
||||||
if (h) nodeConfig.height = h as number;
|
if (h) nodeConfig.height = h as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate ports dynamically for if-else node based on cases
|
// Generate ports dynamically for if-else node based on cases
|
||||||
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
|
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
|
||||||
const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE
|
const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE
|
||||||
|
|
||||||
const portItems: PortMetadata[] = [
|
const portItems: PortMetadata[] = [
|
||||||
defaultPortItems[0],
|
defaultPortItems[0],
|
||||||
];
|
];
|
||||||
@@ -240,24 +240,24 @@ export const useWorkflowGraph = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeConfig.ports = {
|
nodeConfig.ports = {
|
||||||
groups: defaultAbsolutePortGroups,
|
groups: defaultAbsolutePortGroups,
|
||||||
items: portItems
|
items: portItems
|
||||||
};
|
};
|
||||||
|
|
||||||
nodeConfig.height = calcConditionNodeTotalHeight(config.cases);
|
nodeConfig.height = calcConditionNodeTotalHeight(config.cases);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate ports dynamically for question-classifier node based on categories
|
// Generate ports dynamically for question-classifier node based on categories
|
||||||
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
||||||
const categoryCount = config.categories.length;
|
const categoryCount = config.categories.length;
|
||||||
const newHeight = conditionNodeHeight + (categoryCount - 2) * conditionNodeItemHeight;
|
const newHeight = conditionNodeHeight + (categoryCount - 2) * conditionNodeItemHeight;
|
||||||
|
|
||||||
const portItems: PortMetadata[] = [
|
const portItems: PortMetadata[] = [
|
||||||
defaultPortItems[0]
|
defaultPortItems[0]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add category ports
|
// Add category ports
|
||||||
config.categories.forEach((_category: any, index: number) => {
|
config.categories.forEach((_category: any, index: number) => {
|
||||||
portItems.push({
|
portItems.push({
|
||||||
@@ -269,15 +269,15 @@ export const useWorkflowGraph = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
nodeConfig.ports = {
|
nodeConfig.ports = {
|
||||||
groups: defaultAbsolutePortGroups,
|
groups: defaultAbsolutePortGroups,
|
||||||
items: portItems
|
items: portItems
|
||||||
};
|
};
|
||||||
|
|
||||||
nodeConfig.height = newHeight;
|
nodeConfig.height = newHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check error_handle.method config for http-request node
|
// Check error_handle.method config for http-request node
|
||||||
if (type === 'http-request' && (nodeConfig as any).error_handle?.method === 'branch') {
|
if (type === 'http-request' && (nodeConfig as any).error_handle?.method === 'branch') {
|
||||||
nodeConfig.ports = {
|
nodeConfig.ports = {
|
||||||
@@ -294,21 +294,22 @@ export const useWorkflowGraph = ({
|
|||||||
x: nodeWidth,
|
x: nodeWidth,
|
||||||
y: portItemArgsY + portItemArgsY,
|
y: portItemArgsY + portItemArgsY,
|
||||||
},
|
},
|
||||||
id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}}
|
id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs } }
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodeConfig
|
return nodeConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
// Separate parent nodes and child nodes
|
// Separate parent nodes and child nodes
|
||||||
const parentNodes = nodeList.filter(node => !node.data.cycle)
|
const parentNodes = nodeList.filter(node => !node.data.cycle)
|
||||||
const childNodes = nodeList.filter(node => node.data.cycle)
|
const childNodes = nodeList.filter(node => node.data.cycle)
|
||||||
|
|
||||||
// Add parent nodes first
|
// Add parent nodes first
|
||||||
graphRef.current?.addNodes(parentNodes)
|
graphRef.current?.addNodes(parentNodes)
|
||||||
|
|
||||||
// Then process child nodes, use addChild to add to corresponding parent node
|
// Then process child nodes, use addChild to add to corresponding parent node
|
||||||
childNodes.forEach(childNode => {
|
childNodes.forEach(childNode => {
|
||||||
const cycleId = childNode.data.cycle
|
const cycleId = childNode.data.cycle
|
||||||
@@ -322,14 +323,14 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Adjust parent node size to fit child nodes
|
// Adjust parent node size to fit child nodes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const parentNodesWithChildren = parentNodes.filter(parentNode => {
|
const parentNodesWithChildren = parentNodes.filter(parentNode => {
|
||||||
const parentId = parentNode.data.id
|
const parentId = parentNode.data.id
|
||||||
return childNodes.some(child => child.data.cycle === parentId)
|
return childNodes.some(child => child.data.cycle === parentId)
|
||||||
})
|
})
|
||||||
|
|
||||||
parentNodesWithChildren.forEach(parentNodeConfig => {
|
parentNodesWithChildren.forEach(parentNodeConfig => {
|
||||||
const parentNode = graphRef.current?.getCellById(parentNodeConfig.data.id)
|
const parentNode = graphRef.current?.getCellById(parentNodeConfig.data.id)
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
@@ -340,18 +341,18 @@ export const useWorkflowGraph = ({
|
|||||||
const minY = Math.min(...childBounds.map(b => b.y))
|
const minY = Math.min(...childBounds.map(b => b.y))
|
||||||
const maxX = Math.max(...childBounds.map(b => b.x + b.width))
|
const maxX = Math.max(...childBounds.map(b => b.x + b.width))
|
||||||
const maxY = Math.max(...childBounds.map(b => b.y + b.height))
|
const maxY = Math.max(...childBounds.map(b => b.y + b.height))
|
||||||
|
|
||||||
const padding = 24
|
const padding = 24
|
||||||
const headerHeight = 50
|
const headerHeight = 50
|
||||||
const parentBBox = parentNode.getBBox()
|
const parentBBox = parentNode.getBBox()
|
||||||
|
|
||||||
const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2)
|
const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2)
|
||||||
const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight)
|
const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight)
|
||||||
|
|
||||||
console.log('newWidth', newHeight, newWidth)
|
console.log('newWidth', newHeight, newWidth)
|
||||||
|
|
||||||
parentNode.prop('size', { width: newWidth, height: newHeight })
|
parentNode.prop('size', { width: newWidth, height: newHeight })
|
||||||
|
|
||||||
// Update x position of right group ports
|
// Update x position of right group ports
|
||||||
const ports = (parentNode as Node).getPorts()
|
const ports = (parentNode as Node).getPorts()
|
||||||
ports.forEach(port => {
|
ports.forEach(port => {
|
||||||
@@ -371,7 +372,7 @@ export const useWorkflowGraph = ({
|
|||||||
const sourceCell = graphRef.current?.getCellById(e.source);
|
const sourceCell = graphRef.current?.getCellById(e.source);
|
||||||
const sourceType = sourceCell?.getData()?.type;
|
const sourceType = sourceCell?.getData()?.type;
|
||||||
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
||||||
|
|
||||||
if (isMultiPortNode) {
|
if (isMultiPortNode) {
|
||||||
// Multi-port nodes need to compare source, target and label
|
// Multi-port nodes need to compare source, target and label
|
||||||
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
||||||
@@ -381,18 +382,18 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
}) === index;
|
}) === index;
|
||||||
});
|
});
|
||||||
|
|
||||||
const edgeList = uniqueEdges.map(edge => {
|
const edgeList = uniqueEdges.map(edge => {
|
||||||
const { source, target, label } = edge
|
const { source, target, label } = edge
|
||||||
const sourceCell = graphRef.current?.getCellById(source)
|
const sourceCell = graphRef.current?.getCellById(source)
|
||||||
const targetCell = graphRef.current?.getCellById(target)
|
const targetCell = graphRef.current?.getCellById(target)
|
||||||
|
|
||||||
if (sourceCell && targetCell) {
|
if (sourceCell && targetCell) {
|
||||||
const sourcePorts = (sourceCell as Node).getPorts()
|
const sourcePorts = (sourceCell as Node).getPorts()
|
||||||
const targetPorts = (targetCell as Node).getPorts()
|
const targetPorts = (targetCell as Node).getPorts()
|
||||||
|
|
||||||
let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||||
|
|
||||||
// If if-else node has label, match corresponding port by label
|
// If if-else node has label, match corresponding port by label
|
||||||
if (sourceCell.getData()?.type === 'if-else' && label) {
|
if (sourceCell.getData()?.type === 'if-else' && label) {
|
||||||
// Find matching port ID
|
// Find matching port ID
|
||||||
@@ -401,7 +402,7 @@ export const useWorkflowGraph = ({
|
|||||||
sourcePort = label;
|
sourcePort = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If question-classifier node has label, match corresponding port by label
|
// If question-classifier node has label, match corresponding port by label
|
||||||
if (sourceCell.getData()?.type === 'question-classifier' && label) {
|
if (sourceCell.getData()?.type === 'question-classifier' && label) {
|
||||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||||
@@ -409,7 +410,7 @@ export const useWorkflowGraph = ({
|
|||||||
sourcePort = label;
|
sourcePort = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If http-request node has label, match corresponding port by label
|
// If http-request node has label, match corresponding port by label
|
||||||
if (sourceCell.getData()?.type === 'http-request' && label) {
|
if (sourceCell.getData()?.type === 'http-request' && label) {
|
||||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||||
@@ -417,7 +418,7 @@ export const useWorkflowGraph = ({
|
|||||||
sourcePort = label;
|
sourcePort = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const edgeConfig = {
|
const edgeConfig = {
|
||||||
source: {
|
source: {
|
||||||
cell: sourceCell.id,
|
cell: sourceCell.id,
|
||||||
@@ -438,15 +439,23 @@ export const useWorkflowGraph = ({
|
|||||||
})
|
})
|
||||||
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize after completion, display nodes in visible area
|
// Initialize after completion, display nodes in visible area
|
||||||
if (nodes.length > 0 || edges.length > 0) {
|
if (nodes.length > 0 || edges.length > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (graphRef.current) {
|
if (graphRef.current) {
|
||||||
graphRef.current.centerContent()
|
graphRef.current.centerContent()
|
||||||
// graphRef.current.getNodes().forEach(node => node.toFront());
|
graphRef.current.getNodes().forEach(node => {
|
||||||
|
if (!node.getData()?.cycle) node.toFront();
|
||||||
|
});
|
||||||
// Bring edges to front first, then child nodes above edges; parent nodes stay behind
|
// Bring edges to front first, then child nodes above edges; parent nodes stay behind
|
||||||
graphRef.current.getEdges().forEach(edge => edge.toFront());
|
graphRef.current.getEdges().forEach(edge => {
|
||||||
|
const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||||
|
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||||
|
if (sourceCell?.getData()?.cycle || targetCell?.getData()?.cycle) {
|
||||||
|
edge.toFront();
|
||||||
|
}
|
||||||
|
});
|
||||||
graphRef.current.getNodes().forEach(node => {
|
graphRef.current.getNodes().forEach(node => {
|
||||||
if (node.getData()?.cycle) node.toFront();
|
if (node.getData()?.cycle) node.toFront();
|
||||||
});
|
});
|
||||||
@@ -575,6 +584,7 @@ export const useWorkflowGraph = ({
|
|||||||
clearEdgeSelect();
|
clearEdgeSelect();
|
||||||
graphRef.current?.cleanSelection();
|
graphRef.current?.cleanSelection();
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
|
window.dispatchEvent(new CustomEvent('blank:click'));
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Handle canvas scale/zoom event
|
* Handle canvas scale/zoom event
|
||||||
@@ -595,26 +605,26 @@ export const useWorkflowGraph = ({
|
|||||||
// Get parent node and child node bounding boxes
|
// Get parent node and child node bounding boxes
|
||||||
const parentBBox = parentNode.getBBox();
|
const parentBBox = parentNode.getBBox();
|
||||||
const childBBox = node.getBBox();
|
const childBBox = node.getBBox();
|
||||||
|
|
||||||
// Calculate parent node padding
|
// Calculate parent node padding
|
||||||
const padding = 24;
|
const padding = 24;
|
||||||
const headerHeight = 50;
|
const headerHeight = 50;
|
||||||
|
|
||||||
// Calculate minimum and maximum positions allowed for child node
|
// Calculate minimum and maximum positions allowed for child node
|
||||||
const minX = parentBBox.x + padding;
|
const minX = parentBBox.x + padding;
|
||||||
const minY = parentBBox.y + padding + headerHeight;
|
const minY = parentBBox.y + padding + headerHeight;
|
||||||
const maxX = parentBBox.x + parentBBox.width - padding - childBBox.width;
|
const maxX = parentBBox.x + parentBBox.width - padding - childBBox.width;
|
||||||
const maxY = parentBBox.y + parentBBox.height - padding - childBBox.height;
|
const maxY = parentBBox.y + parentBBox.height - padding - childBBox.height;
|
||||||
|
|
||||||
// Restrict child node movement within parent node
|
// Restrict child node movement within parent node
|
||||||
let newX = childBBox.x;
|
let newX = childBBox.x;
|
||||||
let newY = childBBox.y;
|
let newY = childBBox.y;
|
||||||
|
|
||||||
if (newX < minX) newX = minX;
|
if (newX < minX) newX = minX;
|
||||||
if (newY < minY) newY = minY;
|
if (newY < minY) newY = minY;
|
||||||
if (newX > maxX) newX = maxX;
|
if (newX > maxX) newX = maxX;
|
||||||
if (newY > maxY) newY = maxY;
|
if (newY > maxY) newY = maxY;
|
||||||
|
|
||||||
// If child node position is restricted, update its position
|
// If child node position is restricted, update its position
|
||||||
if (newX !== childBBox.x || newY !== childBBox.y) {
|
if (newX !== childBBox.x || newY !== childBBox.y) {
|
||||||
node.setPosition(newX, newY);
|
node.setPosition(newX, newY);
|
||||||
@@ -706,7 +716,7 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
// Add child node to deletion list
|
// Add child node to deletion list
|
||||||
cells.push(nodeToDelete);
|
cells.push(nodeToDelete);
|
||||||
}
|
}
|
||||||
// Check if it's LoopNode, IterationNode or SubGraphNode
|
// Check if it's LoopNode, IterationNode or SubGraphNode
|
||||||
else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') {
|
else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') {
|
||||||
// Find all child nodes with cycle equal to current node id
|
// Find all child nodes with cycle equal to current node id
|
||||||
@@ -718,7 +728,7 @@ export const useWorkflowGraph = ({
|
|||||||
});
|
});
|
||||||
// Add parent node to deletion list
|
// Add parent node to deletion list
|
||||||
cells.push(nodeToDelete);
|
cells.push(nodeToDelete);
|
||||||
}
|
}
|
||||||
// Normal node
|
// Normal node
|
||||||
else {
|
else {
|
||||||
cells.push(nodeToDelete);
|
cells.push(nodeToDelete);
|
||||||
@@ -726,7 +736,7 @@ export const useWorkflowGraph = ({
|
|||||||
});
|
});
|
||||||
blankClick();
|
blankClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all collected nodes and edges
|
// Delete all collected nodes and edges
|
||||||
if (cells.length > 0) {
|
if (cells.length > 0) {
|
||||||
graphRef.current?.removeCells(cells);
|
graphRef.current?.removeCells(cells);
|
||||||
@@ -873,19 +883,19 @@ export const useWorkflowGraph = ({
|
|||||||
const targetGroup = targetMagnet ? getPortGroup(targetMagnet) : null;
|
const targetGroup = targetMagnet ? getPortGroup(targetMagnet) : null;
|
||||||
|
|
||||||
if (sourceGroup === 'left' || targetGroup === 'right') return false;
|
if (sourceGroup === 'left' || targetGroup === 'right') return false;
|
||||||
|
|
||||||
// Node cannot connect to itself
|
// Node cannot connect to itself
|
||||||
if (sourceCell?.id === targetCell?.id) return false;
|
if (sourceCell?.id === targetCell?.id) return false;
|
||||||
|
|
||||||
const targetType = targetCell?.getData()?.type;
|
const targetType = targetCell?.getData()?.type;
|
||||||
|
|
||||||
// Start node cannot be connection target
|
// Start node cannot be connection target
|
||||||
if (targetType === 'start') return false;
|
if (targetType === 'start') return false;
|
||||||
|
|
||||||
// Get source node and target node parent IDs
|
// Get source node and target node parent IDs
|
||||||
const sourceParentId = sourceCell?.getData()?.cycle;
|
const sourceParentId = sourceCell?.getData()?.cycle;
|
||||||
const targetParentId = targetCell?.getData()?.cycle;
|
const targetParentId = targetCell?.getData()?.cycle;
|
||||||
|
|
||||||
// Validate parent-child relationship:
|
// Validate parent-child relationship:
|
||||||
// 1. If both nodes have parent IDs, they must be same to connect
|
// 1. If both nodes have parent IDs, they must be same to connect
|
||||||
// 2. If both have no parent ID, can connect normally
|
// 2. If both have no parent ID, can connect normally
|
||||||
@@ -969,25 +979,6 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.on('edge:click', edgeClick);
|
graphRef.current.on('edge:click', edgeClick);
|
||||||
// Listen to port click event
|
// Listen to port click event
|
||||||
graphRef.current.on('node:port:click', nodePortClickEvent);
|
graphRef.current.on('node:port:click', nodePortClickEvent);
|
||||||
// Port hover: show circle style on right ports
|
|
||||||
graphRef.current.on('node:port:mouseenter', ({ node, port }) => {
|
|
||||||
console.log('node:port:mouseenter', port)
|
|
||||||
if (!port) return;
|
|
||||||
const portData = node.getPort(port);
|
|
||||||
if (portData?.group !== 'right') return;
|
|
||||||
node.toFront();
|
|
||||||
node.setPortProp(port, 'attrs/body/opacity', 0);
|
|
||||||
node.setPortProp(port, 'attrs/hoverBody/opacity', 1);
|
|
||||||
node.setPortProp(port, 'attrs/label/opacity', 1);
|
|
||||||
});
|
|
||||||
graphRef.current.on('node:port:mouseleave', ({ node, port }) => {
|
|
||||||
if (!port) return;
|
|
||||||
const portData = node.getPort(port);
|
|
||||||
if (portData?.group !== 'right') return;
|
|
||||||
node.setPortProp(port, 'attrs/body/opacity', 1);
|
|
||||||
node.setPortProp(port, 'attrs/hoverBody/opacity', 0);
|
|
||||||
node.setPortProp(port, 'attrs/label/opacity', 0);
|
|
||||||
});
|
|
||||||
// Listen to canvas click event, cancel selection
|
// Listen to canvas click event, cancel selection
|
||||||
graphRef.current.on('blank:click', blankClick);
|
graphRef.current.on('blank:click', blankClick);
|
||||||
// Node hover: highlight connected edges
|
// Node hover: highlight connected edges
|
||||||
@@ -1005,11 +996,6 @@ export const useWorkflowGraph = ({
|
|||||||
edge.setData({ ...edge.getData(), isNodeHover: true });
|
edge.setData({ ...edge.getData(), isNodeHover: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.getPorts().filter(p => p.group === 'right').forEach(p => {
|
|
||||||
node.setPortProp(p.id!, 'attrs/body/opacity', 0);
|
|
||||||
node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 1);
|
|
||||||
node.setPortProp(p.id!, 'attrs/label/opacity', 1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
graphRef.current.on('node:mouseleave', ({ node }) => {
|
graphRef.current.on('node:mouseleave', ({ node }) => {
|
||||||
graphRef.current?.getConnectedEdges(node).forEach(edge => {
|
graphRef.current?.getConnectedEdges(node).forEach(edge => {
|
||||||
@@ -1018,48 +1004,31 @@ export const useWorkflowGraph = ({
|
|||||||
edge.setData({ ...edge.getData(), isNodeHover: false });
|
edge.setData({ ...edge.getData(), isNodeHover: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.getPorts().filter(p => p.group === 'right').forEach(p => {
|
|
||||||
node.setPortProp(p.id!, 'attrs/body/opacity', 1);
|
|
||||||
node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0);
|
|
||||||
node.setPortProp(p.id!, 'attrs/label/opacity', 0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
// Listen to zoom event
|
// Listen to zoom event
|
||||||
graphRef.current.on('scale', scaleEvent);
|
graphRef.current.on('scale', scaleEvent);
|
||||||
// Listen to node move event
|
// Listen to node move event
|
||||||
graphRef.current.on('node:moved', nodeMoved);
|
graphRef.current.on('node:moved', nodeMoved);
|
||||||
// When parent (isGroup) node position changes, move children with it
|
|
||||||
graphRef.current.on('node:change:position', ({ node, current, previous }: { node: Node; current: { x: number; y: number }; previous: { x: number; y: number } }) => {
|
|
||||||
|
|
||||||
if (!(node.getData()?.type === 'iteration' && node.getData()?.type === 'loop') || !current || !previous) return;
|
|
||||||
|
|
||||||
const dx = current.x - previous.x;
|
|
||||||
const dy = current.y - previous.y;
|
|
||||||
const parentId = node.getData()?.id || node.id;
|
|
||||||
graphRef.current?.getNodes().forEach(child => {
|
|
||||||
if (child.getData()?.cycle === parentId) {
|
|
||||||
const cp = child.getPosition();
|
|
||||||
child.setPosition(cp.x + dx, cp.y + dy, { silent: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
graphRef.current.on('node:removed', blankClick)
|
graphRef.current.on('node:removed', blankClick)
|
||||||
// When edge connected, bring connected nodes' ports to front
|
// When edge connected, bring connected nodes' ports to front
|
||||||
graphRef.current.on('edge:connected', ({ isNew, edge }) => {
|
graphRef.current.on('edge:connected', ({ isNew }) => {
|
||||||
// Bring edge to front first, then bring child nodes above edges
|
// Bring edge to front first, then bring child nodes above edges
|
||||||
// Parent (loop/iteration) nodes stay behind to avoid covering edges
|
// Parent (loop/iteration) nodes stay behind to avoid covering edges
|
||||||
edge.toFront();
|
|
||||||
graphRef.current?.getNodes().forEach(node => {
|
|
||||||
if (node.getData()?.cycle) node.toFront();
|
|
||||||
});
|
|
||||||
// Reset any port hover state left from dragging
|
// Reset any port hover state left from dragging
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
graphRef.current?.getNodes().forEach(node => {
|
graphRef.current?.getNodes().forEach(node => {
|
||||||
node.getPorts().filter(p => p.group === 'right').forEach(p => {
|
if (!node.getData()?.cycle) node.toFront();
|
||||||
node.setPortProp(p.id!, 'attrs/body/opacity', 1);
|
});
|
||||||
node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0);
|
graphRef.current?.getEdges().forEach(edge => {
|
||||||
node.setPortProp(p.id!, 'attrs/label/opacity', 0);
|
const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||||
});
|
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||||
|
if (sourceCell?.getData()?.cycle || targetCell?.getData()?.cycle) {
|
||||||
|
edge.toFront();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
graphRef.current?.getNodes().forEach(node => {
|
||||||
|
if (node.getData()?.cycle) node.toFront();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1089,34 +1058,9 @@ export const useWorkflowGraph = ({
|
|||||||
if (found) break;
|
if (found) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found?.node.id !== lastHoveredPort?.node.id || found?.portId !== lastHoveredPort?.portId) {
|
lastHoveredPort = found;
|
||||||
// Leave previous
|
|
||||||
if (lastHoveredPort) {
|
|
||||||
const { node, portId } = lastHoveredPort;
|
|
||||||
node.setPortProp(portId, 'attrs/body/opacity', 1);
|
|
||||||
node.setPortProp(portId, 'attrs/hoverBody/opacity', 0);
|
|
||||||
node.setPortProp(portId, 'attrs/label/opacity', 0);
|
|
||||||
}
|
|
||||||
// Enter new
|
|
||||||
if (found) {
|
|
||||||
const { node, portId } = found;
|
|
||||||
node.toFront();
|
|
||||||
node.setPortProp(portId, 'attrs/body/opacity', 0);
|
|
||||||
node.setPortProp(portId, 'attrs/hoverBody/opacity', 1);
|
|
||||||
node.setPortProp(portId, 'attrs/label/opacity', 1);
|
|
||||||
}
|
|
||||||
lastHoveredPort = found;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
graphRef.current.on('edge:mouseup', () => {
|
|
||||||
if (lastHoveredPort) {
|
|
||||||
const { node, portId } = lastHoveredPort;
|
|
||||||
node.setPortProp(portId, 'attrs/body/opacity', 1);
|
|
||||||
node.setPortProp(portId, 'attrs/hoverBody/opacity', 0);
|
|
||||||
node.setPortProp(portId, 'attrs/label/opacity', 0);
|
|
||||||
lastHoveredPort = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
graphRef.current.on('edge:mouseup', () => { lastHoveredPort = null; });
|
||||||
// Listen to copy keyboard event
|
// Listen to copy keyboard event
|
||||||
graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent);
|
graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent);
|
||||||
// Listen to paste keyboard event
|
// Listen to paste keyboard event
|
||||||
@@ -1173,7 +1117,7 @@ export const useWorkflowGraph = ({
|
|||||||
if (!graph) return;
|
if (!graph) return;
|
||||||
|
|
||||||
const point = graphRef.current.clientToLocal(event.clientX, event.clientY);
|
const point = graphRef.current.clientToLocal(event.clientX, event.clientY);
|
||||||
|
|
||||||
// Get original config from node library to avoid config data chaining
|
// Get original config from node library to avoid config data chaining
|
||||||
let nodeLibraryConfig = [...nodeLibrary]
|
let nodeLibraryConfig = [...nodeLibrary]
|
||||||
.flatMap(category => category.nodes)
|
.flatMap(category => category.nodes)
|
||||||
@@ -1326,11 +1270,11 @@ export const useWorkflowGraph = ({
|
|||||||
const sourcePortId = edge.getSourcePortId();
|
const sourcePortId = edge.getSourcePortId();
|
||||||
|
|
||||||
// Filter invalid edges: source or target node doesn't exist, or is add-node type
|
// Filter invalid edges: source or target node doesn't exist, or is add-node type
|
||||||
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id ||
|
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id ||
|
||||||
sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') {
|
sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If if-else node right port connection, add label
|
// If if-else node right port connection, add label
|
||||||
if (sourceCell?.getData()?.type === 'if-else' && sourcePortId?.startsWith('CASE')) {
|
if (sourceCell?.getData()?.type === 'if-else' && sourcePortId?.startsWith('CASE')) {
|
||||||
return {
|
return {
|
||||||
@@ -1339,7 +1283,7 @@ export const useWorkflowGraph = ({
|
|||||||
label: sourcePortId,
|
label: sourcePortId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If question-classifier node right port connection, add label
|
// If question-classifier node right port connection, add label
|
||||||
if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) {
|
if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) {
|
||||||
return {
|
return {
|
||||||
@@ -1348,7 +1292,7 @@ export const useWorkflowGraph = ({
|
|||||||
label: sourcePortId,
|
label: sourcePortId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If http-request node right port connection, add label
|
// If http-request node right port connection, add label
|
||||||
if (sourceCell?.getData()?.type === 'http-request') {
|
if (sourceCell?.getData()?.type === 'http-request') {
|
||||||
if (sourcePortId === 'ERROR') {
|
if (sourcePortId === 'ERROR') {
|
||||||
@@ -1365,40 +1309,40 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: sourceCell?.getData().id,
|
source: sourceCell?.getData().id,
|
||||||
target: targetCell?.getData().id,
|
target: targetCell?.getData().id,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(edge => edge !== null)
|
.filter(edge => edge !== null)
|
||||||
.filter((edge, index, arr) => {
|
.filter((edge, index, arr) => {
|
||||||
// Deduplication: For if-else and question-classifier nodes, different ports can connect to same node
|
// Deduplication: For if-else and question-classifier nodes, different ports can connect to same node
|
||||||
return arr.findIndex(e => {
|
return arr.findIndex(e => {
|
||||||
if (!e || !edge) return false;
|
if (!e || !edge) return false;
|
||||||
const sourceCell = graphRef.current?.getCellById(e.source);
|
const sourceCell = graphRef.current?.getCellById(e.source);
|
||||||
const sourceType = sourceCell?.getData()?.type;
|
const sourceType = sourceCell?.getData()?.type;
|
||||||
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
||||||
|
|
||||||
if (isMultiPortNode) {
|
if (isMultiPortNode) {
|
||||||
// Multi-port nodes need to compare source, target and label
|
// Multi-port nodes need to compare source, target and label
|
||||||
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
||||||
} else {
|
} else {
|
||||||
// Other nodes only compare source and target
|
// Other nodes only compare source and target
|
||||||
return e.source === edge.source && e.target === edge.target;
|
return e.source === edge.source && e.target === edge.target;
|
||||||
}
|
}
|
||||||
}) === index;
|
}) === index;
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
saveWorkflowConfig(config.app_id, params as WorkflowConfig)
|
saveWorkflowConfig(config.app_id, params as WorkflowConfig)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (flag) {
|
if (flag) {
|
||||||
message.success({ content: t('common.saveSuccess'), duration: 1 })
|
message.success({ content: t('common.saveSuccess'), duration: 1 })
|
||||||
}
|
}
|
||||||
resolve(res)
|
resolve(res)
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user