Compare commits

..

9 Commits

Author SHA1 Message Date
Ke Sun
8e397b83b6 Merge branch 'release/v0.2.10' 2026-04-08 21:44:27 +08:00
Ke Sun
4961e7df79 Merge pull request #781 from SuanmoSuanyangTechnology/hotfix/v0.2.9
fix(web): string type language Editor init
2026-04-02 17:43:28 +08:00
Ke Sun
cae87de6ef Merge pull request #777 from SuanmoSuanyangTechnology/hotfix/v0.2.9
fix(web): jinja2 editor
2026-04-02 15:39:21 +08:00
Ke Sun
9ff3a3d5f7 Merge pull request #768 from SuanmoSuanyangTechnology/hotfix/v0.2.9
fix(web): knowledge base model api params
2026-04-02 14:39:43 +08:00
Ke Sun
18703919a8 Merge pull request #772 from SuanmoSuanyangTechnology/hotfix/gitee-sync
docs: add status badges to README files
2026-04-02 11:58:44 +08:00
Ke Sun
d1beb9e5d5 Merge pull request #771 from SuanmoSuanyangTechnology/hotfix/gitee-sync
ci(gitee): update Gitee repository path in sync workflow
2026-04-02 11:50:59 +08:00
Ke Sun
1aec7115a5 Merge pull request #769 from SuanmoSuanyangTechnology/hotfix/gitee-sync
ci: refactor Gitee sync workflow with selective branch filtering
2026-04-02 11:34:49 +08:00
Ke Sun
8b9eb81d36 Merge pull request #767 from SuanmoSuanyangTechnology/hotfix/gitee-sync
Hotfix/gitee sync
2026-04-02 10:54:45 +08:00
Ke Sun
daaad51357 Merge pull request #765 from SuanmoSuanyangTechnology/hotfix/gitee-sync
ci: add GitHub Actions workflow to sync branches to Gitee
2026-04-02 10:44:22 +08:00
28 changed files with 268 additions and 415 deletions

View File

@@ -23,7 +23,6 @@ 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()
@@ -461,20 +460,18 @@ 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=llm_key.api_key, key=db_knowledge.llm.api_keys[0].api_key,
model_name=llm_key.model_name, model_name=db_knowledge.llm.api_keys[0].model_name,
base_url=llm_key.api_base base_url=db_knowledge.llm.api_keys[0].api_base
) )
embedding_model = OpenAIEmbed( embedding_model = OpenAIEmbed(
key=emb_key.api_key, key=db_knowledge.embedding.api_keys[0].api_key,
model_name=emb_key.model_name, model_name=db_knowledge.embedding.api_keys[0].model_name,
base_url=emb_key.api_base base_url=db_knowledge.embedding.api_keys[0].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")

View File

@@ -292,10 +292,9 @@ 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 sys.modules[LOCK_KEY_pdfplumber]: # ← 加这一行,获取全局锁 with pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm)) as pdf:
with pdfplumber.open(fnm) if isinstance(fnm, (str, PathLike)) else pdfplumber.open(BytesIO(fnm)) as pdf: self.pdf = pdf
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])]
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

View File

@@ -28,7 +28,6 @@ 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__)
@@ -115,8 +114,9 @@ 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:
all_results = rerank(db=db, reranker_id=reranker_id, query=query, docs=all_results, top_k=reranker_top_k) return 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,10 +132,7 @@ 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, DocumentChunk( all_results.insert(0, doc)
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)}")
@@ -201,18 +198,16 @@ 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=llm_key.api_key, key=db_knowledge.llm.api_keys[0].api_key,
model_name=llm_key.model_name, model_name=db_knowledge.llm.api_keys[0].model_name,
base_url=llm_key.api_base, base_url=db_knowledge.llm.api_keys[0].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=emb_key.api_key, key=db_knowledge.embedding.api_keys[0].api_key,
model_name=emb_key.model_name, model_name=db_knowledge.embedding.api_keys[0].model_name,
base_url=emb_key.api_base, base_url=db_knowledge.embedding.api_keys[0].api_base,
) )
vector_service = ElasticSearchVectorFactory().init_vector(knowledge=db_knowledge) vector_service = ElasticSearchVectorFactory().init_vector(knowledge=db_knowledge)
@@ -253,29 +248,6 @@ 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

View File

@@ -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").strip() input_value = kwargs.get("input_value")
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()) * 1000, "timestamp": int(dt.timestamp()),
"iso_format": dt.isoformat(), "iso_format": dt.isoformat(),
"result_data": int(dt.timestamp()) * 1000 "result_data": int(dt.timestamp())
} }
def _calculate_datetime(self, kwargs) -> dict: def _calculate_datetime(self, kwargs) -> dict:

View File

@@ -138,29 +138,6 @@ 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 []

View File

@@ -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*}}')

View File

@@ -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.ARRAY_NUMBER, VariableType.NUMBER,
VariableType.ARRAY_OBJECT, VariableType.ARRAY_OBJECT,
VariableType.ARRAY_BOOLEAN VariableType.BOOLEAN
]: ]:
if config.flatten: if config.flatten:
outputs['output'] = config.output_type outputs['output'] = config.output_type

View File

@@ -8,8 +8,6 @@ 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
@@ -42,8 +40,7 @@ class KnowledgeRetrievalNode(BaseNode):
return business_result["chunks"] return business_result["chunks"]
return business_result return business_result
@staticmethod def _extract_citations(self, business_result: Any) -> list:
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 []
@@ -233,23 +230,23 @@ class KnowledgeRetrievalNode(BaseNode):
} }
) )
) )
case retrieve_type if retrieve_type in (RetrieveType.HYBRID, RetrieveType.Graph): case RetrieveType.HYBRID:
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
@@ -269,33 +266,6 @@ 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

View File

@@ -574,29 +574,6 @@ 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 [

View File

@@ -229,11 +229,7 @@ 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,
retrieve_type: item.retrieve_type, ...(item.config || {})
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 => {

View File

@@ -117,7 +117,6 @@ 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])

View File

@@ -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'
] ]
/** /**

View File

@@ -88,10 +88,6 @@ 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",

View File

@@ -155,10 +155,12 @@ 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 />}

View File

@@ -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>('mineru'); const [pdfEnhancementMethod, setPdfEnhancementMethod] = useState<string>('deepdoc');
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(
() => [ () => [

View File

@@ -89,6 +89,14 @@ 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}`),

View File

@@ -149,8 +149,8 @@ const Editor: FC<LexicalEditorProps> =({
<HistoryPlugin /> <HistoryPlugin />
<CommandPlugin /> <CommandPlugin />
<AutocompletePlugin options={options} /> <AutocompletePlugin options={options} />
<CharacterCountPlugin setCount={setCount} /> <CharacterCountPlugin setCount={setCount} onChange={onChange} />
<InitialValuePlugin value={value} options={options} onChange={onChange} /> <InitialValuePlugin value={value} options={options} />
<BlurPlugin /> <BlurPlugin />
</div> </div>
</LexicalComposer> </LexicalComposer>

View File

@@ -1,22 +1,41 @@
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { $getRoot, $isParagraphNode } from 'lexical'; import { $getRoot, $isParagraphNode } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
const CharacterCountPlugin = ({ setCount }: { setCount: (count: number) => void }) => { import { $isVariableNode } from '../nodes/VariableNode';
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)) {
paragraphs.push(child.getChildren().map(n => n.getTextContent()).join('')); let paragraphContent = '';
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]);

View File

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

View File

@@ -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, $isParagraphNode } from 'lexical'; import { $getRoot, $createParagraphNode, $createTextNode } 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,34 +14,24 @@ 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 = [], onChange }) => { const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
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 paragraphs: string[] = []; const textContent = root.getTextContent();
root.getChildren().forEach(child => { if (textContent !== prevValueRef.current) {
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 = text; prevValueRef.current = textContent;
onChangeRef.current?.(text);
} }
}); });
}); });

View File

@@ -49,24 +49,23 @@ 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: any) => { incomingEdges?.forEach(edge => {
addedEdges.push(graph.addEdge({ 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: any) => { outgoingEdges?.forEach(edge => {
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();
addedEdges.push(graph.addEdge({ 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
@@ -76,15 +75,6 @@ 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) {

View File

@@ -59,9 +59,6 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
target: { cell: addNode.id, port: targetPort }, target: { cell: addNode.id, port: targetPort },
...edgeAttrs, ...edgeAttrs,
}); });
cycleStartNode.toFront()
addNode.toFront()
} }
} }
@@ -120,12 +117,6 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
...edgeAttrs ...edgeAttrs
} }
graph.addEdge(edgeConfig) graph.addEdge(edgeConfig)
setTimeout(() => {
cycleStartNode.toFront()
addNode.toFront()
}, 0)
} }
return ( return (

View File

@@ -34,12 +34,9 @@ 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);
}; };
}, []); }, []);
@@ -191,39 +188,38 @@ 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';
addedEdges.push(graph.addEdge({ 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
})); });
addedEdges.push(graph.addEdge({ 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';
addedEdges.push(graph.addEdge({ 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';
addedEdges.push(graph.addEdge({ 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
@@ -270,44 +266,6 @@ 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

View File

@@ -61,20 +61,6 @@ 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;
@@ -153,10 +139,6 @@ const CaseList: FC<CaseListProps> = ({
...edgeAttrs, ...edgeAttrs,
}); });
} }
sourceCell.toFront()
selectedNode.toFront()
bringLoopChildrenToFront(sourceCell)
bringLoopChildrenToFront(selectedNode)
graphRef.current?.removeCell(edge); graphRef.current?.removeCell(edge);
return; return;
} }
@@ -201,10 +183,6 @@ const CaseList: FC<CaseListProps> = ({
target: { cell: targetCellId, port: targetPortId }, target: { cell: targetCellId, port: targetPortId },
...edgeAttrs ...edgeAttrs
}); });
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
targetCell.toFront()
bringLoopChildrenToFront(targetCell)
} }
} }

View File

@@ -25,20 +25,6 @@ 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;
@@ -102,10 +88,6 @@ 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;
} }
@@ -137,10 +119,6 @@ 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)
} }
} }
}); });

View File

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

View File

@@ -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: 1, opacity: 0,
}, },
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: 1, opacity: 0,
}, },
}, },
} }

View File

@@ -211,7 +211,7 @@ export const useWorkflowGraph = ({
id, id,
type, type,
name, name,
data: { ...node, ...nodeLibraryConfig }, data: { ...node, ...nodeLibraryConfig},
...position, ...position,
} }
@@ -294,8 +294,7 @@ 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 }}}
}
] ]
}; };
} }
@@ -445,17 +444,9 @@ export const useWorkflowGraph = ({
setTimeout(() => { setTimeout(() => {
if (graphRef.current) { if (graphRef.current) {
graphRef.current.centerContent() graphRef.current.centerContent()
graphRef.current.getNodes().forEach(node => { // graphRef.current.getNodes().forEach(node => node.toFront());
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 => { graphRef.current.getEdges().forEach(edge => edge.toFront());
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();
}); });
@@ -584,7 +575,6 @@ 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
@@ -979,6 +969,25 @@ 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
@@ -996,6 +1005,11 @@ 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 => {
@@ -1004,31 +1018,48 @@ 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 }) => { graphRef.current.on('edge:connected', ({ isNew, edge }) => {
// 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 => {
if (!node.getData()?.cycle) node.toFront(); node.getPorts().filter(p => p.group === 'right').forEach(p => {
}); node.setPortProp(p.id!, 'attrs/body/opacity', 1);
graphRef.current?.getEdges().forEach(edge => { node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0);
const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId()); node.setPortProp(p.id!, 'attrs/label/opacity', 0);
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();
}); });
} }
}); });
@@ -1058,9 +1089,34 @@ export const useWorkflowGraph = ({
if (found) break; if (found) break;
} }
lastHoveredPort = found; if (found?.node.id !== lastHoveredPort?.node.id || found?.portId !== lastHoveredPort?.portId) {
// 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
@@ -1271,7 +1327,7 @@ export const useWorkflowGraph = ({
// 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;
} }
@@ -1315,34 +1371,34 @@ export const useWorkflowGraph = ({
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)
}) })
}) })
} }