Merge remote-tracking branch 'origin/feat/workflow' into feat/workflow

# Conflicts:
#	api/core/workflow/nodes/question_classifier/question_classifier_node.py
This commit is contained in:
jyong
2024-03-29 19:00:21 +08:00
33 changed files with 518 additions and 419 deletions

View File

@@ -21,6 +21,8 @@ class AdvancedPromptTransform(PromptTransform):
"""
Advanced Prompt Transform for Workflow LLM Node.
"""
def __init__(self, with_variable_tmpl: bool = False) -> None:
self.with_variable_tmpl = with_variable_tmpl
def get_prompt(self, prompt_template: Union[list[ChatModelMessage], CompletionModelPromptTemplate],
inputs: dict,
@@ -74,7 +76,7 @@ class AdvancedPromptTransform(PromptTransform):
prompt_messages = []
prompt_template = PromptTemplateParser(template=raw_prompt)
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs)
@@ -128,7 +130,7 @@ class AdvancedPromptTransform(PromptTransform):
for prompt_item in raw_prompt_list:
raw_prompt = prompt_item.text
prompt_template = PromptTemplateParser(template=raw_prompt)
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
prompt_inputs = self._set_context_variable(context, prompt_template, prompt_inputs)
@@ -211,7 +213,7 @@ class AdvancedPromptTransform(PromptTransform):
if '#histories#' in prompt_template.variable_keys:
if memory:
inputs = {'#histories#': '', **prompt_inputs}
prompt_template = PromptTemplateParser(raw_prompt)
prompt_template = PromptTemplateParser(template=raw_prompt, with_variable_tmpl=self.with_variable_tmpl)
prompt_inputs = {k: inputs[k] for k in prompt_template.variable_keys if k in inputs}
tmp_human_message = UserPromptMessage(
content=prompt_template.format(prompt_inputs)

View File

@@ -1,6 +1,9 @@
import re
REGEX = re.compile(r"\{\{([a-zA-Z_][a-zA-Z0-9_]{0,29}|#histories#|#query#|#context#)\}\}")
WITH_VARIABLE_TMPL_REGEX = re.compile(
r"\{\{([a-zA-Z_][a-zA-Z0-9_]{0,29}|#[a-zA-Z0-9_]{1,50}\.[a-zA-Z0-9_\.]{1,100}#|#histories#|#query#|#context#)\}\}"
)
class PromptTemplateParser:
@@ -15,13 +18,15 @@ class PromptTemplateParser:
`{{#histories#}}` `{{#query#}}` `{{#context#}}`. No other `{{##}}` template variables are allowed.
"""
def __init__(self, template: str):
def __init__(self, template: str, with_variable_tmpl: bool = False):
self.template = template
self.with_variable_tmpl = with_variable_tmpl
self.regex = WITH_VARIABLE_TMPL_REGEX if with_variable_tmpl else REGEX
self.variable_keys = self.extract()
def extract(self) -> list:
# Regular expression to match the template rules
return re.findall(REGEX, self.template)
return re.findall(self.regex, self.template)
def format(self, inputs: dict, remove_template_variables: bool = True) -> str:
def replacer(match):
@@ -29,12 +34,12 @@ class PromptTemplateParser:
value = inputs.get(key, match.group(0)) # return original matched string if key not found
if remove_template_variables:
return PromptTemplateParser.remove_template_variables(value)
return PromptTemplateParser.remove_template_variables(value, self.with_variable_tmpl)
return value
prompt = re.sub(REGEX, replacer, self.template)
prompt = re.sub(self.regex, replacer, self.template)
return re.sub(r'<\|.*?\|>', '', prompt)
@classmethod
def remove_template_variables(cls, text: str):
return re.sub(REGEX, r'{\1}', text)
def remove_template_variables(cls, text: str, with_variable_tmpl: bool = False):
return re.sub(WITH_VARIABLE_TMPL_REGEX if with_variable_tmpl else REGEX, r'{\1}', text)

View File

@@ -13,6 +13,7 @@ from core.workflow.nodes.answer.entities import (
VarGenerateRouteChunk,
)
from core.workflow.nodes.base_node import BaseNode
from core.workflow.utils.variable_template_parser import VariableTemplateParser
from models.workflow import WorkflowNodeExecutionStatus
@@ -66,32 +67,8 @@ class AnswerNode(BaseNode):
part = cast(TextGenerateRouteChunk, part)
answer += part.text
# re-fetch variable values
variable_values = {}
for variable_selector in node_data.variables:
value = variable_pool.get_variable_value(
variable_selector=variable_selector.value_selector
)
if isinstance(value, str | int | float):
value = str(value)
elif isinstance(value, FileVar):
value = value.to_dict()
elif isinstance(value, list):
new_value = []
for item in value:
if isinstance(item, FileVar):
new_value.append(item.to_dict())
else:
new_value.append(item)
value = new_value
variable_values[variable_selector.variable] = value
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs=variable_values,
outputs={
"answer": answer
}
@@ -116,15 +93,18 @@ class AnswerNode(BaseNode):
:param node_data: node data object
:return:
"""
variable_template_parser = VariableTemplateParser(template=node_data.answer)
variable_selectors = variable_template_parser.extract_variable_selectors()
value_selector_mapping = {
variable_selector.variable: variable_selector.value_selector
for variable_selector in node_data.variables
for variable_selector in variable_selectors
}
variable_keys = list(value_selector_mapping.keys())
# format answer template
template_parser = PromptTemplateParser(node_data.answer)
template_parser = PromptTemplateParser(template=node_data.answer, with_variable_tmpl=True)
template_variable_keys = template_parser.variable_keys
# Take the intersection of variable_keys and template_variable_keys
@@ -164,8 +144,11 @@ class AnswerNode(BaseNode):
"""
node_data = cast(cls._node_data_cls, node_data)
variable_template_parser = VariableTemplateParser(template=node_data.answer)
variable_selectors = variable_template_parser.extract_variable_selectors()
variable_mapping = {}
for variable_selector in node_data.variables:
for variable_selector in variable_selectors:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
return variable_mapping

View File

@@ -2,14 +2,12 @@
from pydantic import BaseModel
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.variable_entities import VariableSelector
class AnswerNodeData(BaseNodeData):
"""
Answer Node Data.
"""
variables: list[VariableSelector] = []
answer: str

View File

@@ -4,7 +4,6 @@ from pydantic import BaseModel
from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.variable_entities import VariableSelector
class ModelConfig(BaseModel):
@@ -44,7 +43,6 @@ class LLMNodeData(BaseNodeData):
LLM Node Data.
"""
model: ModelConfig
variables: list[VariableSelector] = []
prompt_template: Union[list[ChatModelMessage], CompletionModelPromptTemplate]
memory: Optional[MemoryConfig] = None
context: ContextConfig

View File

@@ -15,13 +15,14 @@ from core.model_runtime.entities.model_entities import ModelType
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.model_runtime.utils.encoders import jsonable_encoder
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
from core.prompt.entities.advanced_prompt_entities import MemoryConfig
from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig
from core.prompt.utils.prompt_message_util import PromptMessageUtil
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult, NodeType, SystemVariable
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.nodes.base_node import BaseNode
from core.workflow.nodes.llm.entities import LLMNodeData, ModelConfig
from core.workflow.utils.variable_template_parser import VariableTemplateParser
from extensions.ext_database import db
from models.model import Conversation
from models.provider import Provider, ProviderType
@@ -48,9 +49,7 @@ class LLMNode(BaseNode):
# fetch variables and fetch values from variable pool
inputs = self._fetch_inputs(node_data, variable_pool)
node_inputs = {
**inputs
}
node_inputs = {}
# fetch files
files: list[FileVar] = self._fetch_files(node_data, variable_pool)
@@ -192,10 +191,21 @@ class LLMNode(BaseNode):
:return:
"""
inputs = {}
for variable_selector in node_data.variables:
prompt_template = node_data.prompt_template
variable_selectors = []
if isinstance(prompt_template, list):
for prompt in prompt_template:
variable_template_parser = VariableTemplateParser(template=prompt.text)
variable_selectors.extend(variable_template_parser.extract_variable_selectors())
elif isinstance(prompt_template, CompletionModelPromptTemplate):
variable_template_parser = VariableTemplateParser(template=prompt_template.text)
variable_selectors = variable_template_parser.extract_variable_selectors()
for variable_selector in variable_selectors:
variable_value = variable_pool.get_variable_value(variable_selector.value_selector)
if variable_value is None:
raise ValueError(f'Variable {variable_selector.value_selector} not found')
raise ValueError(f'Variable {variable_selector.variable} not found')
inputs[variable_selector.variable] = variable_value
@@ -411,7 +421,7 @@ class LLMNode(BaseNode):
:param model_config: model config
:return:
"""
prompt_transform = AdvancedPromptTransform()
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
prompt_messages = prompt_transform.get_prompt(
prompt_template=node_data.prompt_template,
inputs=inputs,
@@ -486,9 +496,6 @@ class LLMNode(BaseNode):
node_data = cast(cls._node_data_cls, node_data)
variable_mapping = {}
for variable_selector in node_data.variables:
variable_mapping[variable_selector.variable] = variable_selector.value_selector
if node_data.context.enabled:
variable_mapping['#context#'] = node_data.context.variable_selector

View File

@@ -128,7 +128,7 @@ class QuestionClassifierNode(LLMNode):
:param model_config: model config
:return:
"""
prompt_transform = AdvancedPromptTransform()
prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True)
prompt_template = self._get_prompt_template(node_data, query, memory)
prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template,

View File

View File

@@ -0,0 +1,58 @@
import re
from core.workflow.entities.variable_entities import VariableSelector
REGEX = re.compile(r"\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}")
class VariableTemplateParser:
"""
Rules:
1. Template variables must be enclosed in `{{}}`.
2. The template variable Key can only be: #node_id.var1.var2#.
3. The template variable Key cannot contain new lines or spaces, and must comply with rule 2.
"""
def __init__(self, template: str):
self.template = template
self.variable_keys = self.extract()
def extract(self) -> list:
# Regular expression to match the template rules
matches = re.findall(REGEX, self.template)
first_group_matches = [match[0] for match in matches]
return list(set(first_group_matches))
def extract_variable_selectors(self) -> list[VariableSelector]:
variable_selectors = []
for variable_key in self.variable_keys:
remove_hash = variable_key.replace('#', '')
split_result = remove_hash.split('.')
if len(split_result) < 2:
continue
variable_selectors.append(VariableSelector(
variable=variable_key,
value_selector=split_result
))
return variable_selectors
def format(self, inputs: dict, remove_template_variables: bool = True) -> str:
def replacer(match):
key = match.group(1)
value = inputs.get(key, match.group(0)) # return original matched string if key not found
if remove_template_variables:
return VariableTemplateParser.remove_template_variables(value)
return value
prompt = re.sub(REGEX, replacer, self.template)
return re.sub(r'<\|.*?\|>', '', prompt)
@classmethod
def remove_template_variables(cls, text: str):
return re.sub(REGEX, r'{\1}', text)

View File

@@ -291,7 +291,7 @@ class WorkflowConverter:
if app_model.mode == AppMode.CHAT.value:
http_request_variables.append({
"variable": "_query",
"value_selector": ["start", "sys.query"]
"value_selector": ["sys", ".query"]
})
request_body = {
@@ -375,7 +375,7 @@ class WorkflowConverter:
"""
retrieve_config = dataset_config.retrieve_config
if new_app_mode == AppMode.ADVANCED_CHAT:
query_variable_selector = ["start", "sys.query"]
query_variable_selector = ["sys", "query"]
elif retrieve_config.query_variable:
# fetch query variable
query_variable_selector = ["start", retrieve_config.query_variable]
@@ -449,19 +449,31 @@ class WorkflowConverter:
has_context=knowledge_retrieval_node is not None,
query_in_prompt=False
)
template = prompt_template_config['prompt_template'].template
for v in start_node['data']['variables']:
template = template.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}')
prompts = [
{
"role": 'user',
"text": prompt_template_config['prompt_template'].template
"text": template
}
]
else:
advanced_chat_prompt_template = prompt_template.advanced_chat_prompt_template
prompts = [{
"role": m.role.value,
"text": m.text
} for m in advanced_chat_prompt_template.messages] \
if advanced_chat_prompt_template else []
prompts = []
for m in advanced_chat_prompt_template.messages:
if advanced_chat_prompt_template:
text = m.text
for v in start_node['data']['variables']:
text = text.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}')
prompts.append({
"role": m.role.value,
"text": text
})
# Completion Model
else:
if prompt_template.prompt_type == PromptTemplateEntity.PromptType.SIMPLE:
@@ -475,8 +487,13 @@ class WorkflowConverter:
has_context=knowledge_retrieval_node is not None,
query_in_prompt=False
)
template = prompt_template_config['prompt_template'].template
for v in start_node['data']['variables']:
template = template.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}')
prompts = {
"text": prompt_template_config['prompt_template'].template
"text": template
}
prompt_rules = prompt_template_config['prompt_rules']
@@ -486,9 +503,16 @@ class WorkflowConverter:
}
else:
advanced_completion_prompt_template = prompt_template.advanced_completion_prompt_template
if advanced_completion_prompt_template:
text = advanced_completion_prompt_template.prompt
for v in start_node['data']['variables']:
text = text.replace('{{' + v['variable'] + '}}', '{{#start.' + v['variable'] + '#}}')
else:
text = ""
prompts = {
"text": advanced_completion_prompt_template.prompt,
} if advanced_completion_prompt_template else {"text": ""}
"text": text,
}
if advanced_completion_prompt_template.role_prefix:
role_prefix = {
@@ -519,10 +543,6 @@ class WorkflowConverter:
"mode": model_config.mode,
"completion_params": completion_params
},
"variables": [{
"variable": v['variable'],
"value_selector": ["start", v['variable']]
} for v in start_node['data']['variables']],
"prompt_template": prompts,
"memory": memory,
"context": {
@@ -532,7 +552,7 @@ class WorkflowConverter:
},
"vision": {
"enabled": file_upload is not None,
"variable_selector": ["start", "sys.files"] if file_upload is not None else None,
"variable_selector": ["sys", "files"] if file_upload is not None else None,
"configs": {
"detail": file_upload.image_config['detail']
} if file_upload is not None else None
@@ -571,11 +591,7 @@ class WorkflowConverter:
"data": {
"title": "ANSWER",
"type": NodeType.ANSWER.value,
"variables": [{
"variable": "text",
"value_selector": ["llm", "text"]
}],
"answer": "{{text}}"
"answer": "{{#llm.text#}}"
}
}

View File

@@ -40,32 +40,17 @@ def test_execute_llm(setup_openai_mock):
'mode': 'chat',
'completion_params': {}
},
'variables': [
{
'variable': 'weather',
'value_selector': ['abc', 'output'],
},
{
'variable': 'query',
'value_selector': ['sys', 'query']
}
],
'prompt_template': [
{
'role': 'system',
'text': 'you are a helpful assistant.\ntoday\'s weather is {{weather}}.'
'text': 'you are a helpful assistant.\ntoday\'s weather is {{#abc.output#}}.'
},
{
'role': 'user',
'text': '{{query}}'
'text': '{{#sys.query#}}'
}
],
'memory': {
'window': {
'enabled': True,
'size': 2
}
},
'memory': None,
'context': {
'enabled': False
},

View File

@@ -4,7 +4,6 @@ from core.workflow.entities.node_entities import SystemVariable
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.nodes.answer.answer_node import AnswerNode
from core.workflow.nodes.base_node import UserFrom
from core.workflow.nodes.if_else.if_else_node import IfElseNode
from extensions.ext_database import db
from models.workflow import WorkflowNodeExecutionStatus
@@ -21,17 +20,7 @@ def test_execute_answer():
'data': {
'title': '123',
'type': 'answer',
'variables': [
{
'value_selector': ['llm', 'text'],
'variable': 'text'
},
{
'value_selector': ['start', 'weather'],
'variable': 'weather'
},
],
'answer': 'Today\'s weather is {{weather}}\n{{text}}\n{{img}}\nFin.'
'answer': 'Today\'s weather is {{#start.weather#}}\n{{#llm.text#}}\n{{img}}\nFin.'
}
}
)

View File

@@ -19,7 +19,7 @@ from services.workflow.workflow_converter import WorkflowConverter
def default_variables():
return [
VariableEntity(
variable="text-input",
variable="text_input",
label="text-input",
type=VariableEntity.Type.TEXT_INPUT
),
@@ -43,7 +43,7 @@ def test__convert_to_start_node(default_variables):
# assert
assert isinstance(result["data"]["variables"][0]["type"], str)
assert result["data"]["variables"][0]["type"] == "text-input"
assert result["data"]["variables"][0]["variable"] == "text-input"
assert result["data"]["variables"][0]["variable"] == "text_input"
assert result["data"]["variables"][1]["variable"] == "paragraph"
assert result["data"]["variables"][2]["variable"] == "select"
@@ -191,7 +191,7 @@ def test__convert_to_http_request_node_for_workflow_app(default_variables):
def test__convert_to_knowledge_retrieval_node_for_chatbot():
new_app_mode = AppMode.CHAT
new_app_mode = AppMode.ADVANCED_CHAT
dataset_config = DatasetEntity(
dataset_ids=["dataset_id_1", "dataset_id_2"],
@@ -221,7 +221,7 @@ def test__convert_to_knowledge_retrieval_node_for_chatbot():
)
assert node["data"]["type"] == "knowledge-retrieval"
assert node["data"]["query_variable_selector"] == ["start", "sys.query"]
assert node["data"]["query_variable_selector"] == ["sys", "query"]
assert node["data"]["dataset_ids"] == dataset_config.dataset_ids
assert (node["data"]["retrieval_mode"]
== dataset_config.retrieve_config.retrieve_strategy.value)
@@ -276,7 +276,7 @@ def test__convert_to_knowledge_retrieval_node_for_workflow_app():
def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables):
new_app_mode = AppMode.CHAT
new_app_mode = AppMode.ADVANCED_CHAT
model = "gpt-4"
model_mode = LLMMode.CHAT
@@ -298,7 +298,7 @@ def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables):
prompt_template = PromptTemplateEntity(
prompt_type=PromptTemplateEntity.PromptType.SIMPLE,
simple_prompt_template="You are a helpful assistant {{text-input}}, {{paragraph}}, {{select}}."
simple_prompt_template="You are a helpful assistant {{text_input}}, {{paragraph}}, {{select}}."
)
llm_node = workflow_converter._convert_to_llm_node(
@@ -311,16 +311,15 @@ def test__convert_to_llm_node_for_chatbot_simple_chat_model(default_variables):
assert llm_node["data"]["type"] == "llm"
assert llm_node["data"]["model"]['name'] == model
assert llm_node["data"]['model']["mode"] == model_mode.value
assert llm_node["data"]["variables"] == [{
"variable": v.variable,
"value_selector": ["start", v.variable]
} for v in default_variables]
assert llm_node["data"]["prompts"][0]['text'] == prompt_template.simple_prompt_template + '\n'
template = prompt_template.simple_prompt_template
for v in default_variables:
template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}')
assert llm_node["data"]["prompt_template"][0]['text'] == template + '\n'
assert llm_node["data"]['context']['enabled'] is False
def test__convert_to_llm_node_for_chatbot_simple_completion_model(default_variables):
new_app_mode = AppMode.CHAT
new_app_mode = AppMode.ADVANCED_CHAT
model = "gpt-3.5-turbo-instruct"
model_mode = LLMMode.COMPLETION
@@ -342,7 +341,7 @@ def test__convert_to_llm_node_for_chatbot_simple_completion_model(default_variab
prompt_template = PromptTemplateEntity(
prompt_type=PromptTemplateEntity.PromptType.SIMPLE,
simple_prompt_template="You are a helpful assistant {{text-input}}, {{paragraph}}, {{select}}."
simple_prompt_template="You are a helpful assistant {{text_input}}, {{paragraph}}, {{select}}."
)
llm_node = workflow_converter._convert_to_llm_node(
@@ -355,16 +354,15 @@ def test__convert_to_llm_node_for_chatbot_simple_completion_model(default_variab
assert llm_node["data"]["type"] == "llm"
assert llm_node["data"]["model"]['name'] == model
assert llm_node["data"]['model']["mode"] == model_mode.value
assert llm_node["data"]["variables"] == [{
"variable": v.variable,
"value_selector": ["start", v.variable]
} for v in default_variables]
assert llm_node["data"]["prompts"]['text'] == prompt_template.simple_prompt_template + '\n'
template = prompt_template.simple_prompt_template
for v in default_variables:
template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}')
assert llm_node["data"]["prompt_template"]['text'] == template + '\n'
assert llm_node["data"]['context']['enabled'] is False
def test__convert_to_llm_node_for_chatbot_advanced_chat_model(default_variables):
new_app_mode = AppMode.CHAT
new_app_mode = AppMode.ADVANCED_CHAT
model = "gpt-4"
model_mode = LLMMode.CHAT
@@ -404,17 +402,16 @@ def test__convert_to_llm_node_for_chatbot_advanced_chat_model(default_variables)
assert llm_node["data"]["type"] == "llm"
assert llm_node["data"]["model"]['name'] == model
assert llm_node["data"]['model']["mode"] == model_mode.value
assert llm_node["data"]["variables"] == [{
"variable": v.variable,
"value_selector": ["start", v.variable]
} for v in default_variables]
assert isinstance(llm_node["data"]["prompts"], list)
assert len(llm_node["data"]["prompts"]) == len(prompt_template.advanced_chat_prompt_template.messages)
assert llm_node["data"]["prompts"][0]['text'] == prompt_template.advanced_chat_prompt_template.messages[0].text
assert isinstance(llm_node["data"]["prompt_template"], list)
assert len(llm_node["data"]["prompt_template"]) == len(prompt_template.advanced_chat_prompt_template.messages)
template = prompt_template.advanced_chat_prompt_template.messages[0].text
for v in default_variables:
template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}')
assert llm_node["data"]["prompt_template"][0]['text'] == template
def test__convert_to_llm_node_for_workflow_advanced_completion_model(default_variables):
new_app_mode = AppMode.CHAT
new_app_mode = AppMode.ADVANCED_CHAT
model = "gpt-3.5-turbo-instruct"
model_mode = LLMMode.COMPLETION
@@ -456,9 +453,8 @@ def test__convert_to_llm_node_for_workflow_advanced_completion_model(default_var
assert llm_node["data"]["type"] == "llm"
assert llm_node["data"]["model"]['name'] == model
assert llm_node["data"]['model']["mode"] == model_mode.value
assert llm_node["data"]["variables"] == [{
"variable": v.variable,
"value_selector": ["start", v.variable]
} for v in default_variables]
assert isinstance(llm_node["data"]["prompts"], dict)
assert llm_node["data"]["prompts"]['text'] == prompt_template.advanced_completion_prompt_template.prompt
assert isinstance(llm_node["data"]["prompt_template"], dict)
template = prompt_template.advanced_completion_prompt_template.prompt
for v in default_variables:
template = template.replace('{{' + v.variable + '}}', '{{#start.' + v.variable + '#}}')
assert llm_node["data"]["prompt_template"]['text'] == template

View File

@@ -215,7 +215,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
getRedirection(isCurrentWorkspaceManager, app, push)
}}
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='relative shrink-0'>

View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="checklist-square">
<path id="Vector" d="M9.7823 11.9146C9.32278 11.6082 8.70191 11.7324 8.39554 12.1919C8.08918 12.6514 8.21333 13.2723 8.67285 13.5787L9.7823 11.9146ZM10.9151 13.8717L10.3603 14.7037C10.8019 14.9982 11.3966 14.8963 11.7151 14.4717L10.9151 13.8717ZM14.5226 10.7284C14.8539 10.2865 14.7644 9.65973 14.3225 9.32836C13.8807 8.99699 13.2539 9.08653 12.9225 9.52836L14.5226 10.7284ZM19.3333 11C18.781 11 18.3333 11.4477 18.3333 12C18.3333 12.5523 18.781 13 19.3333 13V11ZM22 13C22.5523 13 23 12.5523 23 12C23 11.4477 22.5523 11 22 11V13ZM19.3333 19C18.781 19 18.3333 19.4477 18.3333 20C18.3333 20.5523 18.781 21 19.3333 21V19ZM22 21C22.5523 21 23 20.5523 23 20C23 19.4477 22.5523 19 22 19V21ZM9.86913 19.9163C9.4096 19.6099 8.78873 19.7341 8.48238 20.1937C8.17602 20.6532 8.3002 21.274 8.75973 21.5804L9.86913 19.9163ZM11.0019 21.8734L10.4472 22.7054C10.8888 22.9998 11.4835 22.8979 11.8019 22.4734L11.0019 21.8734ZM14.6094 18.7301C14.9408 18.2883 14.8512 17.6615 14.4094 17.3301C13.9676 16.9987 13.3408 17.0883 13.0094 17.5301L14.6094 18.7301ZM6.18404 27.564L5.73005 28.455H5.73005L6.18404 27.564ZM4.43597 25.816L3.54497 26.27H3.54497L4.43597 25.816ZM27.564 25.816L28.455 26.27L27.564 25.816ZM25.816 27.564L26.27 28.455L25.816 27.564ZM25.816 4.43597L26.27 3.54497V3.54497L25.816 4.43597ZM27.564 6.18404L28.455 5.73005V5.73005L27.564 6.18404ZM6.18404 4.43597L5.73005 3.54497L6.18404 4.43597ZM4.43597 6.18404L3.54497 5.73005L4.43597 6.18404ZM8.67285 13.5787L10.3603 14.7037L11.4698 13.0397L9.7823 11.9146L8.67285 13.5787ZM11.7151 14.4717L14.5226 10.7284L12.9225 9.52836L10.1151 13.2717L11.7151 14.4717ZM19.3333 13H22V11H19.3333V13ZM19.3333 21H22V19H19.3333V21ZM8.75973 21.5804L10.4472 22.7054L11.5566 21.0413L9.86913 19.9163L8.75973 21.5804ZM11.8019 22.4734L14.6094 18.7301L13.0094 17.5301L10.2019 21.2733L11.8019 22.4734ZM10.4 5H21.6V3H10.4V5ZM27 10.4V21.6H29V10.4H27ZM21.6 27H10.4V29H21.6V27ZM5 21.6V10.4H3V21.6H5ZM10.4 27C9.26339 27 8.47108 26.9992 7.85424 26.9488C7.24907 26.8994 6.90138 26.8072 6.63803 26.673L5.73005 28.455C6.32234 28.7568 6.96253 28.8826 7.69138 28.9422C8.40855 29.0008 9.2964 29 10.4 29V27ZM3 21.6C3 22.7036 2.99922 23.5914 3.05782 24.3086C3.11737 25.0375 3.24318 25.6777 3.54497 26.27L5.32698 25.362C5.19279 25.0986 5.10062 24.7509 5.05118 24.1458C5.00078 23.5289 5 22.7366 5 21.6H3ZM6.63803 26.673C6.07354 26.3854 5.6146 25.9265 5.32698 25.362L3.54497 26.27C4.02433 27.2108 4.78924 27.9757 5.73005 28.455L6.63803 26.673ZM27 21.6C27 22.7366 26.9992 23.5289 26.9488 24.1458C26.8994 24.7509 26.8072 25.0986 26.673 25.362L28.455 26.27C28.7568 25.6777 28.8826 25.0375 28.9422 24.3086C29.0008 23.5914 29 22.7036 29 21.6H27ZM21.6 29C22.7036 29 23.5914 29.0008 24.3086 28.9422C25.0375 28.8826 25.6777 28.7568 26.27 28.455L25.362 26.673C25.0986 26.8072 24.7509 26.8994 24.1458 26.9488C23.5289 26.9992 22.7366 27 21.6 27V29ZM26.673 25.362C26.3854 25.9265 25.9265 26.3854 25.362 26.673L26.27 28.455C27.2108 27.9757 27.9757 27.2108 28.455 26.27L26.673 25.362ZM21.6 5C22.7366 5 23.5289 5.00078 24.1458 5.05118C24.7509 5.10062 25.0986 5.19279 25.362 5.32698L26.27 3.54497C25.6777 3.24318 25.0375 3.11737 24.3086 3.05782C23.5914 2.99922 22.7036 3 21.6 3V5ZM29 10.4C29 9.2964 29.0008 8.40855 28.9422 7.69138C28.8826 6.96253 28.7568 6.32234 28.455 5.73005L26.673 6.63803C26.8072 6.90138 26.8994 7.24907 26.9488 7.85424C26.9992 8.47108 27 9.26339 27 10.4H29ZM25.362 5.32698C25.9265 5.6146 26.3854 6.07354 26.673 6.63803L28.455 5.73005C27.9757 4.78924 27.2108 4.02433 26.27 3.54497L25.362 5.32698ZM10.4 3C9.2964 3 8.40855 2.99922 7.69138 3.05782C6.96253 3.11737 6.32234 3.24318 5.73005 3.54497L6.63803 5.32698C6.90138 5.19279 7.24907 5.10062 7.85424 5.05118C8.47108 5.00078 9.26339 5 10.4 5V3ZM5 10.4C5 9.26339 5.00078 8.47108 5.05118 7.85424C5.10062 7.24907 5.19279 6.90138 5.32698 6.63803L3.54497 5.73005C3.24318 6.32234 3.11737 6.96253 3.05782 7.69138C2.99922 8.40855 3 9.2964 3 10.4H5ZM5.73005 3.54497C4.78924 4.02433 4.02433 4.78924 3.54497 5.73005L5.32698 6.63803C5.6146 6.07354 6.07354 5.6146 6.63803 5.32698L5.73005 3.54497Z" fill="#D0D5DD"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,36 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "32",
"height": "32",
"viewBox": "0 0 32 32",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "checklist-square"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M9.7823 11.9146C9.32278 11.6082 8.70191 11.7324 8.39554 12.1919C8.08918 12.6514 8.21333 13.2723 8.67285 13.5787L9.7823 11.9146ZM10.9151 13.8717L10.3603 14.7037C10.8019 14.9982 11.3966 14.8963 11.7151 14.4717L10.9151 13.8717ZM14.5226 10.7284C14.8539 10.2865 14.7644 9.65973 14.3225 9.32836C13.8807 8.99699 13.2539 9.08653 12.9225 9.52836L14.5226 10.7284ZM19.3333 11C18.781 11 18.3333 11.4477 18.3333 12C18.3333 12.5523 18.781 13 19.3333 13V11ZM22 13C22.5523 13 23 12.5523 23 12C23 11.4477 22.5523 11 22 11V13ZM19.3333 19C18.781 19 18.3333 19.4477 18.3333 20C18.3333 20.5523 18.781 21 19.3333 21V19ZM22 21C22.5523 21 23 20.5523 23 20C23 19.4477 22.5523 19 22 19V21ZM9.86913 19.9163C9.4096 19.6099 8.78873 19.7341 8.48238 20.1937C8.17602 20.6532 8.3002 21.274 8.75973 21.5804L9.86913 19.9163ZM11.0019 21.8734L10.4472 22.7054C10.8888 22.9998 11.4835 22.8979 11.8019 22.4734L11.0019 21.8734ZM14.6094 18.7301C14.9408 18.2883 14.8512 17.6615 14.4094 17.3301C13.9676 16.9987 13.3408 17.0883 13.0094 17.5301L14.6094 18.7301ZM6.18404 27.564L5.73005 28.455H5.73005L6.18404 27.564ZM4.43597 25.816L3.54497 26.27H3.54497L4.43597 25.816ZM27.564 25.816L28.455 26.27L27.564 25.816ZM25.816 27.564L26.27 28.455L25.816 27.564ZM25.816 4.43597L26.27 3.54497V3.54497L25.816 4.43597ZM27.564 6.18404L28.455 5.73005V5.73005L27.564 6.18404ZM6.18404 4.43597L5.73005 3.54497L6.18404 4.43597ZM4.43597 6.18404L3.54497 5.73005L4.43597 6.18404ZM8.67285 13.5787L10.3603 14.7037L11.4698 13.0397L9.7823 11.9146L8.67285 13.5787ZM11.7151 14.4717L14.5226 10.7284L12.9225 9.52836L10.1151 13.2717L11.7151 14.4717ZM19.3333 13H22V11H19.3333V13ZM19.3333 21H22V19H19.3333V21ZM8.75973 21.5804L10.4472 22.7054L11.5566 21.0413L9.86913 19.9163L8.75973 21.5804ZM11.8019 22.4734L14.6094 18.7301L13.0094 17.5301L10.2019 21.2733L11.8019 22.4734ZM10.4 5H21.6V3H10.4V5ZM27 10.4V21.6H29V10.4H27ZM21.6 27H10.4V29H21.6V27ZM5 21.6V10.4H3V21.6H5ZM10.4 27C9.26339 27 8.47108 26.9992 7.85424 26.9488C7.24907 26.8994 6.90138 26.8072 6.63803 26.673L5.73005 28.455C6.32234 28.7568 6.96253 28.8826 7.69138 28.9422C8.40855 29.0008 9.2964 29 10.4 29V27ZM3 21.6C3 22.7036 2.99922 23.5914 3.05782 24.3086C3.11737 25.0375 3.24318 25.6777 3.54497 26.27L5.32698 25.362C5.19279 25.0986 5.10062 24.7509 5.05118 24.1458C5.00078 23.5289 5 22.7366 5 21.6H3ZM6.63803 26.673C6.07354 26.3854 5.6146 25.9265 5.32698 25.362L3.54497 26.27C4.02433 27.2108 4.78924 27.9757 5.73005 28.455L6.63803 26.673ZM27 21.6C27 22.7366 26.9992 23.5289 26.9488 24.1458C26.8994 24.7509 26.8072 25.0986 26.673 25.362L28.455 26.27C28.7568 25.6777 28.8826 25.0375 28.9422 24.3086C29.0008 23.5914 29 22.7036 29 21.6H27ZM21.6 29C22.7036 29 23.5914 29.0008 24.3086 28.9422C25.0375 28.8826 25.6777 28.7568 26.27 28.455L25.362 26.673C25.0986 26.8072 24.7509 26.8994 24.1458 26.9488C23.5289 26.9992 22.7366 27 21.6 27V29ZM26.673 25.362C26.3854 25.9265 25.9265 26.3854 25.362 26.673L26.27 28.455C27.2108 27.9757 27.9757 27.2108 28.455 26.27L26.673 25.362ZM21.6 5C22.7366 5 23.5289 5.00078 24.1458 5.05118C24.7509 5.10062 25.0986 5.19279 25.362 5.32698L26.27 3.54497C25.6777 3.24318 25.0375 3.11737 24.3086 3.05782C23.5914 2.99922 22.7036 3 21.6 3V5ZM29 10.4C29 9.2964 29.0008 8.40855 28.9422 7.69138C28.8826 6.96253 28.7568 6.32234 28.455 5.73005L26.673 6.63803C26.8072 6.90138 26.8994 7.24907 26.9488 7.85424C26.9992 8.47108 27 9.26339 27 10.4H29ZM25.362 5.32698C25.9265 5.6146 26.3854 6.07354 26.673 6.63803L28.455 5.73005C27.9757 4.78924 27.2108 4.02433 26.27 3.54497L25.362 5.32698ZM10.4 3C9.2964 3 8.40855 2.99922 7.69138 3.05782C6.96253 3.11737 6.32234 3.24318 5.73005 3.54497L6.63803 5.32698C6.90138 5.19279 7.24907 5.10062 7.85424 5.05118C8.47108 5.00078 9.26339 5 10.4 5V3ZM5 10.4C5 9.26339 5.00078 8.47108 5.05118 7.85424C5.10062 7.24907 5.19279 6.90138 5.32698 6.63803L3.54497 5.73005C3.24318 6.32234 3.11737 6.96253 3.05782 7.69138C2.99922 8.40855 3 9.2964 3 10.4H5ZM5.73005 3.54497C4.78924 4.02433 4.02433 4.78924 3.54497 5.73005L5.32698 6.63803C5.6146 6.07354 6.07354 5.6146 6.63803 5.32698L5.73005 3.54497Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "ChecklistSquare"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ChecklistSquare.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ChecklistSquare'
export default Icon

View File

@@ -3,6 +3,7 @@ export { default as Bookmark } from './Bookmark'
export { default as CheckCircle } from './CheckCircle'
export { default as CheckDone01 } from './CheckDone01'
export { default as Check } from './Check'
export { default as ChecklistSquare } from './ChecklistSquare'
export { default as Checklist } from './Checklist'
export { default as DotsGrid } from './DotsGrid'
export { default as DotsHorizontal } from './DotsHorizontal'

View File

@@ -13,7 +13,7 @@ import { CustomTextNode } from '../custom-text/node'
import { $createWorkflowVariableBlockNode } from './node'
import { WorkflowVariableBlockNode } from './index'
const REGEX = /\{\{#(\d+|sys)(\.[a-zA-Z_][a-zA-Z0-9_]{0,29})+#\}\}/gi
const REGEX = /\{\{(#[a-zA-Z0-9_]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi
const WorkflowVariableBlockReplacementBlock = ({
getWorkflowNode = () => undefined,

View File

@@ -1,16 +1,77 @@
import {
memo,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
getIncomers,
getOutgoers,
useEdges,
useNodes,
} from 'reactflow'
import BlockIcon from '../block-icon'
import {
useNodesExtraData,
useNodesInteractions,
} from '../hooks'
import type { CommonNodeType } from '../types'
import { BlockEnum } from '../types'
import { useStore } from '../store'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Checklist } from '@/app/components/base/icons/src/vender/line/general'
import {
Checklist,
ChecklistSquare,
XClose,
} from '@/app/components/base/icons/src/vender/line/general'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
const WorkflowChecklist = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const nodes = useNodes<CommonNodeType>()
const edges = useEdges()
const nodesExtraData = useNodesExtraData()
const { handleNodeSelect } = useNodesInteractions()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const needWarningNodes = useMemo(() => {
const list = []
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
const incomers = getIncomers(node, nodes, edges)
const outgoers = getOutgoers(node, nodes, edges)
const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t)
let toolIcon
if (node.data.type === BlockEnum.Tool) {
if (node.data.provider_type === 'builtin')
toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon
if (node.data.provider_type === 'custom')
toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon
}
if (errorMessage || ((!incomers.length && !outgoers.length))) {
list.push({
id: node.id,
type: node.data.type,
title: node.data.title,
toolIcon,
unConnected: !incomers.length && !outgoers.length,
errorMessage,
})
}
}
return list
}, [t, nodes, edges, nodesExtraData, buildInTools, customTools])
return (
<PortalToFollowElem
@@ -23,7 +84,7 @@ const WorkflowChecklist = () => {
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className='flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs'>
<div className='relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs'>
<div
className={`
group flex items-center justify-center w-full h-full rounded-md cursor-pointer
@@ -38,9 +99,90 @@ const WorkflowChecklist = () => {
}
/>
</div>
{
!!needWarningNodes.length && (
<div className='absolute -right-1.5 -top-1.5 flex items-center justify-center min-w-[18px] h-[18px] rounded-full border border-gray-100 text-white text-[11px] font-semibold bg-[#F79009]'>
{needWarningNodes.length}
</div>
)
}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent></PortalToFollowElemContent>
<PortalToFollowElemContent className='z-[12]'>
<div
className='w-[420px] rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg overflow-y-auto'
style={{
maxHeight: 'calc(2 / 3 * 100vh)',
}}
>
<div className='sticky top-0 bg-white flex items-center pl-4 pr-3 pt-3 h-[44px] text-md font-semibold text-gray-900 z-[1]'>
<div className='grow'>{t('workflow.panel.checklist')}{needWarningNodes.length ? `(${needWarningNodes.length})` : ''}</div>
<div
className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={() => setOpen(false)}
>
<XClose className='w-4 h-4 text-gray-500' />
</div>
</div>
<div className='py-2'>
{
!!needWarningNodes.length && (
<>
<div className='px-4 text-xs text-gray-400'>{t('workflow.panel.checklistTip')}</div>
<div className='px-4 py-2'>
{
needWarningNodes.map(node => (
<div
key={node.id}
className='mb-2 last-of-type:mb-0 border-[0.5px] border-gray-200 bg-white shadow-xs rounded-lg cursor-pointer'
onClick={() => handleNodeSelect(node.id)}
>
<div className='flex items-center p-2 h-9 text-xs font-medium text-gray-700'>
<BlockIcon
type={node.type}
className='mr-1.5'
toolIcon={node.toolIcon}
/>
{node.title}
</div>
{
node.unConnected && (
<div className='px-3 py-2 border-t-[0.5px] border-t-black/[0.02] bg-gray-25 rounded-b-lg'>
<div className='flex text-xs leading-[18px] text-gray-500'>
<AlertTriangle className='mt-[3px] mr-2 w-3 h-3 text-[#F79009]' />
{t('workflow.common.needConnecttip')}
</div>
</div>
)
}
{
node.errorMessage && (
<div className='px-3 py-2 border-t-[0.5px] border-t-black/[0.02] bg-gray-25 rounded-b-lg'>
<div className='flex text-xs leading-[18px] text-gray-500'>
<AlertTriangle className='mt-[3px] mr-2 w-3 h-3 text-[#F79009]' />
{node.errorMessage}
</div>
</div>
)
}
</div>
))
}
</div>
</>
)
}
{
!needWarningNodes.length && (
<div className='mx-4 mb-3 py-4 rounded-lg bg-gray-50 text-gray-400 text-center'>
<ChecklistSquare className='mx-auto mb-[5px] w-8 h-8 text-gray-300' />
{t('workflow.panel.checklistResolved')}
</div>
)
}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

View File

@@ -119,7 +119,14 @@ const Header: FC = () => {
{t('workflow.common.features')}
</Button>
<Publish />
<Checklist />
{
!nodesReadOnly && (
<>
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Checklist />
</>
)
}
</div>
)
}
@@ -150,7 +157,6 @@ const Header: FC = () => {
>
{t('workflow.common.restore')}
</Button>
<Checklist />
</div>
)
}

View File

@@ -19,7 +19,6 @@ import TooltipPlus from '@/app/components/base/tooltip-plus'
type Props = {
title: string | JSX.Element
value: string
variables: string[]
onChange: (value: string) => void
readOnly?: boolean
showRemove?: boolean
@@ -78,8 +77,8 @@ const Editor: FC<Props> = ({
return (
<div className={cn(wrapClassName)}>
<div ref={ref} className={cn(isFocus && s.gradientBorder, isExpand && 'h-full', '!rounded-[9px]')}>
<div className={cn(isFocus ? 'bg-white' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
<div ref={ref} className={cn(isFocus ? s.gradientBorder : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5')}>
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
<div className='pt-1 pl-3 pr-2 flex justify-between h-6 items-center'>
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
<div className='flex items-center'>

View File

@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import React, { useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { Method } from '../types'
@@ -38,7 +38,6 @@ const ApiInput: FC<Props> = ({
}) => {
const { t } = useTranslation()
const inputRef = useRef<HTMLInputElement>(null)
const [isFocus, setIsFocus] = useState(false)
const availableVarList = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
@@ -47,10 +46,6 @@ const ApiInput: FC<Props> = ({
},
})
useEffect(() => {
if (isFocus)
inputRef.current?.focus()
}, [isFocus])
return (
<div className='flex items-start space-x-1'>
<Selector

View File

@@ -8,11 +8,14 @@ import { BodyType } from '../../types'
import useKeyValueList from '../../hooks/use-key-value-list'
import KeyValue from '../key-value'
import TextEditor from '../../../_base/components/editor/text-editor'
import CodeEditor from '../../../_base/components/editor/code-editor'
import { CodeLanguage } from '../../../code/types'
import useAvailableVarList from '../../../_base/hooks/use-available-var-list'
import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
type Props = {
readonly: boolean
nodeId: string
payload: Body
onChange: (payload: Body) => void
}
@@ -34,10 +37,17 @@ const bodyTextMap = {
const EditBody: FC<Props> = ({
readonly,
nodeId,
payload,
onChange,
}) => {
const { type } = payload
const availableVarList = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number].includes(varPayload.type)
},
})
const handleTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newType = e.target.value as BodyType
@@ -54,8 +64,6 @@ const EditBody: FC<Props> = ({
list: body,
setList: setBody,
addItem: addBody,
isKeyValueEdit: isBodyKeyValueEdit,
toggleIsKeyValueEdit: toggleIsBodyKeyValueEdit,
} = useKeyValueList(payload.data, (value) => {
const newBody = produce(payload, (draft: Body) => {
draft.data = value
@@ -111,11 +119,10 @@ const EditBody: FC<Props> = ({
{(type === BodyType.formData || type === BodyType.xWwwFormUrlencoded) && (
<KeyValue
readonly={readonly}
nodeId={nodeId}
list={body}
onChange={setBody}
onAdd={addBody}
isKeyValueEdit={isBodyKeyValueEdit}
toggleKeyValueEdit={toggleIsBodyKeyValueEdit}
/>
)}
@@ -130,11 +137,19 @@ const EditBody: FC<Props> = ({
)}
{type === BodyType.json && (
<CodeEditor
// <CodeEditor
// readOnly={readonly}
// title={<div className='uppercase'>JSON</div>}
// value={payload.data} onChange={handleBodyValueChange}
// language={CodeLanguage.json}
// />
<InputWithVar
title='JSON'
value={payload.data}
onChange={handleBodyValueChange}
justVar
nodesOutputVars={availableVarList}
readOnly={readonly}
title={<div className='uppercase'>JSON</div>}
value={payload.data} onChange={handleBodyValueChange}
language={CodeLanguage.json}
/>
)}
</div>

View File

@@ -1,65 +1,59 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import React from 'react'
import type { KeyValue } from '../../types'
import KeyValueEdit from './key-value-edit'
import BulkEdit from './bulk-edit'
type Props = {
readonly: boolean
nodeId: string
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
isKeyValueEdit: boolean
toggleKeyValueEdit: () => void
// toggleKeyValueEdit: () => void
}
const KeyValueList: FC<Props> = ({
readonly,
nodeId,
list,
onChange,
onAdd,
isKeyValueEdit,
toggleKeyValueEdit,
// toggleKeyValueEdit,
}) => {
const handleBulkValueChange = useCallback((value: string) => {
const newList = value.split('\n').map((item) => {
const [key, value] = item.split(':')
return {
key: key ? key.trim() : '',
value: value ? value.trim() : '',
}
})
onChange(newList)
}, [onChange])
// const handleBulkValueChange = useCallback((value: string) => {
// const newList = value.split('\n').map((item) => {
// const [key, value] = item.split(':')
// return {
// key: key ? key.trim() : '',
// value: value ? value.trim() : '',
// }
// })
// onChange(newList)
// }, [onChange])
const bulkList = (() => {
const res = list.map((item) => {
if (!item.key && !item.value)
return ''
if (!item.value)
return item.key
return `${item.key}:${item.value}`
}).join('\n')
return res
})()
return (
<>
{isKeyValueEdit
? <KeyValueEdit
readonly={readonly}
list={list}
onChange={onChange}
onAdd={onAdd}
onSwitchToBulkEdit={toggleKeyValueEdit}
/>
: <BulkEdit
value={bulkList}
onChange={handleBulkValueChange}
onSwitchToKeyValueEdit={toggleKeyValueEdit}
/>
}
</>
)
// const bulkList = (() => {
// const res = list.map((item) => {
// if (!item.key && !item.value)
// return ''
// if (!item.value)
// return item.key
// return `${item.key}:${item.value}`
// }).join('\n')
// return res
// })()
return <KeyValueEdit
readonly={readonly}
nodeId={nodeId}
list={list}
onChange={onChange}
onAdd={onAdd}
// onSwitchToBulkEdit={toggleKeyValueEdit}
/>
// : <BulkEdit
// value={bulkList}
// onChange={handleBulkValueChange}
// onSwitchToKeyValueEdit={toggleKeyValueEdit}
// />
}
export default React.memo(KeyValueList)

View File

@@ -5,25 +5,27 @@ import produce from 'immer'
import { useTranslation } from 'react-i18next'
import type { KeyValue } from '../../../types'
import KeyValueItem from './item'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
// import TooltipPlus from '@/app/components/base/tooltip-plus'
// import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
readonly: boolean
nodeId: string
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
onSwitchToBulkEdit: () => void
// onSwitchToBulkEdit: () => void
}
const KeyValueList: FC<Props> = ({
readonly,
nodeId,
list,
onChange,
onAdd,
onSwitchToBulkEdit,
// onSwitchToBulkEdit,
}) => {
const { t } = useTranslation()
@@ -51,7 +53,7 @@ const KeyValueList: FC<Props> = ({
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>{t(`${i18nPrefix}.key`)}</div>
<div className='flex w-1/2 h-full pl-3 pr-1 items-center justify-between'>
<div>{t(`${i18nPrefix}.value`)}</div>
{!readonly && (
{/* {!readonly && (
<TooltipPlus
popupContent={t(`${i18nPrefix}.bulkEdit`)}
>
@@ -61,13 +63,14 @@ const KeyValueList: FC<Props> = ({
>
<EditList className='w-3 h-3' />
</div>
</TooltipPlus>)}
</TooltipPlus>)} */}
</div>
</div>
{
list.map((item, index) => (
<KeyValueItem
key={index}
nodeId={nodeId}
payload={item}
onChange={handleChange(index)}
onRemove={handleRemove(index)}

View File

@@ -1,13 +1,16 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useBoolean } from 'ahooks'
import React, { useCallback, useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import useAvailableVarList from '../../../../_base/hooks/use-available-var-list'
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
import SupportVarInput from '@/app/components/workflow/nodes/_base/components/support-var-input'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
type Props = {
className?: string
nodeId: string
value: string
onChange: (newValue: string) => void
hasRemove: boolean
@@ -18,6 +21,7 @@ type Props = {
const InputItem: FC<Props> = ({
className,
nodeId,
value,
onChange,
hasRemove,
@@ -25,15 +29,17 @@ const InputItem: FC<Props> = ({
placeholder,
readOnly,
}) => {
const hasValue = !!value
const [isEdit, {
setTrue: setIsEditTrue,
setFalse: setIsEditFalse,
}] = useBoolean(false)
const { t } = useTranslation()
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
}, [onChange])
const hasValue = !!value
const [isFocus, setIsFocus] = useState(false)
const availableVarList = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number].includes(varPayload.type)
},
})
const handleRemove = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
@@ -41,34 +47,47 @@ const InputItem: FC<Props> = ({
}, [onRemove])
return (
<div className={cn(className, !isEdit && 'hover:bg-gray-50 hover:cursor-text', 'relative flex h-full items-center pl-2')}>
{(isEdit && !readOnly)
<div className={cn(className, 'hover:bg-gray-50 hover:cursor-text', 'relative flex h-full items-center')}>
{(!readOnly)
? (
<input
type='text'
className='w-full h-[18px] leading-[18px] pl-0.5 text-gray-900 text-xs font-normal placeholder:text-gray-300 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
// <input
// type='text'
// className='w-full h-[18px] leading-[18px] pl-0.5 text-gray-900 text-xs font-normal placeholder:text-gray-300 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
// value={value}
// onChange={handleChange}
// onBlur={setIsEditFalse}
// autoFocus
// placeholder={placeholder}
// readOnly={readOnly}
// />
<Input
className={cn(isFocus ? 'bg-gray-100' : 'bg-width', 'w-0 grow px-3 py-1')}
value={value}
onChange={handleChange}
onBlur={setIsEditFalse}
autoFocus
placeholder={placeholder}
onChange={onChange}
readOnly={readOnly}
nodesOutputVars={availableVarList}
onFocusChange={setIsFocus}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)
: <div
className="pl-0.5 w-full h-[18px] leading-[18px]"
onClick={setIsEditTrue}
>
{!hasValue && <div className='text-gray-300 text-xs font-normal'>{placeholder}</div>}
{hasValue && (
<SupportVarInput
wrapClassName='w-0 grow truncate flex items-center'
textClassName='text-gray-900 text-xs font-normal'
<Input
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
value={value}
readonly
onChange={onChange}
readOnly={readOnly}
nodesOutputVars={availableVarList}
onFocusChange={setIsFocus}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
{hasRemove && !isEdit && (
{hasRemove && !isFocus && (
<RemoveButton
className='group-hover:block hidden absolute right-1 top-0.5'
onClick={handleRemove}

View File

@@ -11,6 +11,7 @@ const i18nPrefix = 'workflow.nodes.http'
type Props = {
className?: string
nodeId: string
readonly: boolean
canRemove: boolean
payload: KeyValue
@@ -22,6 +23,7 @@ type Props = {
const KeyValueItem: FC<Props> = ({
className,
nodeId,
readonly,
canRemove,
payload,
@@ -45,10 +47,10 @@ const KeyValueItem: FC<Props> = ({
return (
// group class name is for hover row show remove button
<div className={cn(className, 'group flex items-center h-7 border-t border-gray-200')}>
<div className={cn(className, 'group flex items-start h-min-7 border-t border-gray-200')}>
<div className='w-1/2 h-full border-r border-gray-200'>
<InputItem
className='pr-2.5'
nodeId={nodeId}
value={payload.key}
onChange={handleChange('key')}
hasRemove={false}
@@ -58,7 +60,7 @@ const KeyValueItem: FC<Props> = ({
</div>
<div className='w-1/2 h-full'>
<InputItem
className='pr-1'
nodeId={nodeId}
value={payload.value}
onChange={handleChange('value')}
hasRemove={!readonly && canRemove}

View File

@@ -8,9 +8,7 @@ import KeyValue from './components/key-value'
import EditBody from './components/edit-body'
import AuthorizationModal from './components/authorization'
import type { HttpNodeType } from './types'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import AddButton from '@/app/components/base/button/add-button'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
@@ -29,21 +27,14 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
const {
readOnly,
inputs,
handleVarListChange,
handleAddVariable,
filterVar,
handleMethodChange,
handleUrlChange,
headers,
setHeaders,
addHeader,
isHeaderKeyValueEdit,
toggleIsHeaderKeyValueEdit,
params,
setParams,
addParam,
isParamKeyValueEdit,
toggleIsParamKeyValueEdit,
setBody,
isShowAuthorization,
showAuthorization,
@@ -64,20 +55,6 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.inputVars`)}
operations={
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
}
>
<VarList
nodeId={id}
readonly={readOnly}
list={inputs.variables}
onChange={handleVarListChange}
filterVar={filterVar}
/>
</Field>
<Field
title={t(`${i18nPrefix}.api`)}
operations={
@@ -106,30 +83,29 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
title={t(`${i18nPrefix}.headers`)}
>
<KeyValue
nodeId={id}
list={headers}
onChange={setHeaders}
onAdd={addHeader}
readonly={readOnly}
isKeyValueEdit={isHeaderKeyValueEdit}
toggleKeyValueEdit={toggleIsHeaderKeyValueEdit}
/>
</Field>
<Field
title={t(`${i18nPrefix}.params`)}
>
<KeyValue
nodeId={id}
list={params}
onChange={setParams}
onAdd={addParam}
readonly={readOnly}
isKeyValueEdit={isParamKeyValueEdit}
toggleKeyValueEdit={toggleIsParamKeyValueEdit}
/>
</Field>
<Field
title={t(`${i18nPrefix}.body`)}
>
<EditBody
nodeId={id}
readonly={readOnly}
payload={inputs.body}
onChange={setBody}

View File

@@ -8,7 +8,6 @@ import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
import { useStore } from '../store'
import { useIsChatMode } from '../hooks'
import WorkflowInfo from './workflow-info'
import DebugAndPreview from './debug-and-preview'
import RunHistory from './run-history'
import Record from './record'
@@ -28,13 +27,11 @@ const Panel: FC = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal } = useAppStore()
const {
showWorkflowInfoPanel,
showNodePanel,
showDebugAndPreviewPanel,
showWorkflowPreview,
} = useMemo(() => {
return {
showWorkflowInfoPanel: !selectedNode && !workflowRunningData && !historyWorkflowData,
showNodePanel: !!selectedNode && !workflowRunningData && !historyWorkflowData,
showDebugAndPreviewPanel: isChatMode && workflowRunningData && !historyWorkflowData,
showWorkflowPreview: !isChatMode && workflowRunningData && !historyWorkflowData,
@@ -91,11 +88,6 @@ const Panel: FC = () => {
<NodePanel {...selectedNode!} />
)
}
{
showWorkflowInfoPanel && (
<WorkflowInfo />
)
}
{
showRunHistory && (
<RunHistory />

View File

@@ -1,143 +0,0 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
getIncomers,
getOutgoers,
useEdges,
useNodes,
} from 'reactflow'
import BlockIcon from '../block-icon'
import { useNodesExtraData } from '../hooks'
import type { CommonNodeType } from '../types'
import { BlockEnum } from '../types'
import { useStore } from '../store'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import { FileCheck02 } from '@/app/components/base/icons/src/vender/line/files'
import { useStore as useAppStore } from '@/app/components/app/store'
import AppIcon from '@/app/components/base/app-icon'
const WorkflowInfo = () => {
const { t } = useTranslation()
const appDetail = useAppStore(state => state.appDetail)
const nodes = useNodes<CommonNodeType>()
const edges = useEdges()
const nodesExtraData = useNodesExtraData()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const needConnectNodes = nodes.filter((node) => {
const incomers = getIncomers(node, nodes, edges)
const outgoers = getOutgoers(node, nodes, edges)
return !incomers.length && !outgoers.length
})
const needWarningNodes = useMemo(() => {
const list = []
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
const incomers = getIncomers(node, nodes, edges)
const outgoers = getOutgoers(node, nodes, edges)
const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t)
let toolIcon
if (node.data.type === BlockEnum.Tool) {
if (node.data.provider_type === 'builtin')
toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon
if (node.data.provider_type === 'custom')
toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon
}
if (errorMessage || ((!incomers.length && !outgoers.length))) {
list.push({
id: node.id,
type: node.data.type,
title: node.data.title,
toolIcon,
unConnected: !incomers.length && !outgoers.length,
errorMessage,
})
}
}
return list
}, [t, nodes, edges, nodesExtraData, buildInTools, customTools])
if (!appDetail)
return null
return (
<div className='w-[420px] h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl overflow-y-auto'>
<div className='sticky top-0 bg-white border-b-[0.5px] border-black/5'>
<div className='flex pt-4 px-4 pb-1'>
<AppIcon
className='mr-3'
size='large'
icon={appDetail.icon}
background={appDetail.icon_background}
/>
<div className='mt-2 text-base font-semibold text-gray-900'>
{appDetail.name}
</div>
</div>
<div className='px-4 py-[13px] text-xs leading-[18px] text-gray-500'>
{appDetail.description}
</div>
<div className='flex items-center px-4 h-[42px] text-[13px] font-semibold text-gray-700'>
<FileCheck02 className='mr-1 w-4 h-4' />
{t('workflow.panel.checklist')}({needConnectNodes.length})
</div>
</div>
<div className='py-2'>
<div className='px-4 py-2 text-xs text-gray-400'>
{t('workflow.panel.checklistTip')}
</div>
<div className='px-4 py-2'>
{
needWarningNodes.map(node => (
<div
key={node.id}
className='mb-2 border-[0.5px] border-gray-200 bg-white shadow-xs rounded-lg'
>
<div className='flex items-center p-2 h-9 text-xs font-medium text-gray-700'>
<BlockIcon
type={node.type}
className='mr-1.5'
toolIcon={node.toolIcon}
/>
{node.title}
</div>
{
node.unConnected && (
<div className='px-3 py-2 border-t-[0.5px] border-t-black/[0.02] bg-gray-25 rounded-b-lg'>
<div className='flex text-xs leading-[18px] text-gray-500'>
<AlertTriangle className='mt-[3px] mr-2 w-3 h-3 text-[#F79009]' />
{t('workflow.common.needConnecttip')}
</div>
</div>
)
}
{
node.errorMessage && (
<div className='px-3 py-2 border-t-[0.5px] border-t-black/[0.02] bg-gray-25 rounded-b-lg'>
<div className='flex text-xs leading-[18px] text-gray-500'>
<AlertTriangle className='mt-[3px] mr-2 w-3 h-3 text-[#F79009]' />
{node.errorMessage}
</div>
</div>
)
}
</div>
))
}
</div>
</div>
</div>
)
}
export default memo(WorkflowInfo)

View File

@@ -110,6 +110,7 @@ const translation = {
runThisStep: 'Run this step',
checklist: 'Checklist',
checklistTip: 'Make sure all issues are resolved before publishing',
checklistResolved: 'All issues are resolved',
organizeBlocks: 'Organize blocks',
change: 'Change',
},
@@ -231,6 +232,7 @@ const translation = {
'api-key-title': 'API Key',
'header': 'Header',
},
insertVarPlaceholder: 'type \'/\' to insert variable',
},
code: {
inputVars: 'Input Variables',

View File

@@ -110,6 +110,7 @@ const translation = {
runThisStep: '运行此步骤',
checklist: '检查清单',
checklistTip: '发布前确保所有问题均已解决',
checklistResolved: '所有问题均已解决',
organizeBlocks: '整理节点',
change: '更改',
},
@@ -231,6 +232,7 @@ const translation = {
'api-key-title': 'API Key',
'header': 'Header',
},
insertVarPlaceholder: '键入 \'/\' 键快速插入变量',
},
code: {
inputVars: '输入变量',