Compare commits

...

60 Commits

Author SHA1 Message Date
JzoNg f03afaf052 Merge branch 'feat/change-user-email-freezes-limit' into deploy/dev 7 months ago
JzoNg 30393043b1 add error handle for email unavailable 7 months ago
JzoNg ae49a2bdd9 add error message of email unavailable 7 months ago
Yansong Zhang c271c65c85 fix ruff liner 7 months ago
Yansong Zhang a1de4fa428 check is email is freeze user email 7 months ago
Yansong Zhang 952bce4196 check is email is freeze user email 7 months ago
NFish a2172117a5 Merge branch 'fix/web-app-support-session-in-url' into deploy/dev 7 months ago
NFish 24fdf10d1d fix: support authorization using session and user_id in URL. 7 months ago
stream 75303d763d fix: add missing param 7 months ago
stream ed27b67422 refactor: better template for instruction generation 7 months ago
stream 4f4473e3d0 chore: run ruff 7 months ago
stream 0ee992733c feat: template for instruction generation 7 months ago
stream c3940f1eed fix: null handling 7 months ago
stream 796dc65813 fix: null handling 7 months ago
stream 1523e1db74 fix: error handling 7 months ago
stream a3074aa0a5 chore: run ruff 7 months ago
stream 35e40b5769 feat: improved capability with parameters of InstructionGenerateApi 7 months ago
stream 807a6890ac feat: implement InstructionGenerateApi 7 months ago
Joel d6af356248 Merge branch 'feat/enchance-prompt-and-code-fe' into deploy/dev 7 months ago
Joel 7ead579335 chore: use markdown render notice 7 months ago
JzoNg 613f39c96a Merge branch 'feat/change-email-completed-notification' into deploy/dev 7 months ago
QuantumGhost b6d55173cb feat(api): Initial support for `boolean` / `array[boolean]` types 7 months ago
KVOJJJin c4f0f3e5e0
Merge branch 'main' into feat/change-email-completed-notification 7 months ago
Joel ae50d3e22f chore: add markdown render tip 7 months ago
Yansong Zhang 4a417ff52b fix ruff linter 7 months ago
JzoNg c4b899ed60 add notification for change email completed 7 months ago
Joel 91b0672001 fix: can overwrite the gened prompt 7 months ago
Joel 0066cfe4ed chore: remove agent can show prompt gen 7 months ago
Joel a44a200245 fix: can update versions 7 months ago
Joel 5ad4465c9c fix: can not gen in second time 7 months ago
Joel c78130990d chore: text 7 months ago
Joel 07288bda8b feat: bottom to show vars 7 months ago
Joel ab45f3ce26 feat: instruction template 7 months ago
Joel 71d08d0580 feat: handle is show current prompt 7 months ago
Joel 534ece0ad0 chore: gen template 7 months ago
Joel f438af1aa1 fix: cannot choose no value vaiable 7 months ago
Joel 4e27d2c35f fix: can set the template value 7 months ago
Joel 525e64467d feat: prompt res 7 months ago
Joel 98c70ff86d chore: no data placeholder 7 months ago
Joel 219c96aee1 feat: code gen result highlight 7 months ago
Joel df95b6eba0 chore: remove debug code 7 months ago
Joel f60792bcc7 feat: can generate code 7 months ago
Joel ba8129cf73 feat: code generate use prompt editor 7 months ago
Joel 635e92d762 chore: copy 7 months ago
Joel e2512b0af9 fix: can not choose error message in code gen 7 months ago
Joel a5653f253a chore: version show 7 months ago
Joel ed737beb4a chore: can set to value 7 months ago
Joel a85908386e feat: version select picker and versions storage 7 months ago
Joel 677716346a chore: change currentprompt 7 months ago
Joel f14678a0d8 feat: call gen api 7 months ago
Joel 945a424fa8 feat: new result 7 months ago
Joel 424a563055 chore: i18n 7 months ago
Joel d82abbef23 chore: 18n 7 months ago
Joel 7907235124 feat: gener instrument left 7 months ago
Joel 5d232ac1bc chore: last run divide 7 months ago
Joel 4c76a5f57b feat: support choose last run 7 months ago
Joel 3160e5e562 feat: add error message 7 months ago
Joel da560e5950 chore: current ui fix 7 months ago
Joel 6075ca5f59 feat: can choose current and show current 7 months ago
Joel 7d80cb6d95 feat: current block 7 months ago

@ -0,0 +1,11 @@
from tests.integration_tests.utils.parent_class import ParentClass
class ChildClass(ParentClass):
"""Test child class for module import helper tests"""
def __init__(self, name):
super().__init__(name)
def get_name(self):
return f"Child: {self.name}"

@ -1,4 +1,5 @@
import os import os
from collections.abc import Sequence
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
@ -114,6 +115,108 @@ class RuleStructuredOutputGenerateApi(Resource):
return structured_output return structured_output
class InstructionGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("flow_id", type=str, required=True, default="", location="json")
parser.add_argument("node_id", type=str, required=False, default="", location="json")
parser.add_argument("current", type=str, required=False, default="", location="json")
parser.add_argument("language", type=str, required=False, default="javascript", location="json")
parser.add_argument("instruction", type=str, required=True, nullable=False, location="json")
parser.add_argument("model_config", type=dict, required=True, nullable=False, location="json")
parser.add_argument("ideal_output", type=str, required=False, default="", location="json")
args = parser.parse_args()
try:
if args["current"] == "" and args["node_id"] != "": # Generate from nothing for a workflow node
from models import App, db
from services.workflow_service import WorkflowService
app = db.session.query(App).filter(App.id == args["flow_id"]).first()
if not app:
return { "error": f"app {args['flow_id']} not found" }, 400
workflow = WorkflowService().get_draft_workflow(app_model=app)
nodes:Sequence = workflow.graph_dict["nodes"]
node = [node for node in nodes if node["id"] == args["node_id"]]
if len(node) == 0:
return { "error": f"node {args['node_id']} not found" }, 400
node_type=node[0]["data"]["type"]
match node_type:
case "llm":
return LLMGenerator.generate_rule_config(
current_user.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
no_variable=True
)
case "agent":
return LLMGenerator.generate_rule_config(
current_user.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
no_variable=True
)
case "code":
return LLMGenerator.generate_code(
tenant_id=current_user.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
code_language=args["language"],
)
case _:
return { "error": f"invalid node type: {node_type}"}
if args["node_id"] == "" and args["current"] != "": # For legacy app without a workflow
return LLMGenerator.instruction_modify_legacy(
tenant_id=current_user.current_tenant_id,
flow_id=args["flow_id"],
current=args["current"],
instruction=args["instruction"],
model_config=args["model_config"],
ideal_output=args["ideal_output"],
)
if args["node_id"] != "" and args["current"] != "": # For workflow node
return LLMGenerator.instruction_modify_workflow(
tenant_id=current_user.current_tenant_id,
flow_id=args["flow_id"],
node_id=args["node_id"],
current=args["current"],
instruction=args["instruction"],
model_config=args["model_config"],
ideal_output=args["ideal_output"],
)
return { "error": "incompatible parameters" }, 400
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e:
raise CompletionRequestError(e.description)
class InstructionGenerationTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self) -> dict:
parser = reqparse.RequestParser()
parser.add_argument("type", type=str, required=True, default=False, location="json")
args = parser.parse_args()
match args["type"]:
case "prompt":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
return { "data": INSTRUCTION_GENERATE_TEMPLATE_PROMPT }
case "code":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_CODE
return { "data": INSTRUCTION_GENERATE_TEMPLATE_CODE }
case _:
raise ValueError(f"Invalid type: {args['type']}")
api.add_resource(RuleGenerateApi, "/rule-generate") api.add_resource(RuleGenerateApi, "/rule-generate")
api.add_resource(RuleCodeGenerateApi, "/rule-code-generate") api.add_resource(RuleCodeGenerateApi, "/rule-code-generate")
api.add_resource(RuleStructuredOutputGenerateApi, "/rule-structured-output-generate") api.add_resource(RuleStructuredOutputGenerateApi, "/rule-structured-output-generate")
api.add_resource(InstructionGenerateApi, "/instruction-generate")
api.add_resource(InstructionGenerationTemplateApi, "/instruction-generate/template")

@ -113,3 +113,9 @@ class MemberNotInTenantError(BaseHTTPException):
error_code = "member_not_in_tenant" error_code = "member_not_in_tenant"
description = "The member is not in the workspace." description = "The member is not in the workspace."
code = 400 code = 400
class AccountInFreezeError(BaseHTTPException):
error_code = "account_in_freeze"
description = "This email is temporarily unavailable."
code = 400

@ -9,6 +9,7 @@ from configs import dify_config
from constants.languages import supported_language from constants.languages import supported_language
from controllers.console import api from controllers.console import api
from controllers.console.auth.error import ( from controllers.console.auth.error import (
AccountInFreezeError,
EmailAlreadyInUseError, EmailAlreadyInUseError,
EmailChangeLimitError, EmailChangeLimitError,
EmailCodeError, EmailCodeError,
@ -479,15 +480,18 @@ class ChangeEmailResetApi(Resource):
parser.add_argument("token", type=str, required=True, nullable=False, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json")
args = parser.parse_args() args = parser.parse_args()
if AccountService.is_account_in_freeze(args["new_email"]):
raise AccountInFreezeError()
if not AccountService.check_email_unique(args["new_email"]):
raise EmailAlreadyInUseError()
reset_data = AccountService.get_change_email_data(args["token"]) reset_data = AccountService.get_change_email_data(args["token"])
if not reset_data: if not reset_data:
raise InvalidTokenError() raise InvalidTokenError()
AccountService.revoke_change_email_token(args["token"]) AccountService.revoke_change_email_token(args["token"])
if not AccountService.check_email_unique(args["new_email"]):
raise EmailAlreadyInUseError()
old_email = reset_data.get("old_email", "") old_email = reset_data.get("old_email", "")
if current_user.email != old_email: if current_user.email != old_email:
raise AccountNotFound() raise AccountNotFound()
@ -507,6 +511,8 @@ class CheckEmailUnique(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")
args = parser.parse_args() args = parser.parse_args()
if AccountService.is_account_in_freeze(args["new_email"]):
raise AccountInFreezeError()
if not AccountService.check_email_unique(args["email"]): if not AccountService.check_email_unique(args["email"]):
raise EmailAlreadyInUseError() raise EmailAlreadyInUseError()
return {"result": "success"} return {"result": "success"}

@ -1,6 +1,7 @@
import json import json
import logging import logging
import re import re
from collections.abc import Sequence
from typing import Optional, cast from typing import Optional, cast
import json_repair import json_repair
@ -11,6 +12,8 @@ from core.llm_generator.prompts import (
CONVERSATION_TITLE_PROMPT, CONVERSATION_TITLE_PROMPT,
GENERATOR_QA_PROMPT, GENERATOR_QA_PROMPT,
JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE, JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE,
LLM_MODIFY_CODE_SYSTEM,
LLM_MODIFY_PROMPT_SYSTEM,
PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE, PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE,
SYSTEM_STRUCTURED_OUTPUT_GENERATE, SYSTEM_STRUCTURED_OUTPUT_GENERATE,
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE, WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
@ -24,6 +27,9 @@ from core.ops.entities.trace_entity import TraceTaskName
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask from core.ops.ops_trace_manager import TraceQueueManager, TraceTask
from core.ops.utils import measure_time from core.ops.utils import measure_time
from core.prompt.utils.prompt_template_parser import PromptTemplateParser from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
from core.workflow.graph_engine.entities.event import AgentLogEvent
from models import App, Message, WorkflowNodeExecutionModel, db
class LLMGenerator: class LLMGenerator:
@ -397,3 +403,177 @@ class LLMGenerator:
except Exception as e: except Exception as e:
logging.exception(f"Failed to invoke LLM model, model: {model_config.get('name')}") logging.exception(f"Failed to invoke LLM model, model: {model_config.get('name')}")
return {"output": "", "error": f"An unexpected error occurred: {str(e)}"} return {"output": "", "error": f"An unexpected error occurred: {str(e)}"}
@staticmethod
def instruction_modify_legacy(
tenant_id: str,
flow_id: str,
current: str,
instruction: str,
model_config: dict,
ideal_output: str | None
) -> dict:
app: App = db.session.query(App).filter(App.id == flow_id).first()
last_run: Message = (db.session.query(Message)
.filter(Message.app_id == flow_id)
.order_by(Message.created_at.desc())
.first())
if not last_run:
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=None,
current=current,
error_message="",
instruction=instruction,
node_type="llm",
ideal_output=ideal_output
)
last_run_dict = {
"query": last_run.query,
"answer": last_run.answer,
"error": last_run.error,
}
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=last_run_dict,
current=current,
error_message=str(last_run.error),
instruction=instruction,
node_type="llm",
ideal_output=ideal_output
)
@staticmethod
def instruction_modify_workflow(
tenant_id: str,
flow_id: str,
node_id: str,
current: str,
instruction: str,
model_config: dict,
ideal_output: str | None
) -> dict:
from services.workflow_service import WorkflowService
app: App = db.session.query(App).filter(App.id == flow_id).first()
workflow = WorkflowService().get_draft_workflow(app_model=app)
if not workflow:
raise ValueError("Workflow not found for the given app model.")
last_run = WorkflowService().get_node_last_run(
app_model=app,
workflow=workflow,
node_id=node_id
)
if not last_run: # Node is not executed yet
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=None,
current=current,
error_message="",
instruction=instruction,
node_type="llm",
ideal_output=ideal_output
)
def agent_log_of(node_execution: WorkflowNodeExecutionModel) -> Sequence:
raw_agent_log = node_execution.execution_metadata_dict.get(WorkflowNodeExecutionMetadataKey.AGENT_LOG)
if not raw_agent_log:
return []
parsed: Sequence[AgentLogEvent] = json.loads(raw_agent_log)
def dict_of_event(event: AgentLogEvent) -> dict:
return {
"status": event.status,
"error": event.error,
"data": event.data,
}
return [dict_of_event(event) for event in parsed]
last_run_dict = {
"inputs": last_run.inputs_dict,
"status": last_run.status,
"error": last_run.error,
"agent_log": agent_log_of(last_run)
}
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=last_run_dict,
current=current,
error_message=last_run.error,
instruction=instruction,
node_type=last_run.node_type,
ideal_output=ideal_output
)
@staticmethod
def __instruction_modify_common(
tenant_id: str,
model_config: dict,
last_run: dict | None,
current: str | None,
error_message: str | None,
instruction: str,
node_type: str,
ideal_output: str | None
) -> dict:
LAST_RUN = "{{#last_run#}}"
CURRENT = "{{#current#}}"
ERROR_MESSAGE = "{{#error_message#}}"
injected_instruction = instruction
if LAST_RUN in injected_instruction:
injected_instruction = injected_instruction.replace(LAST_RUN, json.dumps(last_run))
if CURRENT in injected_instruction:
injected_instruction = injected_instruction.replace(CURRENT, current or "null")
if ERROR_MESSAGE in injected_instruction:
injected_instruction = injected_instruction.replace(ERROR_MESSAGE, error_message or "null")
model_instance = ModelManager().get_model_instance(
tenant_id = tenant_id,
model_type=ModelType.LLM,
provider=model_config.get("provider", ""),
model=model_config.get("name", ""),
)
match node_type:
case "llm", "agent":
system_prompt = LLM_MODIFY_PROMPT_SYSTEM
case "code":
system_prompt = LLM_MODIFY_CODE_SYSTEM
case _:
system_prompt = LLM_MODIFY_PROMPT_SYSTEM
prompt_messages = [
SystemPromptMessage(content=system_prompt)
,UserPromptMessage(content=json.dumps(
{
"current": current,
"last_run": last_run,
"instruction": injected_instruction,
"ideal_output": ideal_output,
}
))
]
model_parameters = {"temperature": 0.4}
try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
)
generated_raw = cast(str, response.message.content)
first_brace = generated_raw.find("{")
last_brace = generated_raw.rfind("}")
return json.loads(generated_raw[first_brace:last_brace+1])
except InvokeError as e:
error = str(e)
return {"error": f"Failed to generate code. Error: {error}"}
except Exception as e:
logging.exception(
f"Failed to invoke LLM model, model: {model_config.get('name')}"
)
return {"error": f"An unexpected error occurred: {str(e)}"}

@ -309,3 +309,108 @@ eg:
Here is the JSON schema: Here is the JSON schema:
{{schema}} {{schema}}
""" # noqa: E501 """ # noqa: E501
LLM_MODIFY_PROMPT_SYSTEM = """
Both your input and output should be in JSON format.
! Below is the schema for input content !
{
"type": "object",
"description": "The user is trying to process some content with a prompt, but the output is not as expected. They hope to achieve their goal by modifying the prompt.",
"properties": {
"current": {
"type": "string",
"description": "The prompt before modification, where placeholders {{}} will be replaced with actual values for the large language model. The content in the placeholders should not be changed."
},
"last_run": {
"type": "object",
"description": "The output result from the large language model after receiving the prompt.",
},
"instruction": {
"type": "string",
"description": "User's instruction to edit the current prompt"
},
"ideal_output": {
"type": "string",
"description": "The ideal output that the user expects from the large language model after modifying the prompt."
}
}
}
! Above is the schema for input content !
! Below is the schema for output content !
{
"type": "object",
"description": "Your feedback to the user after they provide modification suggestions.",
"properties": {
"modified": {
"type": "string",
"description": "Your modified prompt. You should change the original prompt as little as possible to achieve the goal. Keep the language of prompt if not asked to change"
},
"message": {
"type": "string",
"description": "Your feedback to the user, in the user's language, explaining what you did and your thought process in text, providing sufficient emotional value to the user."
}
},
"required": [
"modified",
"message"
]
}
! Above is the schema for output content !
Your output must strictly follow the schema format, do not output any content outside of the JSON body.
""" # noqa: E501
LLM_MODIFY_CODE_SYSTEM = """
Both your input and output should be in JSON format.
! Below is the schema for input content !
{
"type": "object",
"description": "The user is trying to process some data with a code snippet, but the result is not as expected. They hope to achieve their goal by modifying the code.",
"properties": {
"current": {
"type": "string",
"description": "The code before modification."
},
"last_run": {
"type": "object",
"description": "The result of the code.",
},
"message": {
"type": "string",
"description": "User's instruction to edit the current code"
}
}
}
! Above is the schema for input content !
! Below is the schema for output content !
{
"type": "object",
"description": "Your feedback to the user after they provide modification suggestions.",
"properties": {
"modified": {
"type": "string",
"description": "Your modified code. You should change the original code as little as possible to achieve the goal. Keep the programming language of code if not asked to change"
},
"message": {
"type": "string",
"description": "Your feedback to the user, in the user's language, explaining what you did and your thought process in text, providing sufficient emotional value to the user."
}
},
"required": [
"modified",
"message"
]
}
! Above is the schema for output content !
Your output must strictly follow the schema format, do not output any content outside of the JSON body.
""" # noqa: E501
INSTRUCTION_GENERATE_TEMPLATE_PROMPT = """The output of this prompt is not as expected: {{#last_run#}}.
You should edit the prompt according to the IDEAL OUTPUT."""
INSTRUCTION_GENERATE_TEMPLATE_CODE = """Please fix the errors in the {{#error_message#}}."""

@ -144,6 +144,11 @@ class FileSegment(Segment):
return "" return ""
class BooleanSegment(Segment):
value_type: SegmentType = SegmentType.BOOLEAN
value: bool
class ArrayAnySegment(ArraySegment): class ArrayAnySegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_ANY value_type: SegmentType = SegmentType.ARRAY_ANY
value: Sequence[Any] value: Sequence[Any]
@ -188,6 +193,11 @@ class ArrayFileSegment(ArraySegment):
return "" return ""
class ArrayBooleanSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_BOOLEAN
value: Sequence[bool]
def get_segment_discriminator(v: Any) -> SegmentType | None: def get_segment_discriminator(v: Any) -> SegmentType | None:
if isinstance(v, Segment): if isinstance(v, Segment):
return v.value_type return v.value_type
@ -221,11 +231,13 @@ SegmentUnion: TypeAlias = Annotated[
| Annotated[IntegerSegment, Tag(SegmentType.INTEGER)] | Annotated[IntegerSegment, Tag(SegmentType.INTEGER)]
| Annotated[ObjectSegment, Tag(SegmentType.OBJECT)] | Annotated[ObjectSegment, Tag(SegmentType.OBJECT)]
| Annotated[FileSegment, Tag(SegmentType.FILE)] | Annotated[FileSegment, Tag(SegmentType.FILE)]
| Annotated[BooleanSegment, Tag(SegmentType.BOOLEAN)]
| Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)] | Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)]
| Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)] | Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)]
| Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)] | Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)]
| Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)] | Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)]
| Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)] | Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)]
| Annotated[ArrayBooleanSegment, Tag(SegmentType.ARRAY_BOOLEAN)]
), ),
Discriminator(get_segment_discriminator), Discriminator(get_segment_discriminator),
] ]

@ -27,12 +27,14 @@ class SegmentType(StrEnum):
SECRET = "secret" SECRET = "secret"
FILE = "file" FILE = "file"
BOOLEAN = "boolean"
ARRAY_ANY = "array[any]" ARRAY_ANY = "array[any]"
ARRAY_STRING = "array[string]" ARRAY_STRING = "array[string]"
ARRAY_NUMBER = "array[number]" ARRAY_NUMBER = "array[number]"
ARRAY_OBJECT = "array[object]" ARRAY_OBJECT = "array[object]"
ARRAY_FILE = "array[file]" ARRAY_FILE = "array[file]"
ARRAY_BOOLEAN = "array[boolean]"
NONE = "none" NONE = "none"
@ -76,12 +78,18 @@ class SegmentType(StrEnum):
return SegmentType.ARRAY_FILE return SegmentType.ARRAY_FILE
case SegmentType.NONE: case SegmentType.NONE:
return SegmentType.ARRAY_ANY return SegmentType.ARRAY_ANY
case SegmentType.BOOLEAN:
return SegmentType.ARRAY_BOOLEAN
case _: case _:
# This should be unreachable. # This should be unreachable.
raise ValueError(f"not supported value {value}") raise ValueError(f"not supported value {value}")
if value is None: if value is None:
return SegmentType.NONE return SegmentType.NONE
elif isinstance(value, int) and not isinstance(value, bool): # Important: The check for `bool` must precede the check for `int`,
# as `bool` is a subclass of `int` in Python's type hierarchy.
elif isinstance(value, bool):
return SegmentType.BOOLEAN
elif isinstance(value, int):
return SegmentType.INTEGER return SegmentType.INTEGER
elif isinstance(value, float): elif isinstance(value, float):
return SegmentType.FLOAT return SegmentType.FLOAT
@ -126,6 +134,10 @@ class SegmentType(StrEnum):
""" """
if self.is_array_type(): if self.is_array_type():
return self._validate_array(value, array_validation) return self._validate_array(value, array_validation)
# Important: The check for `bool` must precede the check for `int`,
# as `bool` is a subclass of `int` in Python's type hierarchy.
elif self == SegmentType.BOOLEAN:
return isinstance(value, bool)
elif self == SegmentType.NUMBER: elif self == SegmentType.NUMBER:
return isinstance(value, (int, float)) return isinstance(value, (int, float))
elif self == SegmentType.STRING: elif self == SegmentType.STRING:
@ -141,6 +153,27 @@ class SegmentType(StrEnum):
else: else:
raise AssertionError("this statement should be unreachable.") raise AssertionError("this statement should be unreachable.")
@staticmethod
def cast_value(value: Any, type_: "SegmentType") -> Any:
# Cast Python's `bool` type to `int` when the runtime type requires
# an integer or number.
#
# This ensures compatibility with existing workflows that may use `bool` as
# `int`, since in Python's type system, `bool` is a subtype of `int`.
#
# This function exists solely to maintain compatibility with existing workflows.
# It should not be used to compromise the integrity of the runtime type system.
# No additional casting rules should be introduced to this function.
if type_ in (
SegmentType.INTEGER,
SegmentType.NUMBER,
) and isinstance(value, bool):
return int(value)
if type_ == SegmentType.ARRAY_NUMBER and all(isinstance(i, bool) for i in value):
return [int(i) for i in value]
return value
def exposed_type(self) -> "SegmentType": def exposed_type(self) -> "SegmentType":
"""Returns the type exposed to the frontend. """Returns the type exposed to the frontend.
@ -157,6 +190,7 @@ _ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = {
SegmentType.ARRAY_NUMBER: SegmentType.NUMBER, SegmentType.ARRAY_NUMBER: SegmentType.NUMBER,
SegmentType.ARRAY_OBJECT: SegmentType.OBJECT, SegmentType.ARRAY_OBJECT: SegmentType.OBJECT,
SegmentType.ARRAY_FILE: SegmentType.FILE, SegmentType.ARRAY_FILE: SegmentType.FILE,
SegmentType.ARRAY_BOOLEAN: SegmentType.BOOLEAN,
} }
_ARRAY_TYPES = frozenset( _ARRAY_TYPES = frozenset(

@ -8,11 +8,13 @@ from core.helper import encrypter
from .segments import ( from .segments import (
ArrayAnySegment, ArrayAnySegment,
ArrayBooleanSegment,
ArrayFileSegment, ArrayFileSegment,
ArrayNumberSegment, ArrayNumberSegment,
ArrayObjectSegment, ArrayObjectSegment,
ArraySegment, ArraySegment,
ArrayStringSegment, ArrayStringSegment,
BooleanSegment,
FileSegment, FileSegment,
FloatSegment, FloatSegment,
IntegerSegment, IntegerSegment,
@ -96,10 +98,18 @@ class FileVariable(FileSegment, Variable):
pass pass
class BooleanVariable(BooleanSegment, Variable):
pass
class ArrayFileVariable(ArrayFileSegment, ArrayVariable): class ArrayFileVariable(ArrayFileSegment, ArrayVariable):
pass pass
class ArrayBooleanVariable(ArrayBooleanSegment, ArrayVariable):
pass
# The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic. # The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic.
# Use `Variable` for type hinting when serialization is not required. # Use `Variable` for type hinting when serialization is not required.
# #
@ -114,11 +124,13 @@ VariableUnion: TypeAlias = Annotated[
| Annotated[IntegerVariable, Tag(SegmentType.INTEGER)] | Annotated[IntegerVariable, Tag(SegmentType.INTEGER)]
| Annotated[ObjectVariable, Tag(SegmentType.OBJECT)] | Annotated[ObjectVariable, Tag(SegmentType.OBJECT)]
| Annotated[FileVariable, Tag(SegmentType.FILE)] | Annotated[FileVariable, Tag(SegmentType.FILE)]
| Annotated[BooleanVariable, Tag(SegmentType.BOOLEAN)]
| Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)] | Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)]
| Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)] | Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)]
| Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)] | Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)]
| Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)] | Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)]
| Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)] | Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)]
| Annotated[ArrayBooleanVariable, Tag(SegmentType.ARRAY_BOOLEAN)]
| Annotated[SecretVariable, Tag(SegmentType.SECRET)] | Annotated[SecretVariable, Tag(SegmentType.SECRET)]
), ),
Discriminator(get_segment_discriminator), Discriminator(get_segment_discriminator),

@ -12,9 +12,11 @@ _VALID_VAR_TYPE = frozenset(
SegmentType.STRING, SegmentType.STRING,
SegmentType.NUMBER, SegmentType.NUMBER,
SegmentType.OBJECT, SegmentType.OBJECT,
SegmentType.BOOLEAN,
SegmentType.ARRAY_STRING, SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER, SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT, SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_BOOLEAN,
] ]
) )

@ -522,7 +522,12 @@ class LoopNode(BaseNode):
@staticmethod @staticmethod
def _get_segment_for_constant(var_type: SegmentType, value: Any) -> Segment: def _get_segment_for_constant(var_type: SegmentType, value: Any) -> Segment:
"""Get the appropriate segment type for a constant value.""" """Get the appropriate segment type for a constant value."""
if var_type in ["array[string]", "array[number]", "array[object]"]: if var_type in [
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_BOOLEAN,
]:
if value and isinstance(value, str): if value and isinstance(value, str):
value = json.loads(value) value = json.loads(value)
else: else:

@ -2,6 +2,7 @@ from collections.abc import Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Optional, TypeAlias from typing import TYPE_CHECKING, Any, Optional, TypeAlias
from core.variables import SegmentType, Variable from core.variables import SegmentType, Variable
from core.variables.segments import BooleanSegment
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID
from core.workflow.conversation_variable_updater import ConversationVariableUpdater from core.workflow.conversation_variable_updater import ConversationVariableUpdater
from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.node_entities import NodeRunResult
@ -158,8 +159,8 @@ class VariableAssignerNode(BaseNode):
def get_zero_value(t: SegmentType): def get_zero_value(t: SegmentType):
# TODO(QuantumGhost): this should be a method of `SegmentType`. # TODO(QuantumGhost): this should be a method of `SegmentType`.
match t: match t:
case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER: case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER | SegmentType.ARRAY_BOOLEAN:
return variable_factory.build_segment([]) return variable_factory.build_segment_with_type(t, [])
case SegmentType.OBJECT: case SegmentType.OBJECT:
return variable_factory.build_segment({}) return variable_factory.build_segment({})
case SegmentType.STRING: case SegmentType.STRING:
@ -170,5 +171,7 @@ def get_zero_value(t: SegmentType):
return variable_factory.build_segment(0.0) return variable_factory.build_segment(0.0)
case SegmentType.NUMBER: case SegmentType.NUMBER:
return variable_factory.build_segment(0) return variable_factory.build_segment(0)
case SegmentType.BOOLEAN:
return BooleanSegment(value=False)
case _: case _:
raise VariableOperatorNodeError(f"unsupported variable type: {t}") raise VariableOperatorNodeError(f"unsupported variable type: {t}")

@ -4,9 +4,11 @@ from core.variables import SegmentType
EMPTY_VALUE_MAPPING = { EMPTY_VALUE_MAPPING = {
SegmentType.STRING: "", SegmentType.STRING: "",
SegmentType.NUMBER: 0, SegmentType.NUMBER: 0,
SegmentType.BOOLEAN: False,
SegmentType.OBJECT: {}, SegmentType.OBJECT: {},
SegmentType.ARRAY_ANY: [], SegmentType.ARRAY_ANY: [],
SegmentType.ARRAY_STRING: [], SegmentType.ARRAY_STRING: [],
SegmentType.ARRAY_NUMBER: [], SegmentType.ARRAY_NUMBER: [],
SegmentType.ARRAY_OBJECT: [], SegmentType.ARRAY_OBJECT: [],
SegmentType.ARRAY_BOOLEAN: [],
} }

@ -16,28 +16,15 @@ def is_operation_supported(*, variable_type: SegmentType, operation: Operation):
SegmentType.NUMBER, SegmentType.NUMBER,
SegmentType.INTEGER, SegmentType.INTEGER,
SegmentType.FLOAT, SegmentType.FLOAT,
SegmentType.BOOLEAN,
} }
case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE: case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE:
# Only number variable can be added, subtracted, multiplied or divided # Only number variable can be added, subtracted, multiplied or divided
return variable_type in {SegmentType.NUMBER, SegmentType.INTEGER, SegmentType.FLOAT} return variable_type in {SegmentType.NUMBER, SegmentType.INTEGER, SegmentType.FLOAT}
case Operation.APPEND | Operation.EXTEND: case Operation.APPEND | Operation.EXTEND | Operation.REMOVE_FIRST | Operation.REMOVE_LAST:
# Only array variable can be appended or extended # Only array variable can be appended or extended
return variable_type in {
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_FILE,
}
case Operation.REMOVE_FIRST | Operation.REMOVE_LAST:
# Only array variable can have elements removed # Only array variable can have elements removed
return variable_type in { return variable_type.is_array_type()
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_FILE,
}
case _: case _:
return False return False
@ -50,7 +37,7 @@ def is_variable_input_supported(*, operation: Operation):
def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation): def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation):
match variable_type: match variable_type:
case SegmentType.STRING | SegmentType.OBJECT: case SegmentType.STRING | SegmentType.OBJECT | SegmentType.BOOLEAN:
return operation in {Operation.OVER_WRITE, Operation.SET} return operation in {Operation.OVER_WRITE, Operation.SET}
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
return operation in { return operation in {
@ -72,6 +59,9 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
case SegmentType.STRING: case SegmentType.STRING:
return isinstance(value, str) return isinstance(value, str)
case SegmentType.BOOLEAN:
return isinstance(value, bool)
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
if not isinstance(value, int | float): if not isinstance(value, int | float):
return False return False
@ -91,6 +81,8 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
return isinstance(value, int | float) return isinstance(value, int | float)
case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND: case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND:
return isinstance(value, dict) return isinstance(value, dict)
case SegmentType.ARRAY_BOOLEAN if operation == Operation.APPEND:
return isinstance(value, bool)
# Array & Extend / Overwrite # Array & Extend / Overwrite
case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}: case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
@ -101,6 +93,8 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
return isinstance(value, list) and all(isinstance(item, int | float) for item in value) return isinstance(value, list) and all(isinstance(item, int | float) for item in value)
case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}: case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, dict) for item in value) return isinstance(value, list) and all(isinstance(item, dict) for item in value)
case SegmentType.ARRAY_BOOLEAN if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, bool) for item in value)
case _: case _:
return False return False

@ -45,5 +45,5 @@ class SubVariableCondition(BaseModel):
class Condition(BaseModel): class Condition(BaseModel):
variable_selector: list[str] variable_selector: list[str]
comparison_operator: SupportedComparisonOperator comparison_operator: SupportedComparisonOperator
value: str | Sequence[str] | None = None value: str | Sequence[str] | bool | None = None
sub_variable_condition: SubVariableCondition | None = None sub_variable_condition: SubVariableCondition | None = None

@ -1,5 +1,5 @@
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Literal from typing import Any, Literal, Union
from core.file import FileAttribute, file_manager from core.file import FileAttribute, file_manager
from core.variables import ArrayFileSegment from core.variables import ArrayFileSegment
@ -77,7 +77,7 @@ def _evaluate_condition(
*, *,
operator: SupportedComparisonOperator, operator: SupportedComparisonOperator,
value: Any, value: Any,
expected: str | Sequence[str] | None, expected: Union[str, Sequence[str], None],
) -> bool: ) -> bool:
match operator: match operator:
case "contains": case "contains":
@ -130,7 +130,7 @@ def _assert_contains(*, value: Any, expected: Any) -> bool:
if not value: if not value:
return False return False
if not isinstance(value, str | list): if not isinstance(value, (str, list)):
raise ValueError("Invalid actual value type: string or array") raise ValueError("Invalid actual value type: string or array")
if expected not in value: if expected not in value:
@ -142,7 +142,7 @@ def _assert_not_contains(*, value: Any, expected: Any) -> bool:
if not value: if not value:
return True return True
if not isinstance(value, str | list): if not isinstance(value, (str, list)):
raise ValueError("Invalid actual value type: string or array") raise ValueError("Invalid actual value type: string or array")
if expected in value: if expected in value:
@ -178,8 +178,8 @@ def _assert_is(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, str): if not isinstance(value, (str, bool)):
raise ValueError("Invalid actual value type: string") raise ValueError("Invalid actual value type: string or boolean")
if value != expected: if value != expected:
return False return False
@ -190,8 +190,8 @@ def _assert_is_not(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, str): if not isinstance(value, (str, bool)):
raise ValueError("Invalid actual value type: string") raise ValueError("Invalid actual value type: string or boolean")
if value == expected: if value == expected:
return False return False
@ -214,10 +214,13 @@ def _assert_equal(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, int | float): if not isinstance(value, (int, float, bool)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number or boolean")
if isinstance(value, int): # Handle boolean comparison
if isinstance(value, bool):
expected = bool(expected)
elif isinstance(value, int):
expected = int(expected) expected = int(expected)
else: else:
expected = float(expected) expected = float(expected)
@ -231,10 +234,13 @@ def _assert_not_equal(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, int | float): if not isinstance(value, (int, float, bool)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number or boolean")
if isinstance(value, int): # Handle boolean comparison
if isinstance(value, bool):
expected = bool(expected)
elif isinstance(value, int):
expected = int(expected) expected = int(expected)
else: else:
expected = float(expected) expected = float(expected)
@ -248,7 +254,7 @@ def _assert_greater_than(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, int | float): if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")
if isinstance(value, int): if isinstance(value, int):
@ -265,7 +271,7 @@ def _assert_less_than(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, int | float): if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")
if isinstance(value, int): if isinstance(value, int):
@ -282,7 +288,7 @@ def _assert_greater_than_or_equal(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, int | float): if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")
if isinstance(value, int): if isinstance(value, int):
@ -299,7 +305,7 @@ def _assert_less_than_or_equal(*, value: Any, expected: Any) -> bool:
if value is None: if value is None:
return False return False
if not isinstance(value, int | float): if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")
if isinstance(value, int): if isinstance(value, int):

@ -7,11 +7,13 @@ from core.file import File
from core.variables.exc import VariableError from core.variables.exc import VariableError
from core.variables.segments import ( from core.variables.segments import (
ArrayAnySegment, ArrayAnySegment,
ArrayBooleanSegment,
ArrayFileSegment, ArrayFileSegment,
ArrayNumberSegment, ArrayNumberSegment,
ArrayObjectSegment, ArrayObjectSegment,
ArraySegment, ArraySegment,
ArrayStringSegment, ArrayStringSegment,
BooleanSegment,
FileSegment, FileSegment,
FloatSegment, FloatSegment,
IntegerSegment, IntegerSegment,
@ -23,10 +25,12 @@ from core.variables.segments import (
from core.variables.types import SegmentType from core.variables.types import SegmentType
from core.variables.variables import ( from core.variables.variables import (
ArrayAnyVariable, ArrayAnyVariable,
ArrayBooleanVariable,
ArrayFileVariable, ArrayFileVariable,
ArrayNumberVariable, ArrayNumberVariable,
ArrayObjectVariable, ArrayObjectVariable,
ArrayStringVariable, ArrayStringVariable,
BooleanVariable,
FileVariable, FileVariable,
FloatVariable, FloatVariable,
IntegerVariable, IntegerVariable,
@ -49,17 +53,19 @@ class TypeMismatchError(Exception):
# Define the constant # Define the constant
SEGMENT_TO_VARIABLE_MAP = { SEGMENT_TO_VARIABLE_MAP = {
StringSegment: StringVariable, ArrayAnySegment: ArrayAnyVariable,
IntegerSegment: IntegerVariable, ArrayBooleanSegment: ArrayBooleanVariable,
FloatSegment: FloatVariable, ArrayFileSegment: ArrayFileVariable,
ObjectSegment: ObjectVariable,
FileSegment: FileVariable,
ArrayStringSegment: ArrayStringVariable,
ArrayNumberSegment: ArrayNumberVariable, ArrayNumberSegment: ArrayNumberVariable,
ArrayObjectSegment: ArrayObjectVariable, ArrayObjectSegment: ArrayObjectVariable,
ArrayFileSegment: ArrayFileVariable, ArrayStringSegment: ArrayStringVariable,
ArrayAnySegment: ArrayAnyVariable, BooleanSegment: BooleanVariable,
FileSegment: FileVariable,
FloatSegment: FloatVariable,
IntegerSegment: IntegerVariable,
NoneSegment: NoneVariable, NoneSegment: NoneVariable,
ObjectSegment: ObjectVariable,
StringSegment: StringVariable,
} }
@ -99,6 +105,8 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen
mapping = dict(mapping) mapping = dict(mapping)
mapping["value_type"] = SegmentType.FLOAT mapping["value_type"] = SegmentType.FLOAT
result = FloatVariable.model_validate(mapping) result = FloatVariable.model_validate(mapping)
case SegmentType.BOOLEAN:
result = BooleanVariable.model_validate(mapping)
case SegmentType.NUMBER if not isinstance(value, float | int): case SegmentType.NUMBER if not isinstance(value, float | int):
raise VariableError(f"invalid number value {value}") raise VariableError(f"invalid number value {value}")
case SegmentType.OBJECT if isinstance(value, dict): case SegmentType.OBJECT if isinstance(value, dict):
@ -109,6 +117,8 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen
result = ArrayNumberVariable.model_validate(mapping) result = ArrayNumberVariable.model_validate(mapping)
case SegmentType.ARRAY_OBJECT if isinstance(value, list): case SegmentType.ARRAY_OBJECT if isinstance(value, list):
result = ArrayObjectVariable.model_validate(mapping) result = ArrayObjectVariable.model_validate(mapping)
case SegmentType.ARRAY_BOOLEAN if isinstance(value, list):
result = ArrayBooleanVariable.model_validate(mapping)
case _: case _:
raise VariableError(f"not supported value type {value_type}") raise VariableError(f"not supported value type {value_type}")
if result.size > dify_config.MAX_VARIABLE_SIZE: if result.size > dify_config.MAX_VARIABLE_SIZE:
@ -129,6 +139,8 @@ def build_segment(value: Any, /) -> Segment:
return NoneSegment() return NoneSegment()
if isinstance(value, str): if isinstance(value, str):
return StringSegment(value=value) return StringSegment(value=value)
if isinstance(value, bool):
return BooleanSegment(value=value)
if isinstance(value, int): if isinstance(value, int):
return IntegerSegment(value=value) return IntegerSegment(value=value)
if isinstance(value, float): if isinstance(value, float):
@ -152,6 +164,8 @@ def build_segment(value: Any, /) -> Segment:
return ArrayStringSegment(value=value) return ArrayStringSegment(value=value)
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
return ArrayNumberSegment(value=value) return ArrayNumberSegment(value=value)
case SegmentType.BOOLEAN:
return ArrayBooleanSegment(value=value)
case SegmentType.OBJECT: case SegmentType.OBJECT:
return ArrayObjectSegment(value=value) return ArrayObjectSegment(value=value)
case SegmentType.FILE: case SegmentType.FILE:
@ -170,6 +184,7 @@ _segment_factory: Mapping[SegmentType, type[Segment]] = {
SegmentType.INTEGER: IntegerSegment, SegmentType.INTEGER: IntegerSegment,
SegmentType.FLOAT: FloatSegment, SegmentType.FLOAT: FloatSegment,
SegmentType.FILE: FileSegment, SegmentType.FILE: FileSegment,
SegmentType.BOOLEAN: BooleanSegment,
SegmentType.OBJECT: ObjectSegment, SegmentType.OBJECT: ObjectSegment,
# Array types # Array types
SegmentType.ARRAY_ANY: ArrayAnySegment, SegmentType.ARRAY_ANY: ArrayAnySegment,
@ -177,6 +192,7 @@ _segment_factory: Mapping[SegmentType, type[Segment]] = {
SegmentType.ARRAY_NUMBER: ArrayNumberSegment, SegmentType.ARRAY_NUMBER: ArrayNumberSegment,
SegmentType.ARRAY_OBJECT: ArrayObjectSegment, SegmentType.ARRAY_OBJECT: ArrayObjectSegment,
SegmentType.ARRAY_FILE: ArrayFileSegment, SegmentType.ARRAY_FILE: ArrayFileSegment,
SegmentType.ARRAY_BOOLEAN: ArrayBooleanSegment,
} }
@ -225,6 +241,8 @@ def build_segment_with_type(segment_type: SegmentType, value: Any) -> Segment:
return ArrayAnySegment(value=value) return ArrayAnySegment(value=value)
elif segment_type == SegmentType.ARRAY_STRING: elif segment_type == SegmentType.ARRAY_STRING:
return ArrayStringSegment(value=value) return ArrayStringSegment(value=value)
elif segment_type == SegmentType.ARRAY_BOOLEAN:
return ArrayBooleanSegment(value=value)
elif segment_type == SegmentType.ARRAY_NUMBER: elif segment_type == SegmentType.ARRAY_NUMBER:
return ArrayNumberSegment(value=value) return ArrayNumberSegment(value=value)
elif segment_type == SegmentType.ARRAY_OBJECT: elif segment_type == SegmentType.ARRAY_OBJECT:

@ -0,0 +1,11 @@
from tests.integration_tests.utils.parent_class import ParentClass
class LazyLoadChildClass(ParentClass):
"""Test lazy load child class for module import helper tests"""
def __init__(self, name):
super().__init__(name)
def get_name(self):
return self.name

@ -671,6 +671,12 @@ class AccountService:
return account return account
@classmethod
def is_account_in_freeze(cls, email: str) -> bool:
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email):
return True
return False
@staticmethod @staticmethod
@redis_fallback(default_return=None) @redis_fallback(default_return=None)
def add_login_error_rate_limit(email: str) -> None: def add_login_error_rate_limit(email: str) -> None:

@ -272,3 +272,209 @@ def test_array_file_contains_file_name():
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None assert result.outputs is not None
assert result.outputs["result"] is True assert result.outputs["result"] is True
def test_execute_if_else_boolean_conditions():
"""Test IfElseNode with boolean conditions using various operators"""
graph_config = {"edges": [], "nodes": [{"data": {"type": "start"}, "id": "start"}]}
graph = Graph.init(graph_config=graph_config)
init_params = GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config=graph_config,
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)
# construct variable pool with boolean values
pool = VariablePool(
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={}
)
pool.add(["start", "bool_true"], True)
pool.add(["start", "bool_false"], False)
pool.add(["start", "bool_array"], [True, False, True])
pool.add(["start", "mixed_array"], [True, "false", 1, 0])
node = IfElseNode(
id=str(uuid.uuid4()),
graph_init_params=init_params,
graph=graph,
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
config={
"id": "if-else",
"data": {
"title": "Boolean Test",
"type": "if-else",
"logical_operator": "and",
"conditions": [
# Test boolean "is" operator
{"comparison_operator": "is", "variable_selector": ["start", "bool_true"], "value": "true"},
# Test boolean "is not" operator
{"comparison_operator": "is not", "variable_selector": ["start", "bool_false"], "value": "true"},
# Test boolean "=" operator
{"comparison_operator": "=", "variable_selector": ["start", "bool_true"], "value": "1"},
# Test boolean "≠" operator
{"comparison_operator": "", "variable_selector": ["start", "bool_false"], "value": "1"},
# Test boolean "not null" operator
{"comparison_operator": "not null", "variable_selector": ["start", "bool_true"]},
# Test boolean array "contains" operator
{"comparison_operator": "contains", "variable_selector": ["start", "bool_array"], "value": "true"},
# Test boolean "in" operator
{
"comparison_operator": "in",
"variable_selector": ["start", "bool_true"],
"value": ["true", "false"],
},
],
},
},
)
# Mock db.session.close()
db.session.close = MagicMock()
# execute node
result = node._run()
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["result"] is True
def test_execute_if_else_boolean_false_conditions():
"""Test IfElseNode with boolean conditions that should evaluate to false"""
graph_config = {"edges": [], "nodes": [{"data": {"type": "start"}, "id": "start"}]}
graph = Graph.init(graph_config=graph_config)
init_params = GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config=graph_config,
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)
# construct variable pool with boolean values
pool = VariablePool(
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={}
)
pool.add(["start", "bool_true"], True)
pool.add(["start", "bool_false"], False)
pool.add(["start", "bool_array"], [True, False, True])
node = IfElseNode(
id=str(uuid.uuid4()),
graph_init_params=init_params,
graph=graph,
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
config={
"id": "if-else",
"data": {
"title": "Boolean False Test",
"type": "if-else",
"logical_operator": "or",
"conditions": [
# Test boolean "is" operator (should be false)
{"comparison_operator": "is", "variable_selector": ["start", "bool_true"], "value": "false"},
# Test boolean "=" operator (should be false)
{"comparison_operator": "=", "variable_selector": ["start", "bool_false"], "value": "1"},
# Test boolean "not contains" operator (should be false)
{
"comparison_operator": "not contains",
"variable_selector": ["start", "bool_array"],
"value": "true",
},
],
},
},
)
# Mock db.session.close()
db.session.close = MagicMock()
# execute node
result = node._run()
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["result"] is False
def test_execute_if_else_boolean_cases_structure():
"""Test IfElseNode with boolean conditions using the new cases structure"""
graph_config = {"edges": [], "nodes": [{"data": {"type": "start"}, "id": "start"}]}
graph = Graph.init(graph_config=graph_config)
init_params = GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config=graph_config,
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)
# construct variable pool with boolean values
pool = VariablePool(
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={}
)
pool.add(["start", "bool_true"], True)
pool.add(["start", "bool_false"], False)
node = IfElseNode(
id=str(uuid.uuid4()),
graph_init_params=init_params,
graph=graph,
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
config={
"id": "if-else",
"data": {
"title": "Boolean Cases Test",
"type": "if-else",
"cases": [
{
"case_id": "true",
"logical_operator": "and",
"conditions": [
{
"comparison_operator": "is",
"variable_selector": ["start", "bool_true"],
"value": "true",
},
{
"comparison_operator": "is not",
"variable_selector": ["start", "bool_false"],
"value": "true",
},
],
}
],
},
},
)
# Mock db.session.close()
db.session.close = MagicMock()
# execute node
result = node._run()
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["result"] is True
assert result.outputs["selected_case_id"] == "true"

@ -24,16 +24,18 @@ from core.variables.segments import (
ArrayNumberSegment, ArrayNumberSegment,
ArrayObjectSegment, ArrayObjectSegment,
ArrayStringSegment, ArrayStringSegment,
BooleanSegment,
FileSegment, FileSegment,
FloatSegment, FloatSegment,
IntegerSegment, IntegerSegment,
NoneSegment, NoneSegment,
ObjectSegment, ObjectSegment,
Segment,
StringSegment, StringSegment,
) )
from core.variables.types import SegmentType from core.variables.types import SegmentType
from factories import variable_factory from factories import variable_factory
from factories.variable_factory import TypeMismatchError, build_segment_with_type from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type
def test_string_variable(): def test_string_variable():
@ -139,6 +141,26 @@ def test_array_number_variable():
assert isinstance(variable.value[1], float) assert isinstance(variable.value[1], float)
def test_build_segment_scalar_values():
@dataclass
class TestCase:
value: Any
expected: Segment
description: str
cases = [
TestCase(
value=True,
expected=BooleanSegment(value=True),
description="build_segment with boolean should yield BooleanSegment",
)
]
for idx, c in enumerate(cases, 1):
seg = build_segment(c.value)
assert seg == c.expected, f"Test case {idx} failed: {c.description}"
def test_array_object_variable(): def test_array_object_variable():
mapping = { mapping = {
"id": str(uuid4()), "id": str(uuid4()),
@ -847,15 +869,22 @@ class TestBuildSegmentValueErrors:
f"but got: {error_message}" f"but got: {error_message}"
) )
def test_build_segment_boolean_type_note(self): def test_build_segment_boolean_type(self):
"""Note: Boolean values are actually handled as integers in Python, so they don't raise ValueError.""" """Test that Boolean values are correctly handled as boolean type, not integers."""
# Boolean values in Python are subclasses of int, so they get processed as integers # Boolean values should now be processed as BooleanSegment, not IntegerSegment
# True becomes IntegerSegment(value=1) and False becomes IntegerSegment(value=0) # This is because the bool check now comes before the int check in build_segment
true_segment = variable_factory.build_segment(True) true_segment = variable_factory.build_segment(True)
false_segment = variable_factory.build_segment(False) false_segment = variable_factory.build_segment(False)
# Verify they are processed as integers, not as errors # Verify they are processed as booleans, not integers
assert true_segment.value == 1, "Test case 1 (boolean_true): Expected True to be processed as integer 1" assert true_segment.value is True, "Test case 1 (boolean_true): Expected True to be processed as boolean True"
assert false_segment.value == 0, "Test case 2 (boolean_false): Expected False to be processed as integer 0" assert false_segment.value is False, (
assert true_segment.value_type == SegmentType.INTEGER "Test case 2 (boolean_false): Expected False to be processed as boolean False"
assert false_segment.value_type == SegmentType.INTEGER )
assert true_segment.value_type == SegmentType.BOOLEAN
assert false_segment.value_type == SegmentType.BOOLEAN
# Test array of booleans
bool_array_segment = variable_factory.build_segment([True, False, True])
assert bool_array_segment.value_type == SegmentType.ARRAY_BOOLEAN
assert bool_array_segment.value == [True, False, True]

@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Simple test to verify boolean classes can be imported correctly.
"""
import sys
import os
# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
try:
# Test that we can import the boolean classes
from core.variables.segments import BooleanSegment, ArrayBooleanSegment
from core.variables.variables import BooleanVariable, ArrayBooleanVariable
from core.variables.types import SegmentType
print("✅ Successfully imported BooleanSegment")
print("✅ Successfully imported ArrayBooleanSegment")
print("✅ Successfully imported BooleanVariable")
print("✅ Successfully imported ArrayBooleanVariable")
print("✅ Successfully imported SegmentType")
# Test that the segment types exist
print(f"✅ SegmentType.BOOLEAN = {SegmentType.BOOLEAN}")
print(f"✅ SegmentType.ARRAY_BOOLEAN = {SegmentType.ARRAY_BOOLEAN}")
# Test creating boolean segments directly
bool_seg = BooleanSegment(value=True)
print(f"✅ Created BooleanSegment: {bool_seg}")
print(f" Value type: {bool_seg.value_type}")
print(f" Value: {bool_seg.value}")
array_bool_seg = ArrayBooleanSegment(value=[True, False, True])
print(f"✅ Created ArrayBooleanSegment: {array_bool_seg}")
print(f" Value type: {array_bool_seg.value_type}")
print(f" Value: {array_bool_seg.value}")
print("\n🎉 All boolean class imports and basic functionality work correctly!")
except ImportError as e:
print(f"❌ Import error: {e}")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Simple test script to verify boolean condition support in IfElseNode
"""
import sys
import os
# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
from core.workflow.utils.condition.processor import (
ConditionProcessor,
_evaluate_condition,
)
def test_boolean_conditions():
"""Test boolean condition evaluation"""
print("Testing boolean condition support...")
# Test boolean "is" operator
result = _evaluate_condition(value=True, operator="is", expected="true")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is' with True value passed")
result = _evaluate_condition(value=False, operator="is", expected="false")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is' with False value passed")
# Test boolean "is not" operator
result = _evaluate_condition(value=True, operator="is not", expected="false")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is not' with True value passed")
result = _evaluate_condition(value=False, operator="is not", expected="true")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is not' with False value passed")
# Test boolean "=" operator
result = _evaluate_condition(value=True, operator="=", expected="1")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '=' with True=1 passed")
result = _evaluate_condition(value=False, operator="=", expected="0")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '=' with False=0 passed")
# Test boolean "≠" operator
result = _evaluate_condition(value=True, operator="", expected="0")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '' with True≠0 passed")
result = _evaluate_condition(value=False, operator="", expected="1")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '' with False≠1 passed")
# Test boolean "in" operator
result = _evaluate_condition(value=True, operator="in", expected=["true", "false"])
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'in' with True in array passed")
result = _evaluate_condition(value=False, operator="in", expected=["true", "false"])
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'in' with False in array passed")
# Test boolean "not in" operator
result = _evaluate_condition(value=True, operator="not in", expected=["false", "0"])
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'not in' with True not in [false, 0] passed")
# Test boolean "null" and "not null" operators
result = _evaluate_condition(value=True, operator="not null", expected=None)
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'not null' with True passed")
result = _evaluate_condition(value=False, operator="not null", expected=None)
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'not null' with False passed")
print("\n🎉 All boolean condition tests passed!")
def test_backward_compatibility():
"""Test that existing string and number conditions still work"""
print("\nTesting backward compatibility...")
# Test string conditions
result = _evaluate_condition(value="hello", operator="is", expected="hello")
assert result == True, f"Expected True, got {result}"
print("✓ String 'is' condition still works")
result = _evaluate_condition(value="hello", operator="contains", expected="ell")
assert result == True, f"Expected True, got {result}"
print("✓ String 'contains' condition still works")
# Test number conditions
result = _evaluate_condition(value=42, operator="=", expected="42")
assert result == True, f"Expected True, got {result}"
print("✓ Number '=' condition still works")
result = _evaluate_condition(value=42, operator=">", expected="40")
assert result == True, f"Expected True, got {result}"
print("✓ Number '>' condition still works")
print("✓ Backward compatibility maintained!")
if __name__ == "__main__":
try:
test_boolean_conditions()
test_backward_compatibility()
print(
"\n✅ All tests passed! Boolean support has been successfully added to IfElseNode."
)
except Exception as e:
print(f"\n❌ Test failed: {e}")
sys.exit(1)

@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Test script to verify the boolean array comparison fix in condition processor.
"""
import sys
import os
# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
from core.workflow.utils.condition.processor import (
_assert_contains,
_assert_not_contains,
)
def test_boolean_array_contains():
"""Test that boolean arrays work correctly with string comparisons."""
# Test case 1: Boolean array [True, False, True] contains "true"
bool_array = [True, False, True]
# Should return True because "true" converts to True and True is in the array
result1 = _assert_contains(value=bool_array, expected="true")
print(f"Test 1 - [True, False, True] contains 'true': {result1}")
assert result1 == True, "Expected True but got False"
# Should return True because "false" converts to False and False is in the array
result2 = _assert_contains(value=bool_array, expected="false")
print(f"Test 2 - [True, False, True] contains 'false': {result2}")
assert result2 == True, "Expected True but got False"
# Test case 2: Boolean array [True, True] does not contain "false"
bool_array2 = [True, True]
result3 = _assert_contains(value=bool_array2, expected="false")
print(f"Test 3 - [True, True] contains 'false': {result3}")
assert result3 == False, "Expected False but got True"
# Test case 3: Test not_contains
result4 = _assert_not_contains(value=bool_array2, expected="false")
print(f"Test 4 - [True, True] not contains 'false': {result4}")
assert result4 == True, "Expected True but got False"
result5 = _assert_not_contains(value=bool_array, expected="true")
print(f"Test 5 - [True, False, True] not contains 'true': {result5}")
assert result5 == False, "Expected False but got True"
# Test case 4: Test with different string representations
result6 = _assert_contains(
value=bool_array, expected="1"
) # "1" should convert to True
print(f"Test 6 - [True, False, True] contains '1': {result6}")
assert result6 == True, "Expected True but got False"
result7 = _assert_contains(
value=bool_array, expected="0"
) # "0" should convert to False
print(f"Test 7 - [True, False, True] contains '0': {result7}")
assert result7 == True, "Expected True but got False"
print("\n✅ All boolean array comparison tests passed!")
if __name__ == "__main__":
test_boolean_array_contains()

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Simple test script to verify boolean type inference in variable factory.
"""
import sys
import os
# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
try:
from factories.variable_factory import build_segment, segment_to_variable
from core.variables.segments import BooleanSegment, ArrayBooleanSegment
from core.variables.variables import BooleanVariable, ArrayBooleanVariable
from core.variables.types import SegmentType
def test_boolean_inference():
print("Testing boolean type inference...")
# Test single boolean values
true_segment = build_segment(True)
false_segment = build_segment(False)
print(f"True value: {true_segment}")
print(f"Type: {type(true_segment)}")
print(f"Value type: {true_segment.value_type}")
print(f"Is BooleanSegment: {isinstance(true_segment, BooleanSegment)}")
print(f"\nFalse value: {false_segment}")
print(f"Type: {type(false_segment)}")
print(f"Value type: {false_segment.value_type}")
print(f"Is BooleanSegment: {isinstance(false_segment, BooleanSegment)}")
# Test array of booleans
bool_array_segment = build_segment([True, False, True])
print(f"\nBoolean array: {bool_array_segment}")
print(f"Type: {type(bool_array_segment)}")
print(f"Value type: {bool_array_segment.value_type}")
print(
f"Is ArrayBooleanSegment: {isinstance(bool_array_segment, ArrayBooleanSegment)}"
)
# Test empty boolean array
empty_bool_array = build_segment([])
print(f"\nEmpty array: {empty_bool_array}")
print(f"Type: {type(empty_bool_array)}")
print(f"Value type: {empty_bool_array.value_type}")
# Test segment to variable conversion
bool_var = segment_to_variable(
segment=true_segment, selector=["test", "bool_var"], name="test_boolean"
)
print(f"\nBoolean variable: {bool_var}")
print(f"Type: {type(bool_var)}")
print(f"Is BooleanVariable: {isinstance(bool_var, BooleanVariable)}")
array_bool_var = segment_to_variable(
segment=bool_array_segment,
selector=["test", "array_bool_var"],
name="test_array_boolean",
)
print(f"\nArray boolean variable: {array_bool_var}")
print(f"Type: {type(array_bool_var)}")
print(
f"Is ArrayBooleanVariable: {isinstance(array_bool_var, ArrayBooleanVariable)}"
)
# Test that bool comes before int (critical ordering)
print(f"\nTesting bool vs int precedence:")
print(f"True is instance of bool: {isinstance(True, bool)}")
print(f"True is instance of int: {isinstance(True, int)}")
print(f"False is instance of bool: {isinstance(False, bool)}")
print(f"False is instance of int: {isinstance(False, int)}")
# Verify that boolean values are correctly inferred as boolean, not int
assert true_segment.value_type == SegmentType.BOOLEAN, (
"True should be inferred as BOOLEAN"
)
assert false_segment.value_type == SegmentType.BOOLEAN, (
"False should be inferred as BOOLEAN"
)
assert bool_array_segment.value_type == SegmentType.ARRAY_BOOLEAN, (
"Boolean array should be inferred as ARRAY_BOOLEAN"
)
print("\n✅ All boolean inference tests passed!")
if __name__ == "__main__":
test_boolean_inference()
except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running this from the correct directory")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()

@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
Test script to verify boolean support in VariableAssigner node
"""
import sys
import os
# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
from core.variables import SegmentType
from core.workflow.nodes.variable_assigner.v2.helpers import (
is_operation_supported,
is_constant_input_supported,
is_input_value_valid,
)
from core.workflow.nodes.variable_assigner.v2.enums import Operation
from core.workflow.nodes.variable_assigner.v2.constants import EMPTY_VALUE_MAPPING
def test_boolean_operation_support():
"""Test that boolean types support the correct operations"""
print("Testing boolean operation support...")
# Boolean should support SET, OVER_WRITE, and CLEAR
assert is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET
)
assert is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE
)
assert is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.CLEAR
)
# Boolean should NOT support arithmetic operations
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.ADD
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.SUBTRACT
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.MULTIPLY
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.DIVIDE
)
# Boolean should NOT support array operations
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.APPEND
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.EXTEND
)
print("✓ Boolean operation support tests passed")
def test_array_boolean_operation_support():
"""Test that array boolean types support the correct operations"""
print("Testing array boolean operation support...")
# Array boolean should support APPEND, EXTEND, SET, OVER_WRITE, CLEAR
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.EXTEND
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.OVER_WRITE
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.CLEAR
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.REMOVE_FIRST
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.REMOVE_LAST
)
# Array boolean should NOT support arithmetic operations
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.ADD
)
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.SUBTRACT
)
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.MULTIPLY
)
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.DIVIDE
)
print("✓ Array boolean operation support tests passed")
def test_boolean_constant_input_support():
"""Test that boolean types support constant input for correct operations"""
print("Testing boolean constant input support...")
# Boolean should support constant input for SET and OVER_WRITE
assert is_constant_input_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET
)
assert is_constant_input_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE
)
# Boolean should NOT support constant input for arithmetic operations
assert not is_constant_input_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.ADD
)
print("✓ Boolean constant input support tests passed")
def test_boolean_input_validation():
"""Test that boolean input validation works correctly"""
print("Testing boolean input validation...")
# Boolean values should be valid for boolean type
assert is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=True
)
assert is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=False
)
assert is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE, value=True
)
# Non-boolean values should be invalid for boolean type
assert not is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value="true"
)
assert not is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=1
)
assert not is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=0
)
print("✓ Boolean input validation tests passed")
def test_array_boolean_input_validation():
"""Test that array boolean input validation works correctly"""
print("Testing array boolean input validation...")
# Boolean values should be valid for array boolean append
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND, value=True
)
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND, value=False
)
# Boolean arrays should be valid for extend/overwrite
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.EXTEND,
value=[True, False, True],
)
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.OVER_WRITE,
value=[False, False],
)
# Non-boolean values should be invalid
assert not is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.APPEND,
value="true",
)
assert not is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.EXTEND,
value=[True, "false"],
)
print("✓ Array boolean input validation tests passed")
def test_empty_value_mapping():
"""Test that empty value mapping includes boolean types"""
print("Testing empty value mapping...")
# Check that boolean types have correct empty values
assert SegmentType.BOOLEAN in EMPTY_VALUE_MAPPING
assert EMPTY_VALUE_MAPPING[SegmentType.BOOLEAN] is False
assert SegmentType.ARRAY_BOOLEAN in EMPTY_VALUE_MAPPING
assert EMPTY_VALUE_MAPPING[SegmentType.ARRAY_BOOLEAN] == []
print("✓ Empty value mapping tests passed")
def main():
"""Run all tests"""
print("Running VariableAssigner boolean support tests...\n")
try:
test_boolean_operation_support()
test_array_boolean_operation_support()
test_boolean_constant_input_support()
test_boolean_input_validation()
test_array_boolean_input_validation()
test_empty_value_mapping()
print(
"\n🎉 All tests passed! Boolean support has been successfully added to VariableAssigner."
)
except Exception as e:
print(f"\n❌ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

@ -15,6 +15,8 @@ import {
verifyEmail, verifyEmail,
} from '@/service/common' } from '@/service/common'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { asyncRunSafe } from '@/utils'
import type { ResponseError } from '@/service/fetch'
type Props = { type Props = {
show: boolean show: boolean
@ -39,6 +41,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
const [time, setTime] = useState<number>(0) const [time, setTime] = useState<number>(0)
const [stepToken, setStepToken] = useState<string>('') const [stepToken, setStepToken] = useState<string>('')
const [newEmailExited, setNewEmailExited] = useState<boolean>(false) const [newEmailExited, setNewEmailExited] = useState<boolean>(false)
const [unAvailableEmail, setUnAvailableEmail] = useState<boolean>(false)
const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(false) const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(false)
const startCount = () => { const startCount = () => {
@ -124,9 +127,17 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
email, email,
}) })
setNewEmailExited(false) setNewEmailExited(false)
setUnAvailableEmail(false)
} }
catch { catch (e: any) {
setNewEmailExited(true) if (e.status === 400) {
const [, errRespData] = await asyncRunSafe<ResponseError>(e.json())
const { code } = errRespData || {}
if (code === 'email_already_in_use')
setNewEmailExited(true)
if (code === 'account_in_freeze')
setUnAvailableEmail(true)
}
} }
finally { finally {
setIsCheckingEmail(false) setIsCheckingEmail(false)
@ -291,15 +302,18 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
placeholder={t('common.account.changeEmail.emailPlaceholder')} placeholder={t('common.account.changeEmail.emailPlaceholder')}
value={mail} value={mail}
onChange={e => handleNewEmailValueChange(e.target.value)} onChange={e => handleNewEmailValueChange(e.target.value)}
destructive={newEmailExited} destructive={newEmailExited || unAvailableEmail}
/> />
{newEmailExited && ( {newEmailExited && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div> <div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div>
)} )}
{unAvailableEmail && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.unAvailableEmail')}</div>
)}
</div> </div>
<div className='mt-3 space-y-2'> <div className='mt-3 space-y-2'>
<Button <Button
disabled={!mail || newEmailExited || isCheckingEmail || !isValidEmail(mail)} disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
className='!w-full' className='!w-full'
variant='primary' variant='primary'
onClick={sendCodeToNewEmail} onClick={sendCodeToNewEmail}

@ -13,7 +13,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import { getNewVar, getVars } from '@/utils/var' import { getNewVar, getVars } from '@/utils/var'
import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn' import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
import type { AutomaticRes } from '@/service/debug' import type { GenRes } from '@/service/debug'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res' import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import PromptEditor from '@/app/components/base/prompt-editor' import PromptEditor from '@/app/components/base/prompt-editor'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
@ -61,6 +61,7 @@ const Prompt: FC<ISimplePromptInput> = ({
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { const {
appId,
modelConfig, modelConfig,
dataSets, dataSets,
setModelConfig, setModelConfig,
@ -139,21 +140,21 @@ const Prompt: FC<ISimplePromptInput> = ({
} }
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = (res: AutomaticRes) => { const handleAutomaticRes = (res: GenRes) => {
// put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables. // put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables.
eventEmitter?.emit({ eventEmitter?.emit({
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER, type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
payload: res.prompt, payload: res.modified,
} as any) } as any)
const newModelConfig = produce(modelConfig, (draft) => { const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = res.prompt draft.configs.prompt_template = res.modified
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true })) draft.configs.prompt_variables = (res.variables || []).map(key => ({ key, name: key, type: 'string', required: true }))
}) })
setModelConfig(newModelConfig) setModelConfig(newModelConfig)
setPrevPromptConfig(modelConfig.configs) setPrevPromptConfig(modelConfig.configs)
if (mode !== AppType.completion) { if (mode !== AppType.completion) {
setIntroduction(res.opening_statement) setIntroduction(res.opening_statement || '')
const newFeatures = produce(features, (draft) => { const newFeatures = produce(features, (draft) => {
draft.opening = { draft.opening = {
...draft.opening, ...draft.opening,
@ -272,10 +273,13 @@ const Prompt: FC<ISimplePromptInput> = ({
{showAutomatic && ( {showAutomatic && (
<GetAutomaticResModal <GetAutomaticResModal
flowId={appId}
mode={mode as AppType} mode={mode as AppType}
isShow={showAutomatic} isShow={showAutomatic}
onClose={showAutomaticFalse} onClose={showAutomaticFalse}
onFinished={handleAutomaticRes} onFinished={handleAutomaticRes}
currentPrompt={promptTemplate}
isBasicMode
/> />
)} )}
</div> </div>

@ -14,24 +14,18 @@ import {
RiTranslate, RiTranslate,
RiUser2Line, RiUser2Line,
} from '@remixicon/react' } from '@remixicon/react'
import cn from 'classnames'
import s from './style.module.css' import s from './style.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { generateRule } from '@/service/debug' import { generateBasicAppFistTimeRule, generateRule } from '@/service/debug'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import type { CompletionParams, Model } from '@/types/app' import type { CompletionParams, Model } from '@/types/app'
import { AppType } from '@/types/app' import type { AppType } from '@/types/app'
import ConfigVar from '@/app/components/app/configuration/config-var'
import GroupName from '@/app/components/app/configuration/base/group-name'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
// type // type
import type { AutomaticRes } from '@/service/debug' import type { GenRes } from '@/service/debug'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@ -39,13 +33,25 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { ModelModeType } from '@/types/app' import type { ModelModeType } from '@/types/app'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
import InstructionEditorInBasic from './instruction-editor'
import { GeneratorType } from './types'
import Result from './result'
import useGenData from './use-gen-data'
import IdeaOutput from './idea-output'
import ResPlaceholder from './res-placeholder'
import { useGenerateRuleTemplate } from '@/service/use-apps'
const i18nPrefix = 'appDebug.generate'
export type IGetAutomaticResProps = { export type IGetAutomaticResProps = {
mode: AppType mode: AppType
isShow: boolean isShow: boolean
onClose: () => void onClose: () => void
onFinished: (res: AutomaticRes) => void onFinished: (res: GenRes) => void
isInLLMNode?: boolean flowId?: string
nodeId?: string
currentPrompt?: string
isBasicMode?: boolean
} }
const TryLabel: FC<{ const TryLabel: FC<{
@ -68,7 +74,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
mode, mode,
isShow, isShow,
onClose, onClose,
isInLLMNode, flowId,
nodeId,
currentPrompt,
isBasicMode,
onFinished, onFinished,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -124,12 +133,25 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
] ]
const [instruction, setInstruction] = useState<string>('') const [instruction, setInstruction] = useState<string>('')
const [ideaOutput, setIdeaOutput] = useState<string>('')
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const handleChooseTemplate = useCallback((key: string) => { const handleChooseTemplate = useCallback((key: string) => {
return () => { return () => {
const template = t(`appDebug.generate.template.${key}.instruction`) const template = t(`appDebug.generate.template.${key}.instruction`)
setInstruction(template) setInstruction(template)
setEditorKey(`${flowId}-${Date.now()}`)
} }
}, [t]) }, [t])
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.prompt, isBasicMode)
useEffect(() => {
if (!instruction && instructionTemplate) {
setInstruction(instructionTemplate.data)
setEditorKey(`${flowId}-${Date.now()}`)
}
}, [instructionTemplate])
const isValid = () => { const isValid = () => {
if (instruction.trim() === '') { if (instruction.trim() === '') {
Toast.notify({ Toast.notify({
@ -143,7 +165,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
return true return true
} }
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false) const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = useState<AutomaticRes | null>(null) const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}`}`
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
storageKey,
})
useEffect(() => { useEffect(() => {
if (defaultModel) { if (defaultModel) {
@ -170,16 +195,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
</div> </div>
) )
const renderNoData = (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='h-14 w-14 text-text-tertiary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.generate.noDataLine1')}</div>
<div>{t('appDebug.generate.noDataLine2')}</div>
</div>
</div>
)
const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => { const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => {
const newModel = { const newModel = {
...model, ...model,
@ -207,28 +222,59 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
return return
setLoadingTrue() setLoadingTrue()
try { try {
const { error, ...res } = await generateRule({ let apiRes: GenRes
instruction, let hasError = false
model_config: model, if (isBasicMode && !currentPrompt) {
no_variable: !!isInLLMNode, const { error, ...res } = await generateBasicAppFistTimeRule({
}) instruction,
setRes(res) model_config: model,
if (error) { no_variable: false,
Toast.notify({ })
type: 'error', apiRes = {
message: error, ...res,
modified: res.prompt,
} as GenRes
if (error) {
hasError = true
Toast.notify({
type: 'error',
message: error,
})
}
}
else {
const { error, ...res } = await generateRule({
flow_id: flowId,
node_id: nodeId,
current: currentPrompt,
instruction,
idea_output: ideaOutput,
model_config: model,
}) })
apiRes = res
if (error) {
hasError = true
Toast.notify({
type: 'error',
message: error,
})
}
} }
if (!hasError)
addVersion(apiRes)
} }
finally { finally {
setLoadingFalse() setLoadingFalse()
} }
} }
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false) const [isShowConfirmOverwrite, {
setTrue: showConfirmOverwrite,
setFalse: hideShowConfirmOverwrite,
}] = useBoolean(false)
const isShowAutoPromptResPlaceholder = () => { const isShowAutoPromptResPlaceholder = () => {
return !isLoading && !res return !isLoading && !current
} }
return ( return (
@ -236,15 +282,14 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
isShow={isShow} isShow={isShow}
onClose={onClose} onClose={onClose}
className='min-w-[1140px] !p-0' className='min-w-[1140px] !p-0'
closable
> >
<div className='flex h-[680px] flex-wrap'> <div className='flex h-[680px] flex-wrap'>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'> <div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
<div className='mb-8'> <div className='mb-4'>
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div> <div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div> <div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div>
</div> </div>
<div className='mb-8'> <div>
<ModelParameterModal <ModelParameterModal
popupClassName='!w-[520px]' popupClassName='!w-[520px]'
portalToFollowElemContentClassName='z-[1000]' portalToFollowElemContentClassName='z-[1000]'
@ -258,116 +303,99 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
hideDebugWithMultipleModel hideDebugWithMultipleModel
/> />
</div> </div>
<div > {isBasicMode && (
<div className='flex items-center'> <div className='mt-4'>
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div> <div className='flex items-center'>
<div className='h-px grow' style={{ <div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))', <div className='h-px grow' style={{
}}></div> background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}></div>
</div>
<div className='flex flex-wrap'>
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`appDebug.generate.template.${item.key}.name`)}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div>
</div> </div>
<div className='flex flex-wrap'> )}
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`appDebug.generate.template.${item.key}.name`)}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div>
</div>
{/* inputs */} {/* inputs */}
<div className='mt-6'> <div className='mt-4'>
<div className='text-[0px]'> <div>
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.generate.instruction')}</div> <div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.generate.instruction')}</div>
<Textarea {isBasicMode ? (
className="h-[200px] resize-none" <InstructionEditorInBasic
placeholder={t('appDebug.generate.instructionPlaceHolder') as string} editorKey={editorKey}
value={instruction} generatorType={GeneratorType.prompt}
onChange={e => setInstruction(e.target.value)} /> value={instruction}
onChange={setInstruction}
availableVars={[]}
availableNodes={[]}
isShowCurrentBlock={!!currentPrompt}
isShowLastRunBlock={false}
/>
) : (
<InstructionEditorInWorkflow
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
nodeId={nodeId || ''}
isShowCurrentBlock={!!currentPrompt}
/>
)}
</div> </div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className='mt-5 flex justify-end'> <div className='mt-7 flex justify-end space-x-2'>
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
<Button <Button
className='flex space-x-1' className='flex space-x-1'
variant='primary' variant='primary'
onClick={onGenerate} onClick={onGenerate}
disabled={isLoading} disabled={isLoading}
> >
<Generator className='h-4 w-4 text-white' /> <Generator className='h-4 w-4' />
<span className='text-xs font-semibold text-white'>{t('appDebug.generate.generate')}</span> <span className='text-xs font-semibold'>{t('appDebug.generate.generate')}</span>
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{(!isLoading && res) && ( {(!isLoading && current) && (
<div className='h-full w-0 grow p-6 pb-0'> <div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div> <Result
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}> current={current!}
<ConfigPrompt isBasicMode={isBasicMode}
mode={mode} nodeId={nodeId!}
promptTemplate={res?.prompt || ''} currentVersionIndex={currentVersionIndex || 0}
promptVariables={[]} setCurrentVersionIndex={setCurrentVersionIndex}
readonly versions={versions || []}
noTitle={isInLLMNode} onApply={showConfirmOverwrite}
gradientBorder generatorType={GeneratorType.prompt}
editorHeight={isInLLMNode ? 524 : 0} />
noResize={isInLLMNode}
/>
{!isInLLMNode && (
<>
{(res?.variables?.length && res?.variables?.length > 0)
? (
<ConfigVar
promptVariables={res?.variables.map(key => ({ key, name: key, type: 'string', required: true })) || []}
readonly
/>
)
: ''}
{(mode !== AppType.completion && res?.opening_statement) && (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<div
className='mb-1 rounded-xl border-l-[0.5px] border-t-[0.5px] border-effects-highlight bg-background-section-burn p-3'
>
<div className='mb-2 flex items-center gap-2'>
<div className='shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-light-blue-light-500 p-1 shadow-xs'>
<LoveMessage className='h-4 w-4 text-text-primary-on-surface' />
</div>
<div className='system-sm-semibold flex grow items-center text-text-secondary'>
{t('appDebug.feature.conversationOpener.title')}
</div>
</div>
<div className='system-xs-regular min-h-8 text-text-tertiary'>{res.opening_statement}</div>
</div>
</div>
)}
</>
)}
</div>
<div className='flex justify-end bg-background-default py-4'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.generate.apply')}</Button>
</div>
</div> </div>
)} )}
{isLoading && renderLoading} {isLoading && renderLoading}
{isShowAutoPromptResPlaceholder() && renderNoData} {isShowAutoPromptResPlaceholder() && <ResPlaceholder />}
{showConfirmOverwrite && ( {isShowConfirmOverwrite && (
<Confirm <Confirm
title={t('appDebug.generate.overwriteTitle')} title={t('appDebug.generate.overwriteTitle')}
content={t('appDebug.generate.overwriteMessage')} content={t('appDebug.generate.overwriteMessage')}
isShow={showConfirmOverwrite} isShow
onConfirm={() => { onConfirm={() => {
setShowConfirmOverwrite(false) hideShowConfirmOverwrite()
onFinished(res!) onFinished(current!)
}} }}
onCancel={() => setShowConfirmOverwrite(false)} onCancel={hideShowConfirmOverwrite}
/> />
)} )}
</div> </div>

@ -0,0 +1,48 @@
'use client'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import { useBoolean } from 'ahooks'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import Textarea from '@/app/components/base/textarea'
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'appDebug.generate'
type Props = {
value: string
onChange: (value: string) => void
}
const IdeaOutput: FC<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
const [isFoldIdeaOutput, {
toggle: toggleFoldIdeaOutput,
}] = useBoolean(true)
return (
<div className='mt-4 text-[0px]'>
<div
className='mb-1.5 flex cursor-pointer items-center text-sm font-medium leading-5 text-text-primary'
onClick={toggleFoldIdeaOutput}
>
<div className='system-sm-semibold-uppercase mr-1 text-text-secondary'>{t(`${i18nPrefix}.idealOutput`)}</div>
<div className='system-xs-regular text-text-tertiary'>({t(`${i18nPrefix}.optional`)})</div>
<ArrowDownRoundFill className={cn('size text-text-quaternary', isFoldIdeaOutput && 'relative top-[1px] rotate-[-90deg]')} />
</div>
{!isFoldIdeaOutput && (
<Textarea
className="h-[80px]"
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`)}
value={value}
onChange={e => onChange(e.target.value)}
/>
)}
</div>
)
}
export default React.memo(IdeaOutput)

@ -0,0 +1,58 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { GeneratorType } from './types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import InstructionEditor from './instruction-editor'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'
type Props = {
nodeId: string
value: string
editorKey: string
onChange: (text: string) => void
generatorType: GeneratorType
isShowCurrentBlock: boolean
}
const InstructionEditorInWorkflow: FC<Props> = ({
nodeId,
value,
editorKey,
onChange,
generatorType,
isShowCurrentBlock,
}) => {
const workflowStore = useWorkflowStore()
const filterVar = useCallback((payload: Var, selector: ValueSelector) => {
const { nodesWithInspectVars } = workflowStore.getState()
const nodeId = selector?.[0]
return !!nodesWithInspectVars.find(node => node.nodeId === nodeId) && payload.type !== VarType.file && payload.type !== VarType.arrayFile
}, [workflowStore])
const {
availableVars,
availableNodes,
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar,
})
const getVarType = useWorkflowVariableType()
return (
<InstructionEditor
value={value}
onChange={onChange}
editorKey={editorKey}
generatorType={generatorType}
availableVars={availableVars}
availableNodes={availableNodes}
getVarType={getVarType}
isShowCurrentBlock={isShowCurrentBlock}
isShowLastRunBlock
/>
)
}
export default React.memo(InstructionEditorInWorkflow)

@ -0,0 +1,117 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import PromptEditor from '@/app/components/base/prompt-editor'
import type { GeneratorType } from './types'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type Props = {
editorKey: string
value: string
onChange: (text: string) => void
generatorType: GeneratorType
availableVars: NodeOutPutVar[]
availableNodes: Node[]
getVarType?: (params: {
nodeId: string,
valueSelector: ValueSelector,
}) => Type
isShowCurrentBlock: boolean
isShowLastRunBlock: boolean
}
const i18nPrefix = 'appDebug.generate'
const InstructionEditor: FC<Props> = ({
editorKey,
generatorType,
value,
onChange,
availableVars,
availableNodes,
getVarType = () => Type.string,
isShowCurrentBlock,
isShowLastRunBlock,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
const isBasicMode = !!getVarType
// const [controlPromptEditorRerenderKey] =
const isCode = generatorType === 'code'
const placeholder = (
<div className='system-sm-regular text-text-placeholder'>
<div className='leading-6'>{t(`${i18nPrefix}.instructionPlaceHolderTitle`)}</div>
<div className='mt-2'>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine1`)}</div>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine2`)}</div>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine3`)}</div>
</div>
</div>
)
const handleInsertVariable = () => {
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId: editorKey } as any)
}
return (
<div className='relative'>
<PromptEditor
wrapperClassName='border !border-components-input-bg-normal bg-components-input-bg-normal hover:!border-components-input-bg-hover rounded-[10px] px-4 pt-3'
key={editorKey}
instanceId={editorKey}
placeholder={placeholder}
placeholderClassName='px-4 pt-3'
className={cn('min-h-[240px] pb-8')}
value={value}
workflowVariableBlock={{
show: true,
variables: availableVars,
getVarType,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
currentBlock={{
show: isShowCurrentBlock,
generatorType,
}}
errorMessageBlock={{
show: isCode,
}}
lastRunBlock={{
show: isShowLastRunBlock,
}}
onChange={onChange}
editable
isSupportFileVar={false}
/>
<div className='system-xs-regular absolute bottom-0 left-3 flex h-8 items-center space-x-0.5 text-components-input-text-placeholder'>
<span>{t('appDebug.generate.press')}</span>
<span className='system-kbd flex h-4 w-3.5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray text-text-placeholder'>/</span>
<span>{t('appDebug.generate.to')}</span>
<span onClick={handleInsertVariable} className='!ml-1 cursor-pointer hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary'>{t('appDebug.generate.insertContext')}</span>
</div>
</div>
)
}
export default React.memo(InstructionEditor)

@ -0,0 +1,55 @@
'use client'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import type { FC } from 'react'
import React from 'react'
import PromptRes from './prompt-res'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
type Props = {
value: string
nodeId: string
}
const PromptResInWorkflow: FC<Props> = ({
value,
nodeId,
}) => {
const { t } = useTranslation()
const {
availableVars,
availableNodes,
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: _payload => true,
})
return (
<PromptRes
value={value}
workflowVariableBlock={{
show: true,
variables: availableVars || [],
getVarType: () => Type.string,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
>
</PromptRes>
)
}
export default React.memo(PromptResInWorkflow)

@ -0,0 +1,31 @@
'use client'
import PromptEditor from '@/app/components/base/prompt-editor'
import type { WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
import type { FC } from 'react'
import React, { useEffect } from 'react'
type Props = {
value: string
workflowVariableBlock: WorkflowVariableBlockType
}
const keyIdPrefix = 'prompt-res-editor'
const PromptRes: FC<Props> = ({
value,
workflowVariableBlock,
}) => {
const [editorKey, setEditorKey] = React.useState<string>('keyIdPrefix-0')
useEffect(() => {
setEditorKey(`${keyIdPrefix}-${Date.now()}`)
}, [value])
return (
<PromptEditor
key={editorKey}
value={value}
editable={false}
className='h-full bg-transparent pt-0'
workflowVariableBlock={workflowVariableBlock}
/>
)
}
export default React.memo(PromptRes)

@ -0,0 +1,51 @@
import { RiCloseLine, RiInformation2Fill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import React from 'react'
import cn from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown'
type Props = {
message: string
className?: string
}
const PromptToast = ({
message,
className,
}: Props) => {
const [isHide, {
setTrue: hide,
}] = useBoolean(false)
// const message = `
// # h1
// **strong text** ~~strikethrough~~
// * list1
// * list2
// xxxx
// ## h2
// \`\`\`python
// print('Hello, World!')
// \`\`\`
// `
if (isHide)
return
return (
<div className={cn('relative flex items-center p-2 ', className)}>
{/* Background Effect */}
<div className="pointer-events-none absolute inset-0 rounded-lg bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)] opacity-40 shadow-md"></div>
<div className='relative flex h-full w-full justify-between'>
<div className="flex h-full w-0 grow gap-1">
<RiInformation2Fill className="mt-[3px] size-4 shrink-0 text-text-accent" />
<Markdown className="w-0 grow text-sm" content={message} />
</div>
<div className='relative top-[-1px] shrink-0 cursor-pointer p-0.5' onClick={hide}>
<RiCloseLine className='size-5 text-text-tertiary' />
</div>
</div>
</div>
)
}
export default PromptToast

@ -0,0 +1,20 @@
'use client'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Link from 'next/link'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
const ResPlaceholder: FC = () => {
const { t } = useTranslation()
return (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='size-8 text-text-quaternary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.generate.newNoDataLine1')}</div>
<Link className='text-text-accent' href='//todo' target='_blank'>{t('appDebug.generate.newNoDataLine2')}</Link>
</div>
</div>
)
}
export default React.memo(ResPlaceholder)

@ -0,0 +1,96 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { GeneratorType } from './types'
import PromptToast from './prompt-toast'
import Button from '@/app/components/base/button'
import VersionSelector from './version-selector'
import type { GenRes } from '@/service/debug'
import { RiClipboardLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import Toast from '@/app/components/base/toast'
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
import PromptRes from './prompt-res'
import PromptResInWorkflow from './prompt-res-in-workflow'
import cn from '@/utils/classnames'
type Props = {
isBasicMode?: boolean
nodeId?: string
current: GenRes
currentVersionIndex: number
setCurrentVersionIndex: (index: number) => void
versions: GenRes[]
onApply: () => void
generatorType: GeneratorType
}
const Result: FC<Props> = ({
isBasicMode,
nodeId,
current,
currentVersionIndex,
setCurrentVersionIndex,
versions,
onApply,
generatorType,
}) => {
const { t } = useTranslation()
const isGeneratorPrompt = generatorType === GeneratorType.prompt
return (
<div className='flex h-full flex-col'>
<div className='mb-3 flex shrink-0 items-center justify-between'>
<div>
<div className='shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
<VersionSelector
versionLen={versions.length}
value={currentVersionIndex}
onChange={setCurrentVersionIndex}
/>
</div>
<div className='flex items-center space-x-2'>
<Button className='px-2' onClick={() => {
copy(current.modified)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<RiClipboardLine className='h-4 w-4 text-text-secondary' />
</Button>
<Button variant='primary' onClick={onApply}>
{t('appDebug.generate.apply')}
</Button>
</div>
</div>
{
current?.message && (
<PromptToast message={current.message} className='mt-4 shrink-0' />
)
}
<div className={cn('mt-3 grow', isGeneratorPrompt && 'overflow-y-auto')}>
{isGeneratorPrompt ? (
isBasicMode ? (
<PromptRes
value={current?.modified}
workflowVariableBlock={{
show: false,
}}
/>
) : (<PromptResInWorkflow
value={current?.modified || ''}
nodeId={nodeId!}
/>)
) : (
<CodeEditor
editorWrapperClassName='h-full'
className='bg-transparent pt-0'
value={current?.modified}
readOnly
hideTopMenu
/>
)}
</div>
</div>
)
}
export default React.memo(Result)

@ -0,0 +1,4 @@
export enum GeneratorType {
prompt = 'prompt',
code = 'code',
}

@ -0,0 +1,36 @@
import type { GenRes } from '@/service/debug'
import { useSessionStorageState } from 'ahooks'
import { useCallback } from 'react'
type Params = {
storageKey: string
}
const keyPrefix = 'gen-data-'
const useGenData = ({ storageKey }: Params) => {
const [versions, setVersions] = useSessionStorageState<GenRes[]>(`${keyPrefix}${storageKey}-versions`, {
defaultValue: [],
})
const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, {
defaultValue: 0,
})
const current = versions?.[currentVersionIndex || 0]
const addVersion = useCallback((version: GenRes) => {
setCurrentVersionIndex(() => versions?.length || 0)
setVersions((prev) => {
return [...prev!, version]
})
}, [setVersions, setCurrentVersionIndex, versions?.length])
return {
versions,
addVersion,
currentVersionIndex,
setCurrentVersionIndex,
current,
}
}
export default useGenData

@ -0,0 +1,101 @@
import React, { useCallback } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { useBoolean } from 'ahooks'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
type Option = {
label: string
value: number
}
type VersionSelectorProps = {
versionLen: number;
value: number;
onChange: (index: number) => void;
}
const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => {
const [isOpen, {
setFalse: handleOpenFalse,
toggle: handleOpenToggle,
set: handleOpenSet,
}] = useBoolean(false)
const moreThanOneVersion = versionLen > 1
const handleOpen = useCallback((value: boolean) => {
if (moreThanOneVersion)
handleOpenSet(value)
}, [moreThanOneVersion, handleOpenToggle])
const handleToggle = useCallback(() => {
if (moreThanOneVersion)
handleOpenToggle()
}, [moreThanOneVersion, handleOpenToggle])
const versions = Array.from({ length: versionLen }, (_, index) => ({
label: `Version ${index + 1}${index === versionLen - 1 ? ' · Latest' : ''}`,
value: index,
}))
const isLatest = value === versionLen - 1
return (
<PortalToFollowElem
placement={'bottom-start'}
offset={{
mainAxis: 4,
crossAxis: -8,
}}
open={isOpen}
onOpenChange={handleOpen}
>
<PortalToFollowElemTrigger
onClick={handleToggle}
asChild
>
<div className={cn('system-xs-medium flex items-center text-text-secondary', moreThanOneVersion && 'cursor-pointer')}>
<div>Version {value + 1}{isLatest && ' · Latest'}</div>
{moreThanOneVersion && <RiArrowDownSLine className='size-3 ' />}
</div>
</PortalToFollowElemTrigger >
<PortalToFollowElemContent className={cn(
'z-[99]',
)}>
<div
className={cn(
'w-[208px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
)}
>
<div className='system-xs-medium-uppercase flex h-[22px] items-center px-3 pl-3 text-text-tertiary'>
Versions
</div>
{
versions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
)}
title={option.label}
onClick={() => {
onChange(option.value)
handleOpenFalse()
}}
>
<div className='mr-1 grow truncate px-1 pl-1'>
{option.label}
</div>
{
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default VersionSelector

@ -1,16 +1,13 @@
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import cn from 'classnames'
import useBoolean from 'ahooks/lib/useBoolean' import useBoolean from 'ahooks/lib/useBoolean'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ConfigPrompt from '../../config-prompt'
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index' import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
import { generateRuleCode } from '@/service/debug' import { generateRule } from '@/service/debug'
import type { CodeGenRes } from '@/service/debug' import type { GenRes } from '@/service/debug'
import type { ModelModeType } from '@/types/app' import type { ModelModeType } from '@/types/app'
import type { AppType, CompletionParams, Model } from '@/types/app' import type { AppType, CompletionParams, Model } from '@/types/app'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
@ -21,17 +18,31 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import IdeaOutput from '../automatic/idea-output'
import { GeneratorType } from '../automatic/types'
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
import useGenData from '../automatic/use-gen-data'
import Result from '../automatic/result'
import ResPlaceholder from '../automatic/res-placeholder'
import { useGenerateRuleTemplate } from '@/service/use-apps'
const i18nPrefix = 'appDebug.generate'
export type IGetCodeGeneratorResProps = { export type IGetCodeGeneratorResProps = {
flowId: string
nodeId: string
currentCode?: string
mode: AppType mode: AppType
isShow: boolean isShow: boolean
codeLanguages: CodeLanguage codeLanguages: CodeLanguage
onClose: () => void onClose: () => void
onFinished: (res: CodeGenRes) => void onFinished: (res: GenRes) => void
} }
export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
{ {
flowId,
nodeId,
currentCode,
mode, mode,
isShow, isShow,
codeLanguages, codeLanguages,
@ -61,9 +72,23 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
const { const {
defaultModel, defaultModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration) } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const [instruction, setInstruction] = React.useState<string>('') const [instruction, setInstruction] = useState<string>('')
const [ideaOutput, setIdeaOutput] = useState<string>('')
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false) const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = React.useState<CodeGenRes | null>(null) const storageKey = `${flowId}-${nodeId}`
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
storageKey,
})
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.code)
useEffect(() => {
if (!instruction && instructionTemplate) {
setInstruction(instructionTemplate.data)
setEditorKey(`${flowId}-${Date.now()}`)
}
}, [instructionTemplate])
const isValid = () => { const isValid = () => {
if (instruction.trim() === '') { if (instruction.trim() === '') {
Toast.notify({ Toast.notify({
@ -97,7 +122,6 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [model, setModel]) }, [model, setModel])
const isInLLMNode = true
const onGenerate = async () => { const onGenerate = async () => {
if (!isValid()) if (!isValid())
return return
@ -105,25 +129,35 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
return return
setLoadingTrue() setLoadingTrue()
try { try {
const { error, ...res } = await generateRuleCode({ const { error, ...res } = await generateRule({
flow_id: flowId,
node_id: nodeId,
current: currentCode,
instruction, instruction,
model_config: model, model_config: model,
no_variable: !!isInLLMNode, idea_output: ideaOutput,
code_language: languageMap[codeLanguages] || 'javascript', language: languageMap[codeLanguages] || 'javascript',
}) })
setRes(res)
if (error) { if (error) {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
message: error, message: error,
}) })
} }
else {
addVersion(res)
}
} }
finally { finally {
setLoadingFalse() setLoadingFalse()
} }
} }
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
const [isShowConfirmOverwrite, {
setTrue: showConfirmOverwrite,
setFalse: hideShowConfirmOverwrite,
}] = useBoolean(false)
useEffect(() => { useEffect(() => {
if (defaultModel) { if (defaultModel) {
@ -155,26 +189,16 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
<div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div> <div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div>
</div> </div>
) )
const renderNoData = (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='h-14 w-14 text-text-tertiary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.codegen.noDataLine1')}</div>
<div>{t('appDebug.codegen.noDataLine2')}</div>
</div>
</div>
)
return ( return (
<Modal <Modal
isShow={isShow} isShow={isShow}
onClose={onClose} onClose={onClose}
className='min-w-[1140px] !p-0' className='min-w-[1140px] !p-0'
closable
> >
<div className='relative flex h-[680px] flex-wrap'> <div className='relative flex h-[680px] flex-wrap'>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-8'> <div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
<div className='mb-8'> <div className='mb-4'>
<div className={'text-lg font-bold leading-[28px] text-text-primary'}>{t('appDebug.codegen.title')}</div> <div className={'text-lg font-bold leading-[28px] text-text-primary'}>{t('appDebug.codegen.title')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div> <div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div>
</div> </div>
@ -194,84 +218,60 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
</div> </div>
<div> <div>
<div className='text-[0px]'> <div className='text-[0px]'>
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.codegen.instruction')}</div> <div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.codegen.instruction')}</div>
<Textarea <InstructionEditor
className="h-[200px] resize-none" editorKey={editorKey}
placeholder={t('appDebug.codegen.instructionPlaceholder') || ''}
value={instruction} value={instruction}
onChange={e => setInstruction(e.target.value)} onChange={setInstruction}
nodeId={nodeId}
generatorType={GeneratorType.code}
isShowCurrentBlock={!!currentCode}
/> />
</div> </div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className='mt-5 flex justify-end'> <div className='mt-7 flex justify-end space-x-2'>
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
<Button <Button
className='flex space-x-1' className='flex space-x-1'
variant='primary' variant='primary'
onClick={onGenerate} onClick={onGenerate}
disabled={isLoading} disabled={isLoading}
> >
<Generator className='h-4 w-4 text-white' /> <Generator className='h-4 w-4' />
<span className='text-xs font-semibold text-white'>{t('appDebug.codegen.generate')}</span> <span className='text-xs font-semibold '>{t('appDebug.codegen.generate')}</span>
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{isLoading && renderLoading} {isLoading && renderLoading}
{!isLoading && !res && renderNoData} {!isLoading && !current && <ResPlaceholder />}
{(!isLoading && res) && ( {(!isLoading && current) && (
<div className='h-full w-0 grow p-6 pb-0'> <div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.codegen.resTitle')}</div> <Result
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}> current={current!}
<ConfigPrompt currentVersionIndex={currentVersionIndex || 0}
mode={mode} setCurrentVersionIndex={setCurrentVersionIndex}
promptTemplate={res?.code || ''} versions={versions || []}
promptVariables={[]} onApply={showConfirmOverwrite}
readonly generatorType={GeneratorType.code}
noTitle={isInLLMNode} />
gradientBorder
editorHeight={isInLLMNode ? 524 : 0}
noResize={isInLLMNode}
/>
{!isInLLMNode && (
<>
{res?.code && (
<div className='mt-4'>
<h3 className='mb-2 text-sm font-medium text-text-primary'>{t('appDebug.codegen.generatedCode')}</h3>
<pre className='overflow-x-auto rounded-lg bg-gray-50 p-4'>
<code className={`language-${res.language}`}>
{res.code}
</code>
</pre>
</div>
)}
{res?.error && (
<div className='mt-4 rounded-lg bg-red-50 p-4'>
<p className='text-sm text-red-600'>{res.error}</p>
</div>
)}
</>
)}
</div>
<div className='flex justify-end bg-background-default py-4'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.codegen.apply')}</Button>
</div>
</div> </div>
)} )}
</div> </div>
{showConfirmOverwrite && ( {isShowConfirmOverwrite && (
<Confirm <Confirm
title={t('appDebug.codegen.overwriteConfirmTitle')} title={t('appDebug.codegen.overwriteConfirmTitle')}
content={t('appDebug.codegen.overwriteConfirmMessage')} content={t('appDebug.codegen.overwriteConfirmMessage')}
isShow={showConfirmOverwrite} isShow
onConfirm={() => { onConfirm={() => {
setShowConfirmOverwrite(false) hideShowConfirmOverwrite()
onFinished(res!) onFinished(current!)
}} }}
onCancel={() => setShowConfirmOverwrite(false)} onCancel={hideShowConfirmOverwrite}
/> />
)} )}
</Modal> </Modal>

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z" fill="black"/>
<path d="M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z" fill="black"/>
<path d="M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z" fill="black"/>
<path d="M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z" fill="black"/>
<path d="M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z" fill="black"/>
<path d="M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z" fill="black"/>
<path d="M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,53 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "CodeAssistant"
}

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './CodeAssistant.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'CodeAssistant'
export default Icon

@ -0,0 +1,55 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "MagicEdit"
}

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './MagicEdit.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'MagicEdit'
export default Icon

@ -3,6 +3,7 @@ export { default as Bookmark } from './Bookmark'
export { default as CheckDone01 } from './CheckDone01' export { default as CheckDone01 } from './CheckDone01'
export { default as Check } from './Check' export { default as Check } from './Check'
export { default as ChecklistSquare } from './ChecklistSquare' export { default as ChecklistSquare } from './ChecklistSquare'
export { default as CodeAssistant } from './CodeAssistant'
export { default as DotsGrid } from './DotsGrid' export { default as DotsGrid } from './DotsGrid'
export { default as Edit02 } from './Edit02' export { default as Edit02 } from './Edit02'
export { default as Edit04 } from './Edit04' export { default as Edit04 } from './Edit04'
@ -14,6 +15,7 @@ export { default as LinkExternal02 } from './LinkExternal02'
export { default as LogIn04 } from './LogIn04' export { default as LogIn04 } from './LogIn04'
export { default as LogOut01 } from './LogOut01' export { default as LogOut01 } from './LogOut01'
export { default as LogOut04 } from './LogOut04' export { default as LogOut04 } from './LogOut04'
export { default as MagicEdit } from './MagicEdit'
export { default as Menu01 } from './Menu01' export { default as Menu01 } from './Menu01'
export { default as Pin01 } from './Pin01' export { default as Pin01 } from './Pin01'
export { default as Pin02 } from './Pin02' export { default as Pin02 } from './Pin02'

@ -3,6 +3,10 @@ import { SupportUploadFileTypes, type ValueSelector } from '../../workflow/types
export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}' export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}' export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
export const CURRENT_PLACEHOLDER_TEXT = '{{#current#}}'
export const ERROR_MESSAGE_PLACEHOLDER_TEXT = '{{#error_message#}}'
export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}'
export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}' export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role' export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'

@ -1,7 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect } from 'react' import React, { useEffect } from 'react'
import type { import type {
EditorState, EditorState,
} from 'lexical' } from 'lexical'
@ -39,6 +39,22 @@ import {
WorkflowVariableBlockNode, WorkflowVariableBlockNode,
WorkflowVariableBlockReplacementBlock, WorkflowVariableBlockReplacementBlock,
} from './plugins/workflow-variable-block' } from './plugins/workflow-variable-block'
import {
CurrentBlock,
CurrentBlockNode,
CurrentBlockReplacementBlock,
} from './plugins/current-block'
import {
ErrorMessageBlock,
ErrorMessageBlockNode,
ErrorMessageBlockReplacementBlock,
} from './plugins/error-message-block'
import {
LastRunBlock,
LastRunBlockNode,
LastRunReplacementBlock,
} from './plugins/last-run-block'
import VariableBlock from './plugins/variable-block' import VariableBlock from './plugins/variable-block'
import VariableValueBlock from './plugins/variable-value-block' import VariableValueBlock from './plugins/variable-value-block'
import { VariableValueBlockNode } from './plugins/variable-value-block/node' import { VariableValueBlockNode } from './plugins/variable-value-block/node'
@ -48,8 +64,11 @@ import UpdateBlock from './plugins/update-block'
import { textToEditorState } from './utils' import { textToEditorState } from './utils'
import type { import type {
ContextBlockType, ContextBlockType,
CurrentBlockType,
ErrorMessageBlockType,
ExternalToolBlockType, ExternalToolBlockType,
HistoryBlockType, HistoryBlockType,
LastRunBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType,
@ -66,7 +85,7 @@ export type PromptEditorProps = {
compact?: boolean compact?: boolean
wrapperClassName?: string wrapperClassName?: string
className?: string className?: string
placeholder?: string | JSX.Element placeholder?: string | React.JSX.Element
placeholderClassName?: string placeholderClassName?: string
style?: React.CSSProperties style?: React.CSSProperties
value?: string value?: string
@ -80,6 +99,9 @@ export type PromptEditorProps = {
variableBlock?: VariableBlockType variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType workflowVariableBlock?: WorkflowVariableBlockType
currentBlock?: CurrentBlockType
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean isSupportFileVar?: boolean
} }
@ -102,6 +124,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock,
currentBlock,
errorMessageBlock,
lastRunBlock,
isSupportFileVar, isSupportFileVar,
}) => { }) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
@ -119,6 +144,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
QueryBlockNode, QueryBlockNode,
WorkflowVariableBlockNode, WorkflowVariableBlockNode,
VariableValueBlockNode, VariableValueBlockNode,
CurrentBlockNode,
ErrorMessageBlockNode,
LastRunBlockNode, // LastRunBlockNode is used for error message block replacement
], ],
editorState: textToEditorState(value || ''), editorState: textToEditorState(value || ''),
onError: (error: Error) => { onError: (error: Error) => {
@ -178,6 +206,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
variableBlock={variableBlock} variableBlock={variableBlock}
externalToolBlock={externalToolBlock} externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock} workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar} isSupportFileVar={isSupportFileVar}
/> />
<ComponentPickerBlock <ComponentPickerBlock
@ -188,6 +219,9 @@ const PromptEditor: FC<PromptEditorProps> = ({
variableBlock={variableBlock} variableBlock={variableBlock}
externalToolBlock={externalToolBlock} externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock} workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar} isSupportFileVar={isSupportFileVar}
/> />
{ {
@ -230,6 +264,35 @@ const PromptEditor: FC<PromptEditorProps> = ({
</> </>
) )
} }
{
currentBlock?.show && (
<>
<CurrentBlock {...currentBlock} />
<CurrentBlockReplacementBlock {...currentBlock} />
</>
)
}
{
errorMessageBlock?.show && (
<>
<ErrorMessageBlock {...errorMessageBlock} />
<ErrorMessageBlockReplacementBlock {...errorMessageBlock} />
</>
)
}
{
lastRunBlock?.show && (
<>
<LastRunBlock {...lastRunBlock} />
<LastRunReplacementBlock {...lastRunBlock} />
</>
)
}
{
isSupportFileVar && (
<VariableValueBlock />
)
}
<OnChangePlugin onChange={handleEditorChange} /> <OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} /> <OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
<UpdateBlock instanceId={instanceId} /> <UpdateBlock instanceId={instanceId} />

@ -4,8 +4,11 @@ import { $insertNodes } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { import type {
ContextBlockType, ContextBlockType,
CurrentBlockType,
ErrorMessageBlockType,
ExternalToolBlockType, ExternalToolBlockType,
HistoryBlockType, HistoryBlockType,
LastRunBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType,
@ -27,6 +30,7 @@ import { BracketsX } from '@/app/components/base/icons/src/vender/line/developme
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import { VarType } from '@/app/components/workflow/types'
export const usePromptOptions = ( export const usePromptOptions = (
contextBlock?: ContextBlockType, contextBlock?: ContextBlockType,
@ -267,17 +271,61 @@ export const useOptions = (
variableBlock?: VariableBlockType, variableBlock?: VariableBlockType,
externalToolBlockType?: ExternalToolBlockType, externalToolBlockType?: ExternalToolBlockType,
workflowVariableBlockType?: WorkflowVariableBlockType, workflowVariableBlockType?: WorkflowVariableBlockType,
currentBlockType?: CurrentBlockType,
errorMessageBlockType?: ErrorMessageBlockType,
lastRunBlockType?: LastRunBlockType,
queryString?: string, queryString?: string,
) => { ) => {
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
const variableOptions = useVariableOptions(variableBlock, queryString) const variableOptions = useVariableOptions(variableBlock, queryString)
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
const workflowVariableOptions = useMemo(() => { const workflowVariableOptions = useMemo(() => {
if (!workflowVariableBlockType?.show) if (!workflowVariableBlockType?.show)
return [] return []
const res = workflowVariableBlockType.variables || []
return workflowVariableBlockType.variables || [] if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
}, [workflowVariableBlockType]) res.unshift({
nodeId: 'error_message',
title: 'error_message',
isFlat: true,
vars: [
{
variable: 'error_message',
type: VarType.string,
},
],
})
}
if(lastRunBlockType?.show && res.findIndex(v => v.nodeId === 'last_run') === -1) {
res.unshift({
nodeId: 'last_run',
title: 'last_run',
isFlat: true,
vars: [
{
variable: 'last_run',
type: VarType.object,
},
],
})
}
if(currentBlockType?.show && res.findIndex(v => v.nodeId === 'current') === -1) {
const title = currentBlockType.generatorType === 'prompt' ? 'current_prompt' : 'current_code'
res.unshift({
nodeId: 'current',
title,
isFlat: true,
vars: [
{
variable: 'current',
type: VarType.string,
},
],
})
}
return res
}, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, lastRunBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType])
return useMemo(() => { return useMemo(() => {
return { return {

@ -17,8 +17,11 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin' import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import type { import type {
ContextBlockType, ContextBlockType,
CurrentBlockType,
ErrorMessageBlockType,
ExternalToolBlockType, ExternalToolBlockType,
HistoryBlockType, HistoryBlockType,
LastRunBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType,
@ -32,6 +35,10 @@ import type { PickerBlockMenuOption } from './menu'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import { KEY_ESCAPE_COMMAND } from 'lexical' import { KEY_ESCAPE_COMMAND } from 'lexical'
import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block'
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block'
import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block'
type ComponentPickerProps = { type ComponentPickerProps = {
triggerString: string triggerString: string
@ -41,6 +48,9 @@ type ComponentPickerProps = {
variableBlock?: VariableBlockType variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType workflowVariableBlock?: WorkflowVariableBlockType
currentBlock?: CurrentBlockType
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean isSupportFileVar?: boolean
} }
const ComponentPicker = ({ const ComponentPicker = ({
@ -51,6 +61,9 @@ const ComponentPicker = ({
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock,
currentBlock,
errorMessageBlock,
lastRunBlock,
isSupportFileVar, isSupportFileVar,
}: ComponentPickerProps) => { }: ComponentPickerProps) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
@ -87,6 +100,9 @@ const ComponentPicker = ({
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock,
currentBlock,
errorMessageBlock,
lastRunBlock,
) )
const onSelectOption = useCallback( const onSelectOption = useCallback(
@ -112,12 +128,23 @@ const ComponentPicker = ({
if (needRemove) if (needRemove)
needRemove.remove() needRemove.remove()
}) })
const isFlat = variables.length === 1
if (variables[1] === 'sys.query' || variables[1] === 'sys.files') if(isFlat) {
const varName = variables[0]
if(varName === 'current')
editor.dispatchCommand(INSERT_CURRENT_BLOCK_COMMAND, currentBlock?.generatorType)
else if (varName === 'error_message')
editor.dispatchCommand(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null)
else if (varName === 'last_run')
editor.dispatchCommand(INSERT_LAST_RUN_BLOCK_COMMAND, null)
}
else if (variables[1] === 'sys.query' || variables[1] === 'sys.files') {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]]) editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
else }
else {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
}, [editor, checkForTriggerMatch, triggerString]) }
}, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString])
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }) const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
@ -166,6 +193,7 @@ const ComponentPicker = ({
onClose={handleClose} onClose={handleClose}
onBlur={handleClose} onBlur={handleClose}
autoFocus={false} autoFocus={false}
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
/> />
</div> </div>
) )
@ -206,7 +234,7 @@ const ComponentPicker = ({
} }
</> </>
) )
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar]) }, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString])
return ( return (
<LexicalTypeaheadMenuPlugin <LexicalTypeaheadMenuPlugin

@ -0,0 +1,44 @@
import { type FC, useEffect } from 'react'
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useSelectOrDelete } from '../../hooks'
import { CurrentBlockNode, DELETE_CURRENT_BLOCK_COMMAND } from '.'
import cn from '@/utils/classnames'
import { CodeAssistant, MagicEdit } from '../../../icons/src/vender/line/general'
type CurrentBlockComponentProps = {
nodeKey: string
generatorType: GeneratorType
}
const CurrentBlockComponent: FC<CurrentBlockComponentProps> = ({
nodeKey,
generatorType,
}) => {
const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CURRENT_BLOCK_COMMAND)
const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
useEffect(() => {
if (!editor.hasNodes([CurrentBlockNode]))
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
}, [editor])
return (
<div
className={cn(
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-violet-violet-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
onClick={(e) => {
e.stopPropagation()
}}
ref={ref}
>
<Icon className='mr-0.5 h-[14px] w-[14px]' />
<div className='text-xs font-medium'>{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}</div>
</div>
)
}
export default CurrentBlockComponent

@ -0,0 +1,62 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { CURRENT_PLACEHOLDER_TEXT } from '../../constants'
import type { CurrentBlockType } from '../../types'
import {
$createCurrentBlockNode,
CurrentBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT)
const CurrentBlockReplacementBlock = ({
generatorType,
onInsert,
}: CurrentBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([CurrentBlockNode]))
throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor')
}, [editor])
const createCurrentBlockNode = useCallback((): CurrentBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createCurrentBlockNode(generatorType))
}, [onInsert, generatorType])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return null
}
export default memo(CurrentBlockReplacementBlock)

@ -0,0 +1,66 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { CurrentBlockType } from '../../types'
import {
$createCurrentBlockNode,
CurrentBlockNode,
} from './node'
export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND')
export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND')
const CurrentBlock = memo(({
generatorType,
onInsert,
onDelete,
}: CurrentBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([CurrentBlockNode]))
throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_CURRENT_BLOCK_COMMAND,
() => {
const currentBlockNode = $createCurrentBlockNode(generatorType)
$insertNodes([currentBlockNode])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_CURRENT_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, generatorType, onDelete, onInsert])
return null
})
CurrentBlock.displayName = 'CurrentBlock'
export { CurrentBlock }
export { CurrentBlockNode } from './node'
export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block'

@ -0,0 +1,78 @@
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import CurrentBlockComponent from './component'
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; }
export class CurrentBlockNode extends DecoratorNode<React.JSX.Element> {
__generatorType: GeneratorType
static getType(): string {
return 'current-block'
}
static clone(node: CurrentBlockNode): CurrentBlockNode {
return new CurrentBlockNode(node.__generatorType, node.getKey())
}
isInline(): boolean {
return true
}
constructor(generatorType: GeneratorType, key?: NodeKey) {
super(key)
this.__generatorType = generatorType
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): React.JSX.Element {
return (
<CurrentBlockComponent
nodeKey={this.getKey()}
generatorType={this.getGeneratorType()}
/>
)
}
getGeneratorType(): GeneratorType {
const self = this.getLatest()
return self.__generatorType
}
static importJSON(serializedNode: SerializedNode): CurrentBlockNode {
const node = $createCurrentBlockNode(serializedNode.generatorType)
return node
}
exportJSON(): SerializedNode {
return {
type: 'current-block',
version: 1,
generatorType: this.getGeneratorType(),
}
}
getTextContent(): string {
return '{{#current#}}'
}
}
export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode {
return new CurrentBlockNode(type)
}
export function $isCurrentBlockNode(
node: CurrentBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof CurrentBlockNode
}

@ -0,0 +1,40 @@
import { type FC, useEffect } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.'
import cn from '@/utils/classnames'
import { Variable02 } from '../../../icons/src/vender/solid/development'
type Props = {
nodeKey: string
}
const ErrorMessageBlockComponent: FC<Props> = ({
nodeKey,
}) => {
const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)
useEffect(() => {
if (!editor.hasNodes([ErrorMessageBlockNode]))
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
}, [editor])
return (
<div
className={cn(
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-orange-dark-orange-dark-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
onClick={(e) => {
e.stopPropagation()
}}
ref={ref}
>
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
<div className='text-xs font-medium'>error_message</div>
</div>
)
}
export default ErrorMessageBlockComponent

@ -0,0 +1,61 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
import type { ErrorMessageBlockType } from '../../types'
import {
$createErrorMessageBlockNode,
ErrorMessageBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(ERROR_MESSAGE_PLACEHOLDER_TEXT)
const ErrorMessageBlockReplacementBlock = ({
onInsert,
}: ErrorMessageBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([ErrorMessageBlockNode]))
throw new Error('ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor')
}, [editor])
const createErrorMessageBlockNode = useCallback((): ErrorMessageBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createErrorMessageBlockNode())
}, [onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + ERROR_MESSAGE_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return null
}
export default memo(ErrorMessageBlockReplacementBlock)

@ -0,0 +1,65 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { ErrorMessageBlockType } from '../../types'
import {
$createErrorMessageBlockNode,
ErrorMessageBlockNode,
} from './node'
export const INSERT_ERROR_MESSAGE_BLOCK_COMMAND = createCommand('INSERT_ERROR_MESSAGE_BLOCK_COMMAND')
export const DELETE_ERROR_MESSAGE_COMMAND = createCommand('DELETE_ERROR_MESSAGE_COMMAND')
const ErrorMessageBlock = memo(({
onInsert,
onDelete,
}: ErrorMessageBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([ErrorMessageBlockNode]))
throw new Error('ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
() => {
const Node = $createErrorMessageBlockNode()
$insertNodes([Node])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_ERROR_MESSAGE_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onDelete, onInsert])
return null
})
ErrorMessageBlock.displayName = 'ErrorMessageBlock'
export { ErrorMessageBlock }
export { ErrorMessageBlockNode } from './node'
export { default as ErrorMessageBlockReplacementBlock } from './error-message-block-replacement-block'

@ -0,0 +1,67 @@
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import ErrorMessageBlockComponent from './component'
export type SerializedNode = SerializedLexicalNode
export class ErrorMessageBlockNode extends DecoratorNode<React.JSX.Element> {
static getType(): string {
return 'error-message-block'
}
static clone(node: ErrorMessageBlockNode): ErrorMessageBlockNode {
return new ErrorMessageBlockNode(node.getKey())
}
isInline(): boolean {
return true
}
constructor(key?: NodeKey) {
super(key)
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): React.JSX.Element {
return (
<ErrorMessageBlockComponent
nodeKey={this.getKey()}
/>
)
}
static importJSON(): ErrorMessageBlockNode {
const node = $createErrorMessageBlockNode()
return node
}
exportJSON(): SerializedNode {
return {
type: 'error-message-block',
version: 1,
}
}
getTextContent(): string {
return '{{#error_message#}}'
}
}
export function $createErrorMessageBlockNode(): ErrorMessageBlockNode {
return new ErrorMessageBlockNode()
}
export function $isErrorMessageBlockNode(
node: ErrorMessageBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof ErrorMessageBlockNode
}

@ -0,0 +1,40 @@
import { type FC, useEffect } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.'
import cn from '@/utils/classnames'
import { Variable02 } from '../../../icons/src/vender/solid/development'
type Props = {
nodeKey: string
}
const LastRunBlockComponent: FC<Props> = ({
nodeKey,
}) => {
const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)
useEffect(() => {
if (!editor.hasNodes([LastRunBlockNode]))
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
}, [editor])
return (
<div
className={cn(
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-text-accent hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
onClick={(e) => {
e.stopPropagation()
}}
ref={ref}
>
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
<div className='text-xs font-medium'>last_run</div>
</div>
)
}
export default LastRunBlockComponent

@ -0,0 +1,65 @@
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { LastRunBlockType } from '../../types'
import {
$createLastRunBlockNode,
LastRunBlockNode,
} from './node'
export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND')
export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND')
const LastRunBlock = memo(({
onInsert,
onDelete,
}: LastRunBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([LastRunBlockNode]))
throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor')
return mergeRegister(
editor.registerCommand(
INSERT_LAST_RUN_BLOCK_COMMAND,
() => {
const Node = $createLastRunBlockNode()
$insertNodes([Node])
if (onInsert)
onInsert()
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_LAST_RUN_COMMAND,
() => {
if (onDelete)
onDelete()
return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onDelete, onInsert])
return null
})
LastRunBlock.displayName = 'LastRunBlock'
export { LastRunBlock }
export { LastRunBlockNode } from './node'
export { default as LastRunReplacementBlock } from './last-run-block-replacement-block'

@ -0,0 +1,61 @@
import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants'
import type { LastRunBlockType } from '../../types'
import {
$createLastRunBlockNode,
LastRunBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT)
const LastRunReplacementBlock = ({
onInsert,
}: LastRunBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([LastRunBlockNode]))
throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor')
}, [editor])
const createLastRunBlockNode = useCallback((): LastRunBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createLastRunBlockNode())
}, [onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return null
}
export default memo(LastRunReplacementBlock)

@ -0,0 +1,67 @@
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import LastRunBlockComponent from './component'
export type SerializedNode = SerializedLexicalNode
export class LastRunBlockNode extends DecoratorNode<React.JSX.Element> {
static getType(): string {
return 'last-run-block'
}
static clone(node: LastRunBlockNode): LastRunBlockNode {
return new LastRunBlockNode(node.getKey())
}
isInline(): boolean {
return true
}
constructor(key?: NodeKey) {
super(key)
}
createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}
updateDOM(): false {
return false
}
decorate(): React.JSX.Element {
return (
<LastRunBlockComponent
nodeKey={this.getKey()}
/>
)
}
static importJSON(): LastRunBlockNode {
const node = $createLastRunBlockNode()
return node
}
exportJSON(): SerializedNode {
return {
type: 'last-run-block',
version: 1,
}
}
getTextContent(): string {
return '{{#last_run#}}'
}
}
export function $createLastRunBlockNode(): LastRunBlockNode {
return new LastRunBlockNode()
}
export function $isLastRunBlockNode(
node: LastRunBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof LastRunBlockNode
}

@ -1,3 +1,4 @@
import type { GeneratorType } from '../../app/configuration/config/automatic/types'
import type { Type } from '../../workflow/nodes/llm/types' import type { Type } from '../../workflow/nodes/llm/types'
import type { Dataset } from './plugins/context-block' import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block' import type { RoleName } from './plugins/history-block'
@ -75,3 +76,22 @@ export type MenuTextMatch = {
matchingString: string matchingString: string
replaceableString: string replaceableString: string
} }
export type CurrentBlockType = {
show?: boolean
generatorType: GeneratorType
onInsert?: () => void
onDelete?: () => void
}
export type ErrorMessageBlockType = {
show?: boolean
onInsert?: () => void
onDelete?: () => void
}
export type LastRunBlockType = {
show?: boolean
onInsert?: () => void
onDelete?: () => void
}

@ -5,6 +5,7 @@ export const useConfigsMap = () => {
const appId = useStore(s => s.appId) const appId = useStore(s => s.appId)
return useMemo(() => { return useMemo(() => {
return { return {
flowId: appId,
conversationVarsUrl: `apps/${appId}/workflows/draft/conversation-variables`, conversationVarsUrl: `apps/${appId}/workflows/draft/conversation-variables`,
systemVarsUrl: `apps/${appId}/workflows/draft/system-variables`, systemVarsUrl: `apps/${appId}/workflows/draft/system-variables`,
} }

@ -35,7 +35,6 @@ export const useWorkflowRun = () => {
const invalidAllLastRun = useInvalidAllLastRun(appId as string) const invalidAllLastRun = useInvalidAllLastRun(appId as string)
const configsMap = useConfigsMap() const configsMap = useConfigsMap()
const { fetchInspectVars } = useSetWorkflowVarsWithValue({ const { fetchInspectVars } = useSetWorkflowVarsWithValue({
flowId: appId as string,
...configsMap, ...configsMap,
}) })

@ -49,6 +49,7 @@ type CommonHooksFnMap = {
resetConversationVar: (varId: string) => Promise<void> resetConversationVar: (varId: string) => Promise<void>
invalidateConversationVarValues: () => void invalidateConversationVarValues: () => void
configsMap?: { configsMap?: {
flowId: string
conversationVarsUrl: string conversationVarsUrl: string
systemVarsUrl: string systemVarsUrl: string
} }

@ -10,6 +10,7 @@ import type {
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import { useIsChatMode } from './use-workflow' import { useIsChatMode } from './use-workflow'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import type { Type } from '../nodes/llm/types'
export const useWorkflowVariables = () => { export const useWorkflowVariables = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -106,7 +107,7 @@ export const useWorkflowVariableType = () => {
isChatMode, isChatMode,
isConstant: false, isConstant: false,
}) })
return type return type as unknown as Type
} }
return getVarType return getVarType

@ -87,6 +87,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase' headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
containerBackgroundClassName='bg-transparent' containerBackgroundClassName='bg-transparent'
gradientBorder={false} gradientBorder={false}
nodeId={nodeId}
isSupportPromptGenerator={!!def.auto_generate?.type} isSupportPromptGenerator={!!def.auto_generate?.type}
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)} titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
editorContainerClassName='px-0' editorContainerClassName='px-0'

@ -7,25 +7,32 @@ import type { CodeLanguage } from '../../code/types'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import { ActionButton } from '@/app/components/base/action-button' import { ActionButton } from '@/app/components/base/action-button'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import type { CodeGenRes } from '@/service/debug' import type { GenRes } from '@/service/debug'
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res' import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
import { useHooksStore } from '../../../hooks-store'
type Props = { type Props = {
nodeId: string
currentCode?: string
className?: string className?: string
onGenerated?: (prompt: string) => void onGenerated?: (prompt: string) => void
codeLanguages: CodeLanguage codeLanguages: CodeLanguage
} }
const CodeGenerateBtn: FC<Props> = ({ const CodeGenerateBtn: FC<Props> = ({
nodeId,
currentCode,
className, className,
codeLanguages, codeLanguages,
onGenerated, onGenerated,
}) => { }) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = useCallback((res: CodeGenRes) => { const handleAutomaticRes = useCallback((res: GenRes) => {
onGenerated?.(res.code) onGenerated?.(res.modified)
showAutomaticFalse() showAutomaticFalse()
}, [onGenerated, showAutomaticFalse]) }, [onGenerated, showAutomaticFalse])
const configsMap = useHooksStore(s => s.configsMap)
return ( return (
<div className={cn(className)}> <div className={cn(className)}>
<ActionButton <ActionButton
@ -40,6 +47,9 @@ const CodeGenerateBtn: FC<Props> = ({
codeLanguages={codeLanguages} codeLanguages={codeLanguages}
onClose={showAutomaticFalse} onClose={showAutomaticFalse}
onFinished={handleAutomaticRes} onFinished={handleAutomaticRes}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentCode={currentCode}
/> />
)} )}
</div> </div>

@ -16,8 +16,10 @@ import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-tog
import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FileEntity } from '@/app/components/base/file-uploader/types'
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log' import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
type Props = { type Props = {
nodeId?: string
className?: string className?: string
title: React.JSX.Element | string title: React.JSX.Element | string
headerRight?: React.JSX.Element headerRight?: React.JSX.Element
@ -35,9 +37,12 @@ type Props = {
showFileList?: boolean showFileList?: boolean
showCodeGenerator?: boolean showCodeGenerator?: boolean
tip?: React.JSX.Element tip?: React.JSX.Element
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
} }
const Base: FC<Props> = ({ const Base: FC<Props> = ({
nodeId,
className, className,
title, title,
headerRight, headerRight,
@ -86,7 +91,12 @@ const Base: FC<Props> = ({
{headerRight} {headerRight}
{showCodeGenerator && codeLanguages && ( {showCodeGenerator && codeLanguages && (
<div className='ml-1'> <div className='ml-1'>
<CodeGeneratorButton onGenerated={onGenerated} codeLanguages={codeLanguages} /> <CodeGeneratorButton
onGenerated={onGenerated}
codeLanguages={codeLanguages}
currentCode={value}
nodeId={nodeId!}
/>
</div> </div>
)} )}
<ActionButton className='ml-1' onClick={handleCopy}> <ActionButton className='ml-1' onClick={handleCopy}>

@ -20,6 +20,7 @@ loader.config({ paths: { vs: `${basePath}/vs` } })
const CODE_EDITOR_LINE_HEIGHT = 18 const CODE_EDITOR_LINE_HEIGHT = 18
export type Props = { export type Props = {
nodeId?: string
value?: string | object value?: string | object
placeholder?: React.JSX.Element | string placeholder?: React.JSX.Element | string
onChange?: (value: string) => void onChange?: (value: string) => void
@ -47,6 +48,7 @@ export const languageMap = {
} }
const CodeEditor: FC<Props> = ({ const CodeEditor: FC<Props> = ({
nodeId,
value = '', value = '',
placeholder = '', placeholder = '',
onChange = noop, onChange = noop,
@ -175,6 +177,7 @@ const CodeEditor: FC<Props> = ({
</div> </div>
: ( : (
<Base <Base
nodeId={nodeId}
className='relative' className='relative'
title={title} title={title}
value={outPutValue} value={outPutValue}

@ -41,6 +41,7 @@ type Props = {
className?: string className?: string
headerClassName?: string headerClassName?: string
instanceId?: string instanceId?: string
nodeId?: string
title: string | React.JSX.Element title: string | React.JSX.Element
value: string value: string
onChange: (value: string) => void onChange: (value: string) => void
@ -83,6 +84,7 @@ const Editor: FC<Props> = ({
className, className,
headerClassName, headerClassName,
instanceId, instanceId,
nodeId,
title, title,
value, value,
onChange, onChange,
@ -159,7 +161,13 @@ const Editor: FC<Props> = ({
<div className='flex items-center'> <div className='flex items-center'>
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div> <div className='text-xs font-medium leading-[18px] text-text-tertiary'>{value?.length || 0}</div>
{isSupportPromptGenerator && ( {isSupportPromptGenerator && (
<PromptGeneratorBtn className='ml-[5px]' onGenerated={onGenerated} modelConfig={modelConfig} /> <PromptGeneratorBtn
nodeId={nodeId!}
className='ml-[5px]'
onGenerated={onGenerated}
modelConfig={modelConfig}
currentPrompt={value}
/>
)} )}
<div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div> <div className='ml-2 mr-2 h-3 w-px bg-divider-regular'></div>

@ -23,6 +23,7 @@ import type { Field } from '@/app/components/workflow/nodes/llm/types'
import { FILE_STRUCT } from '@/app/components/workflow/constants' import { FILE_STRUCT } from '@/app/components/workflow/constants'
import { Loop } from '@/app/components/base/icons/src/vender/workflow' import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
type ObjectChildrenProps = { type ObjectChildrenProps = {
nodeId: string nodeId: string
@ -46,7 +47,10 @@ type ItemProps = {
isSupportFileVar?: boolean isSupportFileVar?: boolean
isException?: boolean isException?: boolean
isLoopVar?: boolean isLoopVar?: boolean
isFlat?: boolean
isInCodeGeneratorInstructionEditor?: boolean
zIndex?: number zIndex?: number
className?: string
} }
const objVarTypes = [VarType.object, VarType.file] const objVarTypes = [VarType.object, VarType.file]
@ -61,7 +65,10 @@ const Item: FC<ItemProps> = ({
isSupportFileVar, isSupportFileVar,
isException, isException,
isLoopVar, isLoopVar,
isFlat,
isInCodeGeneratorInstructionEditor,
zIndex, zIndex,
className,
}) => { }) => {
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
const isFile = itemData.type === VarType.file && !isStructureOutput const isFile = itemData.type === VarType.file && !isStructureOutput
@ -69,6 +76,29 @@ const Item: FC<ItemProps> = ({
const isSys = itemData.variable.startsWith('sys.') const isSys = itemData.variable.startsWith('sys.')
const isEnv = itemData.variable.startsWith('env.') const isEnv = itemData.variable.startsWith('env.')
const isChatVar = itemData.variable.startsWith('conversation.') const isChatVar = itemData.variable.startsWith('conversation.')
const flatVarIcon = useMemo(() => {
if (!isFlat)
return null
const variable = itemData.variable
let Icon
switch (variable) {
case 'current':
Icon = isInCodeGeneratorInstructionEditor ? CodeAssistant : MagicEdit
return <Icon className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />
case 'error_message':
return <Variable02 className='h-3.5 w-3.5 shrink-0 text-util-colors-orange-dark-orange-dark-600' />
default:
return <Variable02 className='h-3.5 w-3.5 shrink-0 text-text-accent' />
}
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
const varName = useMemo(() => {
if (!isFlat)
return itemData.variable
if (itemData.variable === 'current')
return isInCodeGeneratorInstructionEditor ? 'current_code' : 'current_prompt'
return itemData.variable
}, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])
const objStructuredOutput: StructuredOutput | null = useMemo(() => { const objStructuredOutput: StructuredOutput | null = useMemo(() => {
if (!isObj) return null if (!isObj) return null
@ -125,7 +155,10 @@ const Item: FC<ItemProps> = ({
if (!isSupportFileVar && isFile) if (!isSupportFileVar && isFile)
return return
if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable if (isFlat) {
onChange([itemData.variable], itemData)
}
else if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
onChange([...objPath, ...itemData.variable.split('.')], itemData) onChange([...objPath, ...itemData.variable.split('.')], itemData)
} }
else { else {
@ -144,18 +177,21 @@ const Item: FC<ItemProps> = ({
className={cn( className={cn(
(isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]', (isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]',
isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'), isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3') 'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3',
className,
)
} }
onClick={handleChosen} onClick={handleChosen}
onMouseDown={e => e.preventDefault()} onMouseDown={e => e.preventDefault()}
> >
<div className='flex w-0 grow items-center'> <div className='flex w-0 grow items-center'>
{!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />} {!isFlat && !isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />} {isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />} {isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
{isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />} {isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />}
{isFlat && flatVarIcon}
{!isEnv && !isChatVar && ( {!isEnv && !isChatVar && (
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div> <div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
)} )}
{isEnv && ( {isEnv && (
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('env.', '')}</div> <div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('env.', '')}</div>
@ -263,6 +299,7 @@ type Props = {
onClose?: () => void onClose?: () => void
onBlur?: () => void onBlur?: () => void
zIndex?: number zIndex?: number
isInCodeGeneratorInstructionEditor?: boolean
autoFocus?: boolean autoFocus?: boolean
} }
const VarReferenceVars: FC<Props> = ({ const VarReferenceVars: FC<Props> = ({
@ -276,6 +313,7 @@ const VarReferenceVars: FC<Props> = ({
onClose, onClose,
onBlur, onBlur,
zIndex, zIndex,
isInCodeGeneratorInstructionEditor,
autoFocus = true, autoFocus = true,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -318,7 +356,7 @@ const VarReferenceVars: FC<Props> = ({
{ {
!hideSearch && ( !hideSearch && (
<> <>
<div className={cn('var-search-input-wrapper mx-2 mb-1 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}> <div className={cn('var-search-input-wrapper mx-2 mb-2 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
<Input <Input
className='var-search-input' className='var-search-input'
showLeftIcon showLeftIcon
@ -344,11 +382,13 @@ const VarReferenceVars: FC<Props> = ({
{ {
filteredVars.map((item, i) => ( filteredVars.map((item, i) => (
<div key={i}> <div key={i} className={cn(!item.isFlat && 'mt-3', i === 0 && item.isFlat && 'mt-2')}>
<div {!item.isFlat && (
className='system-xs-medium-uppercase truncate px-3 leading-[22px] text-text-tertiary' <div
title={item.title} className='system-xs-medium-uppercase truncate px-3 leading-[22px] text-text-tertiary'
>{item.title}</div> title={item.title}
>{item.title}</div>
)}
{item.vars.map((v, j) => ( {item.vars.map((v, j) => (
<Item <Item
key={j} key={j}
@ -361,13 +401,22 @@ const VarReferenceVars: FC<Props> = ({
isSupportFileVar={isSupportFileVar} isSupportFileVar={isSupportFileVar}
isException={v.isException} isException={v.isException}
isLoopVar={item.isLoop} isLoopVar={item.isLoop}
isFlat={item.isFlat}
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
zIndex={zIndex} zIndex={zIndex}
/> />
))} ))}
{item.isFlat && !filteredVars[i + 1]?.isFlat && !!filteredVars.find(item => !item.isFlat) && (
<div className='relative mt-[14px] flex items-center space-x-1'>
<div className='h-0 w-3 shrink-0 border border-divider-subtle'></div>
<div className='system-2xs-semibold-uppercase text-text-tertiary'>{t('workflow.debug.lastOutput')}</div>
<div className='h-0 shrink-0 grow border border-divider-subtle'></div>
</div>
)}
</div>)) </div>))
} }
</div> </div>
: <div className='pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>} : <div className='mt-2 pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>}
</> </>
) )
} }

@ -1,18 +1,19 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
type Params = { type Params = {
ref: React.RefObject<HTMLDivElement> ref?: React.RefObject<HTMLDivElement | null>
hasFooter?: boolean hasFooter?: boolean
isInNode?: boolean isInNode?: boolean
} }
const useToggleExpend = ({ ref, hasFooter = true, isInNode }: Params) => { const useToggleExpend = ({ ref, hasFooter = true, isInNode }: Params) => {
const [isExpand, setIsExpand] = useState(false) const [isExpand, setIsExpand] = useState(false)
const [wrapHeight, setWrapHeight] = useState(ref.current?.clientHeight) const [wrapHeight, setWrapHeight] = useState(ref?.current?.clientHeight)
const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0 const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0
useEffect(() => { useEffect(() => {
if (!ref?.current) return
setWrapHeight(ref.current?.clientHeight) setWrapHeight(ref.current?.clientHeight)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isExpand]) }, [isExpand])
const wrapClassName = (() => { const wrapClassName = (() => {

@ -89,6 +89,7 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
</Field> </Field>
<Split /> <Split />
<CodeEditor <CodeEditor
nodeId={id}
isInNode isInNode
readOnly={readOnly} readOnly={readOnly}
title={ title={

@ -1,7 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback } from 'react'
import { uniqueId } from 'lodash-es'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ModelConfig, PromptItem, Variable } from '../../../types' import type { ModelConfig, PromptItem, Variable } from '../../../types'
import { EditionType } from '../../../types' import { EditionType } from '../../../types'
@ -14,11 +13,13 @@ import { PromptRole } from '@/models/debug'
const i18nPrefix = 'workflow.nodes.llm' const i18nPrefix = 'workflow.nodes.llm'
type Props = { type Props = {
instanceId: string
className?: string className?: string
headerClassName?: string headerClassName?: string
canNotChooseSystemRole?: boolean canNotChooseSystemRole?: boolean
readOnly: boolean readOnly: boolean
id: string id: string
nodeId: string
canRemove: boolean canRemove: boolean
isChatModel: boolean isChatModel: boolean
isChatApp: boolean isChatApp: boolean
@ -58,11 +59,13 @@ const roleOptions = [
const roleOptionsWithoutSystemRole = roleOptions.filter(item => item.value !== PromptRole.system) const roleOptionsWithoutSystemRole = roleOptions.filter(item => item.value !== PromptRole.system)
const ConfigPromptItem: FC<Props> = ({ const ConfigPromptItem: FC<Props> = ({
instanceId,
className, className,
headerClassName, headerClassName,
canNotChooseSystemRole, canNotChooseSystemRole,
readOnly, readOnly,
id, id,
nodeId,
canRemove, canRemove,
handleChatModeMessageRoleChange, handleChatModeMessageRoleChange,
isChatModel, isChatModel,
@ -84,10 +87,6 @@ const ConfigPromptItem: FC<Props> = ({
const { const {
setControlPromptEditorRerenderKey, setControlPromptEditorRerenderKey,
} = workflowStore.getState() } = workflowStore.getState()
const [instanceId, setInstanceId] = useState(uniqueId())
useEffect(() => {
setInstanceId(`${id}-${uniqueId()}`)
}, [id])
const handleGenerated = useCallback((prompt: string) => { const handleGenerated = useCallback((prompt: string) => {
onPromptChange(prompt) onPromptChange(prompt)
@ -136,6 +135,7 @@ const ConfigPromptItem: FC<Props> = ({
hasSetBlockStatus={hasSetBlockStatus} hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars} nodesOutputVars={availableVars}
availableNodes={availableNodes} availableNodes={availableNodes}
nodeId={nodeId}
isSupportPromptGenerator={payload.role === PromptRole.system} isSupportPromptGenerator={payload.role === PromptRole.system}
onGenerated={handleGenerated} onGenerated={handleGenerated}
modelConfig={modelConfig} modelConfig={modelConfig}

@ -182,12 +182,14 @@ const ConfigPrompt: FC<Props> = ({
<div key={item.id || index} className='group relative'> <div key={item.id || index} className='group relative'>
{canDrag && <DragHandle className='absolute left-[-14px] top-2 hidden h-3.5 w-3.5 text-text-quaternary group-hover:block' />} {canDrag && <DragHandle className='absolute left-[-14px] top-2 hidden h-3.5 w-3.5 text-text-quaternary group-hover:block' />}
<ConfigPromptItem <ConfigPromptItem
instanceId={item.role === PromptRole.system ? `${nodeId}-chat-workflow-llm-prompt-editor` : `${nodeId}-chat-workflow-llm-prompt-editor-${index}`}
className={cn(canDrag && 'handle')} className={cn(canDrag && 'handle')}
headerClassName={cn(canDrag && 'cursor-grab')} headerClassName={cn(canDrag && 'cursor-grab')}
canNotChooseSystemRole={!canChooseSystemRole} canNotChooseSystemRole={!canChooseSystemRole}
canRemove={payload.length > 1 && !(index === 0 && item.role === PromptRole.system)} canRemove={payload.length > 1 && !(index === 0 && item.role === PromptRole.system)}
readOnly={readOnly} readOnly={readOnly}
id={item.id!} id={item.id!}
nodeId={nodeId}
handleChatModeMessageRoleChange={handleChatModeMessageRoleChange(index)} handleChatModeMessageRoleChange={handleChatModeMessageRoleChange(index)}
isChatModel={isChatModel} isChatModel={isChatModel}
isChatApp={isChatApp} isChatApp={isChatApp}

@ -7,24 +7,30 @@ import { Generator } from '@/app/components/base/icons/src/vender/other'
import { ActionButton } from '@/app/components/base/action-button' import { ActionButton } from '@/app/components/base/action-button'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res' import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import type { AutomaticRes } from '@/service/debug' import type { GenRes } from '@/service/debug'
import type { ModelConfig } from '@/app/components/workflow/types' import type { ModelConfig } from '@/app/components/workflow/types'
import { useHooksStore } from '../../../hooks-store'
type Props = { type Props = {
className?: string className?: string
onGenerated?: (prompt: string) => void onGenerated?: (prompt: string) => void
modelConfig?: ModelConfig modelConfig?: ModelConfig
nodeId: string
currentPrompt?: string
} }
const PromptGeneratorBtn: FC<Props> = ({ const PromptGeneratorBtn: FC<Props> = ({
className, className,
onGenerated, onGenerated,
nodeId,
currentPrompt,
}) => { }) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = useCallback((res: AutomaticRes) => { const handleAutomaticRes = useCallback((res: GenRes) => {
onGenerated?.(res.prompt) onGenerated?.(res.modified)
showAutomaticFalse() showAutomaticFalse()
}, [onGenerated, showAutomaticFalse]) }, [onGenerated, showAutomaticFalse])
const configsMap = useHooksStore(s => s.configsMap)
return ( return (
<div className={cn(className)}> <div className={cn(className)}>
<ActionButton <ActionButton
@ -38,7 +44,9 @@ const PromptGeneratorBtn: FC<Props> = ({
isShow={showAutomatic} isShow={showAutomatic}
onClose={showAutomaticFalse} onClose={showAutomaticFalse}
onFinished={handleAutomaticRes} onFinished={handleAutomaticRes}
isInLLMNode flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentPrompt={currentPrompt}
/> />
)} )}
</div> </div>

@ -294,6 +294,7 @@ export type NodeOutPutVar = {
vars: Var[] vars: Var[]
isStartNode?: boolean isStartNode?: boolean
isLoop?: boolean isLoop?: boolean
isFlat?: boolean
} }
export type Block = { export type Block = {

@ -176,6 +176,7 @@ const Panel: FC = () => {
{/* right */} {/* right */}
<div className='w-0 grow'> <div className='w-0 grow'>
<Right <Right
nodeId={currentFocusNodeId!}
isValueFetching={isCurrentNodeVarValueFetching} isValueFetching={isCurrentNodeVarValueFetching}
currentNodeVar={currentNodeInfo as currentVarType} currentNodeVar={currentNodeInfo as currentVarType}
handleOpenMenu={() => setShowLeftPanel(true)} handleOpenMenu={() => setShowLeftPanel(true)}

@ -3,9 +3,10 @@ import {
RiArrowGoBackLine, RiArrowGoBackLine,
RiCloseLine, RiCloseLine,
RiMenuLine, RiMenuLine,
RiSparklingFill,
} from '@remixicon/react' } from '@remixicon/react'
import { useStore } from '../store' import { useStore } from '../store'
import type { BlockEnum } from '../types' import { BlockEnum } from '../types'
import useCurrentVars from '../hooks/use-inspect-vars-crud' import useCurrentVars from '../hooks/use-inspect-vars-crud'
import Empty from './empty' import Empty from './empty'
import ValueContent from './value-content' import ValueContent from './value-content'
@ -20,14 +21,30 @@ import Loading from '@/app/components/base/loading'
import type { currentVarType } from './panel' import type { currentVarType } from './panel'
import { VarInInspectType } from '@/types/workflow' import { VarInInspectType } from '@/types/workflow'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useNodeInfo from '../nodes/_base/hooks/use-node-info'
import { useBoolean } from 'ahooks'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import GetCodeGeneratorResModal from '../../app/configuration/config/code-generator/get-code-generator-res'
import { AppType } from '@/types/app'
import { useHooksStore } from '../hooks-store'
import { useCallback, useMemo } from 'react'
import { useNodesInteractions } from '../hooks'
import { CodeLanguage } from '../nodes/code/types'
import useNodeCrud from '../nodes/_base/hooks/use-node-crud'
import type { GenRes } from '@/service/debug'
import produce from 'immer'
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../../base/prompt-editor/plugins/update-block'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type Props = { type Props = {
nodeId: string
currentNodeVar?: currentVarType currentNodeVar?: currentVarType
handleOpenMenu: () => void handleOpenMenu: () => void
isValueFetching?: boolean isValueFetching?: boolean
} }
const Right = ({ const Right = ({
nodeId,
currentNodeVar, currentNodeVar,
handleOpenMenu, handleOpenMenu,
isValueFetching, isValueFetching,
@ -74,6 +91,67 @@ const Right = ({
return String(value) return String(value)
} }
const configsMap = useHooksStore(s => s.configsMap)
const { eventEmitter } = useEventEmitterContextContext()
const { handleNodeSelect } = useNodesInteractions()
const { node } = useNodeInfo(nodeId)
const { setInputs } = useNodeCrud(nodeId, node?.data)
const blockType = node?.data?.type
const isCodeBlock = blockType === BlockEnum.Code
const canShowPromptGenerator = [BlockEnum.LLM, BlockEnum.Code].includes(blockType)
const currentPrompt = useMemo(() => {
if (!canShowPromptGenerator)
return ''
if (blockType === BlockEnum.LLM)
return node?.data?.prompt_template?.text || node?.data?.prompt_template?.[0].text
// if (blockType === BlockEnum.Agent) {
// return node?.data?.agent_parameters?.instruction?.value
// }
if (blockType === BlockEnum.Code)
return node?.data?.code
}, [canShowPromptGenerator])
const [isShowPromptGenerator, {
setTrue: doShowPromptGenerator,
setFalse: handleHidePromptGenerator,
}] = useBoolean(false)
const handleShowPromptGenerator = useCallback(() => {
handleNodeSelect(nodeId)
doShowPromptGenerator()
}, [doShowPromptGenerator, handleNodeSelect, nodeId])
const handleUpdatePrompt = useCallback((res: GenRes) => {
const newInputs = produce(node?.data, (draft: any) => {
switch (blockType) {
case BlockEnum.LLM:
if (draft?.prompt_template) {
if (Array.isArray(draft.prompt_template))
draft.prompt_template[0].text = res.modified
else
draft.prompt_template.text = res.modified
}
break
// Agent is a plugin, may has many instructions, can not locate which one to update
// case BlockEnum.Agent:
// if (draft?.agent_parameters?.instruction) {
// draft.agent_parameters.instruction.value = res.modified
// }
// break
case BlockEnum.Code:
draft.code = res.modified
break
}
})
setInputs(newInputs)
eventEmitter?.emit({
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
instanceId: `${nodeId}-chat-workflow-llm-prompt-editor`,
payload: res.modified,
} as any)
handleHidePromptGenerator()
}, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator])
return ( return (
<div className={cn('flex h-full flex-col')}> <div className={cn('flex h-full flex-col')}>
{/* header */} {/* header */}
@ -114,6 +192,16 @@ const Right = ({
<div className='flex shrink-0 items-center gap-1'> <div className='flex shrink-0 items-center gap-1'>
{currentNodeVar && ( {currentNodeVar && (
<> <>
{canShowPromptGenerator && (
<Tooltip popupContent={t('appDebug.generate.optimizePromptTooltip')}>
<div
className='cursor-pointer rounded-md p-1 hover:bg-state-accent-active'
onClick={handleShowPromptGenerator}
>
<RiSparklingFill className='size-4 text-components-input-border-active-prompt-1' />
</div>
</Tooltip>
)}
{currentNodeVar.var.edited && ( {currentNodeVar.var.edited && (
<Badge> <Badge>
<span className='ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary'></span> <span className='ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary'></span>
@ -154,6 +242,28 @@ const Right = ({
)} )}
{currentNodeVar && !isValueFetching && <ValueContent currentVar={currentNodeVar.var} handleValueChange={handleValueChange} />} {currentNodeVar && !isValueFetching && <ValueContent currentVar={currentNodeVar.var} handleValueChange={handleValueChange} />}
</div> </div>
{isShowPromptGenerator && (
isCodeBlock
? <GetCodeGeneratorResModal
isShow
mode={AppType.chat}
onClose={handleHidePromptGenerator}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentCode={currentPrompt}
codeLanguages={node?.data?.code_languages || CodeLanguage.python3}
onFinished={handleUpdatePrompt}
/>
: <GetAutomaticResModal
mode={AppType.chat}
isShow
onClose={handleHidePromptGenerator}
onFinished={handleUpdatePrompt}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentPrompt={currentPrompt}
/>
)}
</div> </div>
) )
} }

@ -248,11 +248,18 @@ const translation = {
description: 'The Prompt Generator uses the configured model to optimize prompts for higher quality and better structure. Please write clear and detailed instructions.', description: 'The Prompt Generator uses the configured model to optimize prompts for higher quality and better structure. Please write clear and detailed instructions.',
tryIt: 'Try it', tryIt: 'Try it',
instruction: 'Instructions', instruction: 'Instructions',
instructionPlaceHolder: 'Write clear and specific instructions.', instructionPlaceHolderTitle: 'Describe how you would like to improve this Prompt. For example:',
instructionPlaceHolderLine1: 'Make the output more concise, retaining the core points.',
instructionPlaceHolderLine2: 'The output format is incorrect, please strictly follow the JSON format.',
instructionPlaceHolderLine3: 'The tone is too harsh, please make it more friendly.',
idealOutput: 'Ideal Output',
idealOutputPlaceholder: 'Describe your ideal response format, length, tone, and content requirements...',
optional: 'Optional',
dismiss: 'Dismiss',
generate: 'Generate', generate: 'Generate',
resTitle: 'Generated Prompt', resTitle: 'Generated Prompt',
noDataLine1: 'Describe your use case on the left,', newNoDataLine1: 'Write a instruction in the left column, and click Generate to see response. ',
noDataLine2: 'the orchestration preview will show here.', newNoDataLine2: 'Learn about prompt design',
apply: 'Apply', apply: 'Apply',
loading: 'Orchestrating the application for you...', loading: 'Orchestrating the application for you...',
overwriteTitle: 'Override existing configuration?', overwriteTitle: 'Override existing configuration?',
@ -295,6 +302,10 @@ const translation = {
instruction: 'Use advanced copyediting techniques to improve your writings', instruction: 'Use advanced copyediting techniques to improve your writings',
}, },
}, },
press: 'Press',
to: 'to ',
insertContext: 'insert context',
optimizePromptTooltip: 'Optimize in Prompt Generator',
}, },
resetConfig: { resetConfig: {
title: 'Confirm reset?', title: 'Confirm reset?',

@ -248,6 +248,7 @@ const translation = {
emailLabel: 'New email', emailLabel: 'New email',
emailPlaceholder: 'Enter a new email', emailPlaceholder: 'Enter a new email',
existingEmail: 'A user with this email already exists.', existingEmail: 'A user with this email already exists.',
unAvailableEmail: 'This email is temporarily unavailable.',
sendVerifyCode: 'Send verification code', sendVerifyCode: 'Send verification code',
continue: 'Continue', continue: 'Continue',
changeTo: 'Change to {{email}}', changeTo: 'Change to {{email}}',

@ -966,6 +966,7 @@ const translation = {
chatNode: 'Conversation', chatNode: 'Conversation',
systemNode: 'System', systemNode: 'System',
}, },
lastOutput: 'Last Output',
}, },
} }

@ -249,6 +249,7 @@ const translation = {
emailLabel: '新しいメール', emailLabel: '新しいメール',
emailPlaceholder: '新しいメールを入力してください', emailPlaceholder: '新しいメールを入力してください',
existingEmail: 'このメールアドレスのユーザーは既に存在します', existingEmail: 'このメールアドレスのユーザーは既に存在します',
unAvailableEmail: 'このメールアドレスは現在使用できません。',
sendVerifyCode: '確認コードを送信', sendVerifyCode: '確認コードを送信',
continue: '続行', continue: '続行',
changeTo: '{{email}} に変更', changeTo: '{{email}} に変更',

@ -236,6 +236,8 @@ const translation = {
apply: '应用', apply: '应用',
applyChanges: '应用更改', applyChanges: '应用更改',
resTitle: '生成的代码', resTitle: '生成的代码',
newNoDataLine1: '在左侧描述您的用例,点击生成查看响应。',
newNoDataLine2: '了解提示词设计',
overwriteConfirmTitle: '是否覆盖现有代码?', overwriteConfirmTitle: '是否覆盖现有代码?',
overwriteConfirmMessage: '此操作将覆盖现有代码。您确定要继续吗?', overwriteConfirmMessage: '此操作将覆盖现有代码。您确定要继续吗?',
}, },
@ -244,11 +246,18 @@ const translation = {
description: '提示词生成器使用配置的模型来优化提示词,以获得更高的质量和更好的结构。请写出清晰详细的说明。', description: '提示词生成器使用配置的模型来优化提示词,以获得更高的质量和更好的结构。请写出清晰详细的说明。',
tryIt: '试一试', tryIt: '试一试',
instruction: '指令', instruction: '指令',
instructionPlaceHolder: '写下清晰、具体的说明。', instructionPlaceHolderTitle: '描述您希望如何改进此提示词。例如:',
instructionPlaceHolderLine1: '使输出更简洁,保留核心要点。',
instructionPlaceHolderLine2: '输出格式不正确,请严格遵循 JSON 格式。',
instructionPlaceHolderLine3: '语气过于生硬,请使其更友好。',
idealOutput: '理想输出',
idealOutputPlaceholder: '描述您理想的回复格式、长度、语气和内容要求……',
optional: '可选',
dismiss: '取消',
generate: '生成', generate: '生成',
resTitle: '生成的提示词', resTitle: '生成的提示词',
noDataLine1: '在左侧描述您的用例,', newNoDataLine1: '在左侧描述您的用例,点击生成查看结果。',
noDataLine2: '编排预览将在此处显示。', newNoDataLine2: '了解提示词设计',
apply: '应用', apply: '应用',
noData: '在左侧描述您的用例,编排预览将在此处显示。', noData: '在左侧描述您的用例,编排预览将在此处显示。',
loading: '为您编排应用程序中…', loading: '为您编排应用程序中…',
@ -292,6 +301,10 @@ const translation = {
instruction: '用地道的编辑技巧改进我的文章', instruction: '用地道的编辑技巧改进我的文章',
}, },
}, },
press: '输入',
to: '来',
insertContext: '插入上下文',
optimizePromptTooltip: '在提示词生成器中优化',
}, },
resetConfig: { resetConfig: {
title: '确认重置?', title: '确认重置?',

@ -248,6 +248,7 @@ const translation = {
emailLabel: '新邮箱', emailLabel: '新邮箱',
emailPlaceholder: '输入新邮箱', emailPlaceholder: '输入新邮箱',
existingEmail: '该邮箱已存在', existingEmail: '该邮箱已存在',
unAvailableEmail: '该邮箱暂时无法使用。',
sendVerifyCode: '发送验证码', sendVerifyCode: '发送验证码',
continue: '继续', continue: '继续',
changeTo: '更改为 {{email}}', changeTo: '更改为 {{email}}',

@ -967,6 +967,7 @@ const translation = {
chatNode: '会话变量', chatNode: '会话变量',
systemNode: '系统变量', systemNode: '系统变量',
}, },
lastOutput: '上次输出',
}, },
} }

@ -3,12 +3,21 @@ import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessag
import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug' import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug'
import type { ModelModeType } from '@/types/app' import type { ModelModeType } from '@/types/app'
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
export type AutomaticRes = { export type BasicAppFirstRes = {
prompt: string prompt: string
variables: string[] variables: string[]
opening_statement: string opening_statement: string
error?: string error?: string
} }
export type GenRes = {
modified: string
message?: string // tip for human
variables?: string[] // only for basic app first time rule
opening_statement?: string // only for basic app first time rule
error?: string
}
export type CodeGenRes = { export type CodeGenRes = {
code: string code: string
language: string[] language: string[]
@ -71,13 +80,14 @@ export const fetchConversationMessages = (appId: string, conversation_id: string
}) })
} }
export const generateRule = (body: Record<string, any>) => { export const generateBasicAppFistTimeRule = (body: Record<string, any>) => {
return post<AutomaticRes>('/rule-generate', { return post<BasicAppFirstRes>('/rule-generate', {
body, body,
}) })
} }
export const generateRuleCode = (body: Record<string, any>) => {
return post<CodeGenRes>('/rule-code-generate', { export const generateRule = (body: Record<string, any>) => {
return post<GenRes>('/instruction-generate', {
body, body,
}) })
} }

@ -1,8 +1,9 @@
import { get } from './base' import { get, post } from './base'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import type { AppListResponse } from '@/models/app' import type { AppListResponse } from '@/models/app'
import { useInvalid } from './use-base' import { useInvalid } from './use-base'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
const NAME_SPACE = 'apps' const NAME_SPACE = 'apps'
@ -25,3 +26,16 @@ export const useAppDetail = (appID: string) => {
queryFn: () => get<App>(`/apps/${appID}`), queryFn: () => get<App>(`/apps/${appID}`),
}) })
} }
export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'generate-rule-template', type],
queryFn: () => post<{ data: string }>('instruction-generate/template', {
body: {
type,
},
}),
enabled: !disabled,
retry: 0,
})
}

Loading…
Cancel
Save