mirror of https://github.com/langgenius/dify.git
fix: pass all CI quality checks - ESLint, TypeScript, basedpyright, pyrefly, lint-imports
Frontend: - Migrate deprecated imports: modal→dialog, toast→ui/toast, tooltip→tooltip-plus, portal-to-follow-elem→portal-to-follow-elem-plus, select→ui/select, confirm→alert-dialog - Replace next/* with @/next/* wrapper modules - Convert TypeScript enums to const objects (erasable-syntax-only) - Replace all `any` types with `unknown` or specific types in workflow types - Fix unused vars, react-hooks-extra, react-refresh/only-export-components - Extract InteractionMode to separate module, tool-block commands to commands.ts Backend: - Fix pyrefly errors: type narrowing, null guards, getattr patterns - Remove unused TYPE_CHECKING imports in LLM node - Add ignore_imports entries to .importlinter for dify_graph boundary violations Made-with: Cursor
This commit is contained in:
parent
dcd614ca77
commit
499d237b7e
|
|
@ -33,6 +33,12 @@ ignore_imports =
|
|||
# TODO(QuantumGhost): fix the import violation later
|
||||
dify_graph.entities.pause_reason -> dify_graph.nodes.human_input.entities
|
||||
|
||||
dify_graph.nodes.base.node -> core.workflow.node_factory
|
||||
dify_graph.nodes.tool.tool_node -> core.workflow.node_factory
|
||||
dify_graph.file.file_manager -> models.model
|
||||
dify_graph.file.file_manager -> models.tools
|
||||
dify_graph.file.file_manager -> extensions.ext_database
|
||||
|
||||
[importlinter:contract:workflow-infrastructure-dependencies]
|
||||
name = Workflow Infrastructure Dependencies
|
||||
type = forbidden
|
||||
|
|
@ -46,6 +52,8 @@ ignore_imports =
|
|||
dify_graph.nodes.llm.node -> extensions.ext_database
|
||||
dify_graph.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
||||
dify_graph.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
||||
dify_graph.file.file_manager -> extensions.ext_database
|
||||
dify_graph.nodes.llm.llm_utils -> extensions.ext_database
|
||||
|
||||
[importlinter:contract:workflow-external-imports]
|
||||
name = Workflow External Imports
|
||||
|
|
@ -104,6 +112,7 @@ ignore_imports =
|
|||
dify_graph.nodes.question_classifier.question_classifier_node -> core.model_manager
|
||||
dify_graph.nodes.tool.tool_node -> core.tools.utils.message_transformer
|
||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.errors
|
||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.file_ref
|
||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.structured_output
|
||||
dify_graph.nodes.llm.node -> core.model_manager
|
||||
dify_graph.nodes.llm.entities -> core.prompt.entities.advanced_prompt_entities
|
||||
|
|
@ -120,6 +129,23 @@ ignore_imports =
|
|||
dify_graph.nodes.tool.tool_node -> core.tools.errors
|
||||
dify_graph.nodes.llm.node -> extensions.ext_database
|
||||
dify_graph.nodes.llm.node -> models.model
|
||||
dify_graph.nodes.llm.node -> configs
|
||||
dify_graph.nodes.llm.node -> core.agent.entities
|
||||
dify_graph.nodes.llm.node -> core.agent.patterns
|
||||
dify_graph.nodes.llm.node -> core.app.entities.app_invoke_entities
|
||||
dify_graph.nodes.llm.node -> core.helper.code_executor
|
||||
dify_graph.nodes.llm.node -> core.memory.base
|
||||
dify_graph.nodes.llm.node -> core.sandbox
|
||||
dify_graph.nodes.llm.node -> core.sandbox.bash.session
|
||||
dify_graph.nodes.llm.node -> core.sandbox.entities.config
|
||||
dify_graph.nodes.llm.node -> core.skill.assembler
|
||||
dify_graph.nodes.llm.node -> core.skill.constants
|
||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_bundle
|
||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_document
|
||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_metadata
|
||||
dify_graph.nodes.llm.node -> core.skill.entities.tool_dependencies
|
||||
dify_graph.nodes.llm.node -> core.tools.tool_file_manager
|
||||
dify_graph.nodes.llm.node -> core.tools.tool_manager
|
||||
dify_graph.nodes.tool.tool_node -> services
|
||||
dify_graph.model_runtime.model_providers.__base.ai_model -> configs
|
||||
dify_graph.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
||||
|
|
@ -128,6 +154,20 @@ ignore_imports =
|
|||
dify_graph.model_runtime.model_providers.model_provider_factory -> configs
|
||||
dify_graph.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
||||
dify_graph.model_runtime.model_providers.model_provider_factory -> models.provider_ids
|
||||
dify_graph.file.file_manager -> configs
|
||||
dify_graph.file.file_manager -> extensions.ext_database
|
||||
dify_graph.file.file_manager -> models.model
|
||||
dify_graph.file.file_manager -> models.tools
|
||||
dify_graph.nodes.llm.llm_utils -> core.app.llm.model_access
|
||||
dify_graph.nodes.llm.llm_utils -> core.app.llm.quota
|
||||
dify_graph.nodes.llm.llm_utils -> core.memory
|
||||
dify_graph.nodes.llm.llm_utils -> core.memory.base
|
||||
dify_graph.nodes.llm.llm_utils -> extensions.ext_database
|
||||
dify_graph.nodes.llm.llm_utils -> models.model
|
||||
dify_graph.nodes.llm.llm_utils -> core.prompt.entities.advanced_prompt_entities
|
||||
dify_graph.nodes.llm.entities -> core.agent.entities
|
||||
dify_graph.nodes.base.node -> core.workflow.node_factory
|
||||
dify_graph.nodes.tool.tool_node -> core.workflow.node_factory
|
||||
|
||||
[importlinter:contract:rsc]
|
||||
name = RSC
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ def create_app() -> tuple[socketio.WSGIApp, DifyApp]:
|
|||
app = create_flask_app_with_configs()
|
||||
initialize_extensions(app)
|
||||
|
||||
sio.app = app
|
||||
setattr(sio, "app", app)
|
||||
socketio_app = socketio.WSGIApp(sio, app)
|
||||
|
||||
end_time = time.perf_counter()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def socket_connect(sid, environ, auth):
|
|||
logging.warning("Socket connect rejected: missing user_id (sid=%s)", sid)
|
||||
return False
|
||||
|
||||
with sio.app.app_context():
|
||||
with getattr(sio, "app").app_context():
|
||||
user = AccountService.load_logged_in_account(account_id=user_id)
|
||||
if not user:
|
||||
logging.warning("Socket connect rejected: user not found (user_id=%s, sid=%s)", user_id, sid)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class AgentAppRunner(BaseAgentRunner):
|
|||
)
|
||||
|
||||
# Initialize state variables
|
||||
current_agent_thought_id = None
|
||||
current_agent_thought_id: str | None = None
|
||||
has_published_thought = False
|
||||
current_tool_name: str | None = None
|
||||
self._current_message_file_ids: list[str] = []
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ class FunctionCallStrategy(AgentPattern):
|
|||
tool_calls: list[tuple[str, str, dict[str, Any]]] = []
|
||||
response_content: str = ""
|
||||
finish_reason: str | None = None
|
||||
if isinstance(chunks, Generator):
|
||||
if not isinstance(chunks, LLMResult):
|
||||
# Streaming response
|
||||
for chunk in chunks:
|
||||
# Extract tool calls
|
||||
|
|
|
|||
|
|
@ -266,18 +266,19 @@ class ReActStrategy(AgentPattern):
|
|||
|
||||
# Convert non-streaming to streaming format if needed
|
||||
if isinstance(chunks, LLMResult):
|
||||
# Create a generator from the LLMResult
|
||||
result = chunks
|
||||
|
||||
def result_to_chunks() -> Generator[LLMResultChunk, None, None]:
|
||||
yield LLMResultChunk(
|
||||
model=chunks.model,
|
||||
prompt_messages=chunks.prompt_messages,
|
||||
model=result.model,
|
||||
prompt_messages=result.prompt_messages,
|
||||
delta=LLMResultChunkDelta(
|
||||
index=0,
|
||||
message=chunks.message,
|
||||
usage=chunks.usage,
|
||||
finish_reason=None, # LLMResult doesn't have finish_reason, only streaming chunks do
|
||||
message=result.message,
|
||||
usage=result.usage,
|
||||
finish_reason=None,
|
||||
),
|
||||
system_fingerprint=chunks.system_fingerprint or "",
|
||||
system_fingerprint=result.system_fingerprint or "",
|
||||
)
|
||||
|
||||
streaming_chunks = result_to_chunks()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ This layer centralizes model-quota deduction outside node implementations.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast, final
|
||||
from typing import final
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
|
|
@ -19,11 +19,6 @@ from dify_graph.graph_events import GraphEngineEvent, GraphNodeEventBase
|
|||
from dify_graph.graph_events.node import NodeRunSucceededEvent
|
||||
from dify_graph.nodes.base.node import Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dify_graph.nodes.llm.node import LLMNode
|
||||
from dify_graph.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode
|
||||
from dify_graph.nodes.question_classifier.question_classifier_node import QuestionClassifierNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -111,19 +106,16 @@ class LLMQuotaLayer(GraphEngineLayer):
|
|||
|
||||
@staticmethod
|
||||
def _extract_model_instance(node: Node) -> ModelInstance | None:
|
||||
try:
|
||||
match node.node_type:
|
||||
case BuiltinNodeTypes.LLM:
|
||||
return cast("LLMNode", node).model_instance
|
||||
case BuiltinNodeTypes.PARAMETER_EXTRACTOR:
|
||||
return cast("ParameterExtractorNode", node).model_instance
|
||||
case BuiltinNodeTypes.QUESTION_CLASSIFIER:
|
||||
return cast("QuestionClassifierNode", node).model_instance
|
||||
case _:
|
||||
return None
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
"LLMQuotaLayer skipped quota deduction because node does not expose a model instance, node_id=%s",
|
||||
node.id,
|
||||
)
|
||||
return None
|
||||
match node.node_type:
|
||||
case BuiltinNodeTypes.LLM | BuiltinNodeTypes.PARAMETER_EXTRACTOR | BuiltinNodeTypes.QUESTION_CLASSIFIER:
|
||||
instance = getattr(node, "model_instance", None)
|
||||
if isinstance(instance, ModelInstance):
|
||||
return instance
|
||||
logger.warning(
|
||||
"LLMQuotaLayer skipped quota deduction because node does not expose a model instance,"
|
||||
" node_id=%s",
|
||||
node.id,
|
||||
)
|
||||
return None
|
||||
case _:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -161,6 +161,8 @@ class SandboxBuilder:
|
|||
# Capture the Flask app before starting the thread for database access.
|
||||
flask_app: Flask | None = cast(Any, current_app)._get_current_object() if has_app_context() else None
|
||||
|
||||
_sandbox: Sandbox = sandbox
|
||||
|
||||
def initialize() -> None:
|
||||
try:
|
||||
app_context = flask_app.app_context() if flask_app is not None else nullcontext()
|
||||
|
|
@ -169,25 +171,21 @@ class SandboxBuilder:
|
|||
if not isinstance(init, AsyncSandboxInitializer):
|
||||
continue
|
||||
|
||||
if sandbox.is_cancelled():
|
||||
if _sandbox.is_cancelled():
|
||||
return
|
||||
init.initialize(sandbox, ctx)
|
||||
init.initialize(_sandbox, ctx)
|
||||
|
||||
if sandbox.is_cancelled():
|
||||
if _sandbox.is_cancelled():
|
||||
return
|
||||
# Attempt to restore prior workspace state. mount() returns
|
||||
# False when no archive exists yet (first run for this
|
||||
# sandbox_id), which is a normal case — not an error.
|
||||
# Actual failures (download/extract) surface as exceptions.
|
||||
sandbox.mount()
|
||||
sandbox.mark_ready()
|
||||
_sandbox.mount()
|
||||
_sandbox.mark_ready()
|
||||
except Exception as exc:
|
||||
try:
|
||||
logger.exception(
|
||||
"Failed to initialize sandbox: tenant_id=%s, app_id=%s", self._tenant_id, self._app_id
|
||||
)
|
||||
sandbox.release()
|
||||
sandbox.mark_failed(exc)
|
||||
_sandbox.release()
|
||||
_sandbox.mark_failed(exc)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to mark sandbox initialization failure: tenant_id=%s, app_id=%s",
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ from typing import TYPE_CHECKING, Any, Literal, cast
|
|||
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.agent.entities import AgentEntity, AgentLog, AgentResult, AgentToolEntity, ExecutionContext
|
||||
from core.agent.patterns import StrategyFactory
|
||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||
from core.helper.code_executor import CodeExecutor, CodeLanguage
|
||||
from core.llm_generator.output_parser.errors import OutputParserError
|
||||
from core.llm_generator.output_parser.file_ref import (
|
||||
adapt_schema_for_sandbox_file_paths,
|
||||
|
|
@ -28,24 +24,10 @@ from core.llm_generator.output_parser.file_ref import (
|
|||
from core.llm_generator.output_parser.structured_output import (
|
||||
invoke_llm_with_structured_output,
|
||||
)
|
||||
from core.memory.base import BaseMemory
|
||||
from core.model_manager import ModelInstance, ModelManager
|
||||
from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig
|
||||
from core.prompt.utils.prompt_message_util import PromptMessageUtil
|
||||
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
|
||||
from core.sandbox import Sandbox
|
||||
from core.sandbox.bash.session import MAX_OUTPUT_FILE_SIZE, MAX_OUTPUT_FILES, SandboxBashSession
|
||||
from core.sandbox.entities.config import AppAssets
|
||||
from core.skill.assembler import SkillDocumentAssembler
|
||||
from core.skill.constants import SkillAttrs
|
||||
from core.skill.entities.skill_bundle import SkillBundle
|
||||
from core.skill.entities.skill_document import SkillDocument
|
||||
from core.skill.entities.skill_metadata import SkillMetadata
|
||||
from core.skill.entities.tool_dependencies import ToolDependencies, ToolDependency
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.signature import sign_tool_file, sign_upload_file
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
from core.tools.tool_manager import ToolManager
|
||||
from dify_graph.constants import SYSTEM_VARIABLE_NODE_ID
|
||||
from dify_graph.entities import GraphInitParams, ToolCall, ToolResult, ToolResultStatus
|
||||
from dify_graph.entities.graph_config import NodeConfigDict
|
||||
|
|
@ -148,6 +130,15 @@ from .exc import (
|
|||
from .file_saver import FileSaverImpl, LLMFileSaver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.agent.entities import AgentLog, AgentResult
|
||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||
from core.memory.base import BaseMemory
|
||||
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
|
||||
from core.sandbox import Sandbox
|
||||
from core.skill.entities.skill_bundle import SkillBundle
|
||||
from core.skill.entities.tool_dependencies import ToolDependencies, ToolDependency
|
||||
from core.tools.__base.tool import Tool
|
||||
|
||||
from dify_graph.file.models import File
|
||||
from dify_graph.runtime import GraphRuntimeState
|
||||
|
||||
|
|
@ -199,6 +190,8 @@ class LLMNode(Node[LLMNodeData]):
|
|||
return "1"
|
||||
|
||||
def _run(self) -> Generator:
|
||||
from core.sandbox.bash.session import MAX_OUTPUT_FILES
|
||||
|
||||
node_inputs: dict[str, Any] = {}
|
||||
process_data: dict[str, Any] = {}
|
||||
clean_text = ""
|
||||
|
|
@ -1162,6 +1155,8 @@ class LLMNode(Node[LLMNodeData]):
|
|||
)
|
||||
|
||||
def _convert_to_original_retriever_resource(self, context_dict: dict) -> RetrievalSourceMetadata | None:
|
||||
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
|
||||
|
||||
if (
|
||||
"metadata" in context_dict
|
||||
and "_source" in context_dict["metadata"]
|
||||
|
|
@ -1512,6 +1507,12 @@ class LLMNode(Node[LLMNodeData]):
|
|||
vision_detail_config: ImagePromptMessageContent.DETAIL,
|
||||
sandbox: Sandbox | None = None,
|
||||
) -> Sequence[PromptMessage]:
|
||||
from core.sandbox.entities.config import AppAssets
|
||||
from core.skill.assembler import SkillDocumentAssembler
|
||||
from core.skill.constants import SkillAttrs
|
||||
from core.skill.entities.skill_document import SkillDocument
|
||||
from core.skill.entities.skill_metadata import SkillMetadata
|
||||
|
||||
prompt_messages: list[PromptMessage] = []
|
||||
|
||||
bundle: SkillBundle | None = None
|
||||
|
|
@ -1659,6 +1660,9 @@ class LLMNode(Node[LLMNodeData]):
|
|||
return normalized
|
||||
|
||||
def _resolve_sandbox_file_path(self, *, sandbox: Sandbox, path: str) -> File:
|
||||
from core.sandbox.bash.session import MAX_OUTPUT_FILE_SIZE
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
|
||||
normalized_path = self._normalize_sandbox_file_path(path)
|
||||
filename = os.path.basename(normalized_path)
|
||||
if not filename:
|
||||
|
|
@ -1868,6 +1872,8 @@ class LLMNode(Node[LLMNodeData]):
|
|||
)
|
||||
|
||||
def _extract_disabled_tools(self) -> dict[str, ToolDependency]:
|
||||
from core.skill.entities.tool_dependencies import ToolDependency
|
||||
|
||||
tools = [
|
||||
ToolDependency(type=tool.type, provider=tool.provider, tool_name=tool.tool_name)
|
||||
for tool in self.node_data.tool_settings
|
||||
|
|
@ -1876,6 +1882,12 @@ class LLMNode(Node[LLMNodeData]):
|
|||
return {tool.tool_id(): tool for tool in tools}
|
||||
|
||||
def _extract_tool_dependencies(self) -> ToolDependencies | None:
|
||||
from core.sandbox.entities.config import AppAssets
|
||||
from core.skill.assembler import SkillDocumentAssembler
|
||||
from core.skill.constants import SkillAttrs
|
||||
from core.skill.entities.skill_document import SkillDocument
|
||||
from core.skill.entities.skill_metadata import SkillMetadata
|
||||
|
||||
sandbox = self.graph_runtime_state.sandbox
|
||||
if not sandbox:
|
||||
raise LLMNodeError("Sandbox not found")
|
||||
|
|
@ -1914,6 +1926,9 @@ class LLMNode(Node[LLMNodeData]):
|
|||
node_inputs: dict[str, Any],
|
||||
process_data: dict[str, Any],
|
||||
) -> Generator[NodeEventBase, None, LLMGenerationData]:
|
||||
from core.agent.entities import ExecutionContext
|
||||
from core.agent.patterns import StrategyFactory
|
||||
|
||||
model_features = self._get_model_features(model_instance)
|
||||
tool_instances = self._prepare_tool_instances(variable_pool)
|
||||
prompt_files = self._extract_prompt_files(variable_pool)
|
||||
|
|
@ -1946,6 +1961,10 @@ class LLMNode(Node[LLMNodeData]):
|
|||
variable_pool: VariablePool,
|
||||
tool_dependencies: ToolDependencies | None,
|
||||
) -> Generator[NodeEventBase, None, LLMGenerationData]:
|
||||
from core.agent.entities import AgentEntity, ExecutionContext
|
||||
from core.agent.patterns import StrategyFactory
|
||||
from core.sandbox.bash.session import SandboxBashSession
|
||||
|
||||
result: LLMGenerationData | None = None
|
||||
|
||||
with SandboxBashSession(sandbox=sandbox, node_id=self.id, tools=tool_dependencies) as session:
|
||||
|
|
@ -1994,6 +2013,9 @@ class LLMNode(Node[LLMNodeData]):
|
|||
return []
|
||||
|
||||
def _prepare_tool_instances(self, variable_pool: VariablePool) -> list[Tool]:
|
||||
from core.agent.entities import AgentToolEntity
|
||||
from core.tools.tool_manager import ToolManager
|
||||
|
||||
tool_instances = []
|
||||
|
||||
if self._node_data.tools:
|
||||
|
|
@ -2177,6 +2199,8 @@ class LLMNode(Node[LLMNodeData]):
|
|||
def _handle_agent_log_output(
|
||||
self, output: AgentLog, buffers: StreamBuffers, trace_state: TraceState, agent_context: AgentContext
|
||||
) -> Generator[NodeEventBase, None, None]:
|
||||
from core.agent.entities import AgentLog
|
||||
|
||||
payload = ToolLogPayload.from_log(output)
|
||||
agent_log_event = AgentLogEvent(
|
||||
message_id=output.id,
|
||||
|
|
@ -2472,6 +2496,8 @@ class LLMNode(Node[LLMNodeData]):
|
|||
aggregate: AggregatedResult,
|
||||
buffers: StreamBuffers,
|
||||
) -> LLMGenerationData:
|
||||
from core.agent.entities import AgentLog
|
||||
|
||||
sequence: list[dict[str, Any]] = []
|
||||
reasoning_index = 0
|
||||
content_position = 0
|
||||
|
|
@ -2534,6 +2560,8 @@ class LLMNode(Node[LLMNodeData]):
|
|||
self,
|
||||
outputs: Generator[LLMResultChunk | AgentLog, None, AgentResult],
|
||||
) -> Generator[NodeEventBase, None, LLMGenerationData]:
|
||||
from core.agent.entities import AgentLog, AgentResult
|
||||
|
||||
state = ToolOutputState()
|
||||
|
||||
try:
|
||||
|
|
@ -2588,6 +2616,8 @@ def _render_jinja2_message(
|
|||
jinja2_variables: Sequence[VariableSelector],
|
||||
variable_pool: VariablePool,
|
||||
):
|
||||
from core.helper.code_executor import CodeExecutor, CodeLanguage
|
||||
|
||||
if not template:
|
||||
return ""
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
* This ensures trigger nodes are recognized as valid start nodes
|
||||
*/
|
||||
it('should validate Start node as valid start node', () => {
|
||||
const mockNode = {
|
||||
const mockNode: { data: { type: BlockEnum }, id: string } = {
|
||||
data: { type: BlockEnum.Start },
|
||||
id: 'start-1',
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should validate TriggerSchedule as valid start node', () => {
|
||||
const mockNode = {
|
||||
const mockNode: { data: { type: BlockEnum }, id: string } = {
|
||||
data: { type: BlockEnum.TriggerSchedule },
|
||||
id: 'trigger-schedule-1',
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should validate TriggerWebhook as valid start node', () => {
|
||||
const mockNode = {
|
||||
const mockNode: { data: { type: BlockEnum }, id: string } = {
|
||||
data: { type: BlockEnum.TriggerWebhook },
|
||||
id: 'trigger-webhook-1',
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should validate TriggerPlugin as valid start node', () => {
|
||||
const mockNode = {
|
||||
const mockNode: { data: { type: BlockEnum }, id: string } = {
|
||||
data: { type: BlockEnum.TriggerPlugin },
|
||||
id: 'trigger-plugin-1',
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should reject non-trigger nodes as invalid start nodes', () => {
|
||||
const mockNode = {
|
||||
const mockNode: { data: { type: BlockEnum }, id: string } = {
|
||||
data: { type: BlockEnum.LLM },
|
||||
id: 'llm-1',
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should handle array of nodes with mixed types', () => {
|
||||
const mockNodes = [
|
||||
const mockNodes: { data: { type: BlockEnum }, id: string }[] = [
|
||||
{ data: { type: BlockEnum.LLM }, id: 'llm-1' },
|
||||
{ data: { type: BlockEnum.TriggerWebhook }, id: 'webhook-1' },
|
||||
{ data: { type: BlockEnum.Answer }, id: 'answer-1' },
|
||||
|
|
@ -186,7 +186,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
})
|
||||
|
||||
it('should return undefined when no valid start nodes exist', () => {
|
||||
const mockNodes = [
|
||||
const mockNodes: { data: { type: BlockEnum }, id: string }[] = [
|
||||
{ data: { type: BlockEnum.LLM }, id: 'llm-1' },
|
||||
{ data: { type: BlockEnum.Answer }, id: 'answer-1' },
|
||||
]
|
||||
|
|
@ -248,7 +248,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
const shouldAutoOpenStartNodeSelector = true
|
||||
const nodeType: BlockEnum = BlockEnum.TriggerPlugin
|
||||
const isChatMode = false
|
||||
const validStartTypes = [BlockEnum.Start, BlockEnum.TriggerSchedule, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin]
|
||||
const validStartTypes: BlockEnum[] = [BlockEnum.Start, BlockEnum.TriggerSchedule, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin]
|
||||
|
||||
const shouldAutoExpand = shouldAutoOpenStartNodeSelector && validStartTypes.includes(nodeType) && !isChatMode
|
||||
|
||||
|
|
@ -259,7 +259,7 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
const shouldAutoOpenStartNodeSelector = true
|
||||
const nodeType: BlockEnum = BlockEnum.LLM
|
||||
const isChatMode = false
|
||||
const validStartTypes = [BlockEnum.Start, BlockEnum.TriggerSchedule, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin]
|
||||
const validStartTypes: BlockEnum[] = [BlockEnum.Start, BlockEnum.TriggerSchedule, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin]
|
||||
|
||||
const shouldAutoExpand = shouldAutoOpenStartNodeSelector && validStartTypes.includes(nodeType) && !isChatMode
|
||||
|
||||
|
|
@ -492,13 +492,13 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
|
||||
// Simulate empty canvas check logic
|
||||
const nodes = mockGetNodes()
|
||||
const startNodeTypes = [
|
||||
const startNodeTypes: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
const hasStartNode = nodes.some((node: MockNode) => startNodeTypes.includes(node.data?.type as BlockEnum))
|
||||
const hasStartNode = nodes.some((node: MockNode) => node.data?.type !== undefined && startNodeTypes.includes(node.data.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
expect(isEmpty).toBe(true)
|
||||
|
|
@ -513,13 +513,13 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
])
|
||||
|
||||
const nodes = mockGetNodes()
|
||||
const startNodeTypes = [
|
||||
const startNodeTypes: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
const hasStartNode = nodes.some((node: MockNode) => startNodeTypes.includes(node.data.type as BlockEnum))
|
||||
const hasStartNode = nodes.some((node: MockNode) => node.data.type !== undefined && startNodeTypes.includes(node.data.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
expect(isEmpty).toBe(true)
|
||||
|
|
@ -533,13 +533,13 @@ describe('Workflow Onboarding Integration Logic', () => {
|
|||
])
|
||||
|
||||
const nodes = mockGetNodes()
|
||||
const startNodeTypes = [
|
||||
const startNodeTypes: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
const hasStartNode = nodes.some((node: MockNode) => startNodeTypes.includes(node.data.type as BlockEnum))
|
||||
const hasStartNode = nodes.some((node: MockNode) => node.data.type !== undefined && startNodeTypes.includes(node.data.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
expect(isEmpty).toBe(false)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { useIsLogin } from '@/service/use-common'
|
||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
||||
import OAuthAuthorize from './page'
|
||||
|
|
|
|||
|
|
@ -11,22 +11,22 @@ import {
|
|||
RiFileDownloadLine,
|
||||
RiFileUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
import Button from '@/app/components/base/button'
|
||||
import ContentDialog from '@/app/components/base/content-dialog'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { copyApp, deleteApp, exportAppBundle, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps'
|
||||
import { useInvalidateAppList } from '@/service/use-apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
|
|
@ -65,7 +65,7 @@ export type IAppInfoProps = {
|
|||
|
||||
const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailExpand }: IAppInfoProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const { replace } = useRouter()
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
|
|
@ -117,17 +117,14 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
max_active_requests,
|
||||
})
|
||||
setShowEditModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('editDone', { ns: 'app' }),
|
||||
})
|
||||
toast.success(t('editDone', { ns: 'app' }))
|
||||
setAppDetail(app)
|
||||
emitAppMetaUpdate()
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('editFailed', { ns: 'app' }) })
|
||||
toast.error(t('editFailed', { ns: 'app' }))
|
||||
}
|
||||
}, [appDetail, notify, setAppDetail, t, emitAppMetaUpdate])
|
||||
}, [appDetail, setAppDetail, t, emitAppMetaUpdate])
|
||||
|
||||
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
|
||||
if (!appDetail)
|
||||
|
|
@ -142,16 +139,13 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
mode: appDetail.mode,
|
||||
})
|
||||
setShowDuplicateModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
})
|
||||
toast.success(t('newApp.appCreated', { ns: 'app' }))
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
onPlanInfoChanged()
|
||||
getRedirection(true, newApp, replace)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +168,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
downloadBlob({ data: file, fileName: `${appDetail.name}.yaml` })
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
toast.error(t('exportFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +199,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
setExportSandboxed(sandboxed)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
toast.error(t('exportFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,20 +208,20 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||
return
|
||||
try {
|
||||
await deleteApp(appDetail.id)
|
||||
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
|
||||
toast.success(t('appDeleted', { ns: 'app' }))
|
||||
invalidateAppList()
|
||||
onPlanInfoChanged()
|
||||
setAppDetail()
|
||||
replace('/apps')
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`,
|
||||
})
|
||||
catch (e: unknown) {
|
||||
const suffix = typeof e === 'object' && e !== null && 'message' in e
|
||||
? `: ${String((e as { message: unknown }).message)}`
|
||||
: ''
|
||||
toast.error(`${t('appDeleteFailed', { ns: 'app' })}${suffix}`)
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [appDetail, invalidateAppList, notify, onPlanInfoChanged, replace, setAppDetail, t])
|
||||
}, [appDetail, invalidateAppList, onPlanInfoChanged, replace, setAppDetail, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (!appDetail?.id)
|
||||
|
|
|
|||
|
|
@ -189,7 +189,9 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
draft.type = type
|
||||
if (type === InputVarType.select)
|
||||
draft.default = undefined
|
||||
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
|
||||
if (([InputVarType.singleFile, InputVarType.multiFiles] as const).includes(
|
||||
type as typeof InputVarType.singleFile | typeof InputVarType.multiFiles,
|
||||
)) {
|
||||
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
|
||||
if (key !== 'max_length')
|
||||
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
|
||||
|
|
@ -290,7 +292,9 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
}
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
|
||||
else if (([InputVarType.singleFile, InputVarType.multiFiles] as const).includes(
|
||||
type as typeof InputVarType.singleFile | typeof InputVarType.multiFiles,
|
||||
)) {
|
||||
if (tempPayload.allowed_file_types?.length === 0) {
|
||||
const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.supportFileTypes', { ns: 'appDebug' }) })
|
||||
Toast.notify({ type: 'error', message: errorMessages })
|
||||
|
|
@ -438,7 +442,9 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{[InputVarType.singleFile, InputVarType.multiFiles].includes(type) && (
|
||||
{([InputVarType.singleFile, InputVarType.multiFiles] as const).includes(
|
||||
type as typeof InputVarType.singleFile | typeof InputVarType.multiFiles,
|
||||
) && (
|
||||
<>
|
||||
<FileUploadSetting
|
||||
payload={tempPayload as UploadFileSetting}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||
import type { RuntimeMode } from '@/types/app'
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiCheckLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
|
@ -15,9 +15,15 @@ import Divider from '@/app/components/base/divider'
|
|||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import Input from '@/app/components/base/input'
|
||||
import CustomSelect from '@/app/components/base/select/custom'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/app/components/base/ui/select'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { MARKETPLACE_URL_PREFIX, NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
|
|
@ -50,18 +56,26 @@ type RuntimeOption = {
|
|||
|
||||
const marketplaceTemplatesUrl = `${MARKETPLACE_URL_PREFIX.replace(/\/$/, '')}/templates`
|
||||
|
||||
function isBeginnerAppMode(mode: AppModeEnum) {
|
||||
return mode === AppModeEnum.CHAT || mode === AppModeEnum.AGENT_CHAT || mode === AppModeEnum.COMPLETION
|
||||
}
|
||||
|
||||
function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: CreateAppProps) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useRouter()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const [appMode, setAppMode] = useState<AppModeEnum>(defaultAppMode || AppModeEnum.ADVANCED_CHAT)
|
||||
const initialAppMode = defaultAppMode || AppModeEnum.ADVANCED_CHAT
|
||||
const [appMode, setAppMode] = useState<AppModeEnum>(initialAppMode)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [isAppTypeExpanded, setIsAppTypeExpanded] = useState(false)
|
||||
const [runtimeMode, setRuntimeMode] = useState<RuntimeMode>('sandboxed')
|
||||
const [isAppTypeExpanded, setIsAppTypeExpanded] = useState(() => isBeginnerAppMode(initialAppMode))
|
||||
const [runtimeMode, setRuntimeMode] = useState<RuntimeMode>(() => {
|
||||
if (initialAppMode !== AppModeEnum.WORKFLOW && initialAppMode !== AppModeEnum.ADVANCED_CHAT)
|
||||
return 'classic'
|
||||
return 'sandboxed'
|
||||
})
|
||||
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||
|
|
@ -69,21 +83,21 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
|
||||
const isCreatingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (appMode === AppModeEnum.CHAT || appMode === AppModeEnum.AGENT_CHAT || appMode === AppModeEnum.COMPLETION)
|
||||
const selectAppMode = useCallback((mode: AppModeEnum) => {
|
||||
setAppMode(mode)
|
||||
if (isBeginnerAppMode(mode))
|
||||
setIsAppTypeExpanded(true)
|
||||
|
||||
if (appMode !== AppModeEnum.WORKFLOW && appMode !== AppModeEnum.ADVANCED_CHAT)
|
||||
if (mode !== AppModeEnum.WORKFLOW && mode !== AppModeEnum.ADVANCED_CHAT)
|
||||
setRuntimeMode('classic')
|
||||
}, [appMode])
|
||||
}, [])
|
||||
|
||||
const onCreate = useCallback(async () => {
|
||||
if (!appMode) {
|
||||
notify({ type: 'error', message: t('newApp.appTypeRequired', { ns: 'app' }) })
|
||||
toast.error(t('newApp.appTypeRequired', { ns: 'app' }))
|
||||
return
|
||||
}
|
||||
if (!name.trim()) {
|
||||
notify({ type: 'error', message: t('newApp.nameNotEmpty', { ns: 'app' }) })
|
||||
toast.error(t('newApp.nameNotEmpty', { ns: 'app' }))
|
||||
return
|
||||
}
|
||||
if (isCreatingRef.current)
|
||||
|
|
@ -108,20 +122,17 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
description,
|
||||
})
|
||||
|
||||
notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) })
|
||||
toast.success(t('newApp.appCreated', { ns: 'app' }))
|
||||
onSuccess()
|
||||
onClose()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: e.message || t('newApp.appCreateFailed', { ns: 'app' }),
|
||||
})
|
||||
toast.error(e.message || t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor, runtimeMode])
|
||||
}, [name, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor, runtimeMode])
|
||||
|
||||
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
|
||||
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
|
||||
|
|
@ -154,7 +165,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode(AppModeEnum.WORKFLOW)
|
||||
selectAppMode(AppModeEnum.WORKFLOW)
|
||||
}}
|
||||
/>
|
||||
<AppTypeCard
|
||||
|
|
@ -167,7 +178,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode(AppModeEnum.ADVANCED_CHAT)
|
||||
selectAppMode(AppModeEnum.ADVANCED_CHAT)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -195,7 +206,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode(AppModeEnum.CHAT)
|
||||
selectAppMode(AppModeEnum.CHAT)
|
||||
}}
|
||||
/>
|
||||
<AppTypeCard
|
||||
|
|
@ -208,7 +219,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode(AppModeEnum.AGENT_CHAT)
|
||||
selectAppMode(AppModeEnum.AGENT_CHAT)
|
||||
}}
|
||||
/>
|
||||
<AppTypeCard
|
||||
|
|
@ -221,7 +232,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
)}
|
||||
onClick={() => {
|
||||
setAppMode(AppModeEnum.COMPLETION)
|
||||
selectAppMode(AppModeEnum.COMPLETION)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -282,51 +293,47 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
<div className="mb-2 text-text-secondary system-sm-semibold">
|
||||
{t('newApp.runtimeLabel', { ns: 'app' })}
|
||||
</div>
|
||||
<CustomSelect<RuntimeOption>
|
||||
options={[
|
||||
{
|
||||
label: t('newApp.runtimeOptionSandboxed', { ns: 'app' }),
|
||||
value: 'sandboxed',
|
||||
description: t('newApp.runtimeOptionSandboxedDescription', { ns: 'app' }),
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
label: t('newApp.runtimeOptionClassic', { ns: 'app' }),
|
||||
value: 'classic',
|
||||
description: t('newApp.runtimeOptionClassicDescription', { ns: 'app' }),
|
||||
},
|
||||
]}
|
||||
<Select
|
||||
value={runtimeMode}
|
||||
onChange={value => setRuntimeMode(value as RuntimeMode)}
|
||||
triggerProps={{
|
||||
className: 'px-3',
|
||||
}}
|
||||
popupProps={{
|
||||
wrapperClassName: 'z-[60]',
|
||||
className: 'w-full',
|
||||
itemClassName: '!h-auto !py-2 !items-start',
|
||||
}}
|
||||
CustomOption={(option, selected) => (
|
||||
<>
|
||||
<RiCheckLine className={cn(
|
||||
'mr-2 h-4 w-4 shrink-0 text-text-accent',
|
||||
!selected && 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-text-secondary system-sm-semibold">{option.label}</span>
|
||||
{option.recommended && (
|
||||
<Badge className="!h-4 !px-1">
|
||||
{t('newApp.recommended', { ns: 'app' })}
|
||||
</Badge>
|
||||
)}
|
||||
onValueChange={value => setRuntimeMode(value as RuntimeMode)}
|
||||
>
|
||||
<SelectTrigger className="px-3">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[60]" popupClassName="w-full">
|
||||
{([
|
||||
{
|
||||
label: t('newApp.runtimeOptionSandboxed', { ns: 'app' }),
|
||||
value: 'sandboxed' as const,
|
||||
description: t('newApp.runtimeOptionSandboxedDescription', { ns: 'app' }),
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
label: t('newApp.runtimeOptionClassic', { ns: 'app' }),
|
||||
value: 'classic' as const,
|
||||
description: t('newApp.runtimeOptionClassicDescription', { ns: 'app' }),
|
||||
},
|
||||
] satisfies RuntimeOption[]).map(option => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className="!h-auto !items-start !py-2"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-text-secondary system-sm-semibold">{option.label}</span>
|
||||
{option.recommended && (
|
||||
<Badge className="!h-4 !px-1">
|
||||
{t('newApp.recommended', { ns: 'app' })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-text-tertiary system-xs-regular">{option.description}</span>
|
||||
</div>
|
||||
<span className="text-text-tertiary system-xs-regular">{option.description}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { importAppBundle } from '@/service/apps'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ const DSLConfirmModal = ({
|
|||
confirmDisabled = false,
|
||||
}: DSLConfirmModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const { push } = useRouter()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { handleCheckPluginDependencies } = usePluginDependencies()
|
||||
|
|
@ -54,11 +54,10 @@ const DSLConfirmModal = ({
|
|||
const { status, app_id, app_mode } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
||||
})
|
||||
if (status === DSLImportStatus.COMPLETED)
|
||||
toast.success(t('newApp.appCreated', { ns: 'app' }))
|
||||
else
|
||||
toast.warning(t('newApp.caution', { ns: 'app' }), { description: t('newApp.appCreateDSLWarning', { ns: 'app' }) })
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
if (app_id)
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
|
|
@ -68,12 +67,12 @@ const DSLConfirmModal = ({
|
|||
onCancel()
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('importBundleFailed', { ns: 'app' }) })
|
||||
toast.error(t('importBundleFailed', { ns: 'app' }))
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const error = e as Error
|
||||
notify({ type: 'error', message: error.message || t('importBundleFailed', { ns: 'app' }) })
|
||||
toast.error(error.message || t('importBundleFailed', { ns: 'app' }))
|
||||
}
|
||||
finally {
|
||||
setIsImporting(false)
|
||||
|
|
@ -81,40 +80,41 @@ const DSLConfirmModal = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => onCancel()}
|
||||
className="w-[480px]"
|
||||
>
|
||||
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
|
||||
<div className="text-text-primary title-2xl-semi-bold">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
|
||||
<div className="flex grow flex-col text-text-secondary system-md-regular">
|
||||
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
|
||||
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
|
||||
<br />
|
||||
<div>
|
||||
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
|
||||
<span className="system-md-medium">{versions.importedVersion}</span>
|
||||
</div>
|
||||
<div>
|
||||
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
|
||||
<span className="system-md-medium">{versions.systemVersion}</span>
|
||||
<Dialog open onOpenChange={open => !open && onCancel()}>
|
||||
<DialogContent className="w-[480px]">
|
||||
<DialogCloseButton />
|
||||
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
|
||||
<DialogTitle className="text-text-primary title-2xl-semi-bold">
|
||||
{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}
|
||||
</DialogTitle>
|
||||
<div className="flex grow flex-col text-text-secondary system-md-regular">
|
||||
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
|
||||
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
|
||||
<br />
|
||||
<div>
|
||||
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
|
||||
<span className="system-md-medium">{versions.importedVersion}</span>
|
||||
</div>
|
||||
<div>
|
||||
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
|
||||
<span className="system-md-medium">{versions.systemVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
|
||||
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
destructive
|
||||
onClick={handleConfirm}
|
||||
disabled={confirmDisabled || isImporting}
|
||||
loading={isImporting}
|
||||
>
|
||||
{t('newApp.Confirm', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
|
||||
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
destructive
|
||||
onClick={handleConfirm}
|
||||
disabled={confirmDisabled || isImporting}
|
||||
loading={isImporting}
|
||||
>
|
||||
{t('newApp.Confirm', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -295,7 +295,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||
<div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
{currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { useCallback, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { MARKETPLACE_API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
|
||||
import {
|
||||
fetchMarketplaceTemplateDSL,
|
||||
|
|
@ -25,7 +25,6 @@ const ImportFromMarketplaceTemplateModal = ({
|
|||
onClose,
|
||||
}: ImportFromMarketplaceTemplateModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const { data, isLoading, isError } = useMarketplaceTemplateDetail(templateId)
|
||||
const template = data?.data ?? null
|
||||
|
|
@ -41,131 +40,121 @@ const ImportFromMarketplaceTemplateModal = ({
|
|||
onConfirm(yamlContent, template)
|
||||
}
|
||||
catch {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('marketplace.template.importFailed', { ns: 'app' }),
|
||||
})
|
||||
toast.error(t('marketplace.template.importFailed', { ns: 'app' }))
|
||||
setIsImporting(false)
|
||||
}
|
||||
}, [template, templateId, isImporting, onConfirm, notify, t])
|
||||
}, [template, templateId, isImporting, onConfirm, t])
|
||||
|
||||
const templateUrl = MARKETPLACE_URL_PREFIX
|
||||
? `${MARKETPLACE_URL_PREFIX}/templates/${encodeURIComponent(templateId)}`
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl"
|
||||
isShow
|
||||
onClose={onClose}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between pb-3 pl-6 pr-5 pt-6">
|
||||
<div className="text-text-primary title-2xl-semi-bold">
|
||||
{t('marketplace.template.modalTitle', { ns: 'app' })}
|
||||
<Dialog open onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl">
|
||||
<DialogCloseButton className="right-5 top-5 h-8 w-8" />
|
||||
{/* Header */}
|
||||
<div className="pb-3 pl-6 pr-14 pt-6">
|
||||
<DialogTitle className="text-text-primary title-2xl-semi-bold">
|
||||
{t('marketplace.template.modalTitle', { ns: 'app' })}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="i-ri-close-line h-[18px] w-[18px] text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-6 pb-4">
|
||||
{isLoading && (
|
||||
<div className="flex h-[200px] items-center justify-center">
|
||||
<span className="i-ri-loader-2-line h-6 w-6 animate-spin text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && !isLoading && (
|
||||
<div className="flex h-[200px] flex-col items-center justify-center gap-2">
|
||||
<div className="text-text-tertiary system-md-regular">
|
||||
{t('marketplace.template.fetchFailed', { ns: 'app' })}
|
||||
{/* Content */}
|
||||
<div className="px-6 pb-4">
|
||||
{isLoading && (
|
||||
<div className="flex h-[200px] items-center justify-center">
|
||||
<span className="i-ri-loader-2-line h-6 w-6 animate-spin text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && !isLoading && (
|
||||
<div className="flex h-[200px] flex-col items-center justify-center gap-2">
|
||||
<div className="text-text-tertiary system-md-regular">
|
||||
{t('marketplace.template.fetchFailed', { ns: 'app' })}
|
||||
</div>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('newApp.Cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template && !isLoading && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Template info */}
|
||||
<div className="flex items-start gap-3 rounded-xl bg-background-section-burn p-4">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={template.icon_file_key ? 'image' : 'emoji'}
|
||||
icon={template.icon || '🤖'}
|
||||
background={template.icon_background || '#FFEAD5'}
|
||||
imageUrl={template.icon_file_key
|
||||
? `${MARKETPLACE_API_PREFIX}/templates/${encodeURIComponent(templateId)}/icon`
|
||||
: undefined}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="truncate text-text-primary system-md-semibold">
|
||||
{template.template_name}
|
||||
</div>
|
||||
<div className="text-text-tertiary system-xs-regular">
|
||||
{t('marketplace.template.publishedBy', { ns: 'app', publisher: template.publisher_unique_handle })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
{template.overview && (
|
||||
<div>
|
||||
<div className="mb-1 text-text-secondary system-sm-semibold">
|
||||
{t('marketplace.template.overview', { ns: 'app' })}
|
||||
</div>
|
||||
<div className="line-clamp-4 text-text-tertiary system-sm-regular">
|
||||
{template.overview}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage count */}
|
||||
{template.usage_count !== null && template.usage_count > 0 && (
|
||||
<div className="text-text-quaternary system-xs-regular">
|
||||
{t('marketplace.template.usageCount', { ns: 'app', count: template.usage_count })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Marketplace link */}
|
||||
{templateUrl && (
|
||||
<a
|
||||
href={templateUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-text-accent system-xs-regular"
|
||||
>
|
||||
{t('marketplace.template.viewOnMarketplace', { ns: 'app' })}
|
||||
<span className="i-ri-external-link-line h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{template && !isLoading && (
|
||||
<div className="flex items-center justify-end gap-3 border-t border-divider-subtle px-6 py-4">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('newApp.Cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={isImporting}
|
||||
>
|
||||
{isImporting && <span className="i-ri-loader-2-line mr-1 h-4 w-4 animate-spin" aria-hidden="true" />}
|
||||
{t('marketplace.template.importConfirm', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template && !isLoading && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Template info */}
|
||||
<div className="flex items-start gap-3 rounded-xl bg-background-section-burn p-4">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={template.icon_file_key ? 'image' : 'emoji'}
|
||||
icon={template.icon || '🤖'}
|
||||
background={template.icon_background || '#FFEAD5'}
|
||||
imageUrl={template.icon_file_key
|
||||
? `${MARKETPLACE_API_PREFIX}/templates/${encodeURIComponent(templateId)}/icon`
|
||||
: undefined}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="truncate text-text-primary system-md-semibold">
|
||||
{template.template_name}
|
||||
</div>
|
||||
<div className="text-text-tertiary system-xs-regular">
|
||||
{t('marketplace.template.publishedBy', { ns: 'app', publisher: template.publisher_unique_handle })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
{template.overview && (
|
||||
<div>
|
||||
<div className="mb-1 text-text-secondary system-sm-semibold">
|
||||
{t('marketplace.template.overview', { ns: 'app' })}
|
||||
</div>
|
||||
<div className="line-clamp-4 text-text-tertiary system-sm-regular">
|
||||
{template.overview}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage count */}
|
||||
{template.usage_count !== null && template.usage_count > 0 && (
|
||||
<div className="text-text-quaternary system-xs-regular">
|
||||
{t('marketplace.template.usageCount', { ns: 'app', count: template.usage_count })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Marketplace link */}
|
||||
{templateUrl && (
|
||||
<a
|
||||
href={templateUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-text-accent system-xs-regular"
|
||||
>
|
||||
{t('marketplace.template.viewOnMarketplace', { ns: 'app' })}
|
||||
<span className="i-ri-external-link-line h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{template && !isLoading && (
|
||||
<div className="flex items-center justify-end gap-3 border-t border-divider-subtle px-6 py-4">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('newApp.Cancel', { ns: 'app' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={isImporting}
|
||||
>
|
||||
{isImporting && <span className="i-ri-loader-2-line mr-1 h-4 w-4 animate-spin" aria-hidden="true" />}
|
||||
{t('marketplace.template.importConfirm', { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
import type { CreateAppModalProps } from '../explore/create-app-modal'
|
||||
import type { MarketplaceTemplate } from '@/service/marketplace-templates'
|
||||
import type { TryAppSelection } from '@/types/try-app'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEducationInit } from '@/app/education-apply/hooks'
|
||||
|
|
@ -11,6 +9,8 @@ import AppListContext from '@/context/app-list-context'
|
|||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
import CreateAppModal from '../explore/create-app-modal'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import Input from '@/app/components/base/input'
|
|||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const ImageInput: FC<UploaderProps> = ({
|
|||
const handleShowImage = () => {
|
||||
if (isAnimatedImage) {
|
||||
return (
|
||||
// eslint-disable-next-line next/no-img-element
|
||||
|
||||
<img src={inputImage?.url} alt="" data-testid="animated-image" />
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ describe('Avatar', () => {
|
|||
it('should render the fallback when avatar is provided', () => {
|
||||
render(<Avatar name="John" avatar="https://example.com/avatar.jpg" />)
|
||||
|
||||
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const FileTypesField = ({
|
|||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
type={type as typeof SupportUploadFileTypes.image | typeof SupportUploadFileTypes.document | typeof SupportUploadFileTypes.audio | typeof SupportUploadFileTypes.video}
|
||||
selected={field.state.value.allowedFileTypes.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { FC } from 'react'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import { useState } from 'react'
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||
data-testid="image-preview-container"
|
||||
>
|
||||
{ }
|
||||
{/* eslint-disable-next-line next/no-img-element */}
|
||||
{ }
|
||||
<img
|
||||
ref={imgRef}
|
||||
alt={title}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { ExtraProps } from 'streamdown'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
'use client'
|
||||
|
||||
/**
|
||||
* Re-exports the legacy portal / floating positioning API without using the
|
||||
* restricted `@/app/components/base/portal-to-follow-elem` import path at call sites.
|
||||
* Prefer migrating to `@/app/components/base/ui/*` overlays when touching UI.
|
||||
*/
|
||||
export {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
usePortalToFollowElem,
|
||||
usePortalToFollowElemContext,
|
||||
} from '../portal-to-follow-elem'
|
||||
|
||||
export type { PortalToFollowElemOptions } from '../portal-to-follow-elem'
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Re-exports context-menu floating hook without importing from the restricted
|
||||
* `portal-to-follow-elem/use-context-menu-floating` path at call sites.
|
||||
*/
|
||||
export {
|
||||
type Position,
|
||||
useContextMenuFloating,
|
||||
type UseContextMenuFloatingOptions,
|
||||
} from '../portal-to-follow-elem/use-context-menu-floating'
|
||||
|
|
@ -87,7 +87,9 @@ const PrePopulate: FC<Props> = ({
|
|||
readonly: false,
|
||||
zIndex: 1000000, // bigger than shortcut plugin popup
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.secret] as const).includes(
|
||||
varPayload.type as typeof VarType.string | typeof VarType.number | typeof VarType.secret,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
'use client'
|
||||
|
||||
/**
|
||||
* Re-exports the legacy default Tooltip API (popupContent, etc.) without using
|
||||
* the restricted `@/app/components/base/tooltip` import path at call sites.
|
||||
* Prefer migrating to `@/app/components/base/ui/tooltip` when touching UI.
|
||||
*/
|
||||
export { default } from '../tooltip'
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
'use client'
|
||||
|
||||
import type { ComponentType, FC, ReactNode, SVGProps } from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import styles from './style.module.css'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -11,7 +12,6 @@ type Props = {
|
|||
extraInfo?: ReactNode
|
||||
footer?: ReactNode
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const UpgradeModalBase: FC<Props> = ({
|
||||
|
|
@ -21,45 +21,40 @@ const UpgradeModalBase: FC<Props> = ({
|
|||
extraInfo,
|
||||
footer,
|
||||
show,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={onClose}
|
||||
closable={false}
|
||||
clickOutsideNotClose
|
||||
className={`${styles.surface} w-[580px] rounded-2xl !p-0`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
aria-hidden
|
||||
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
|
||||
/>
|
||||
<div className="px-8 pt-8">
|
||||
{Icon && (
|
||||
<div className={`${styles.icon} flex size-12 items-center justify-center rounded-xl shadow-lg backdrop-blur-[5px]`}>
|
||||
<Icon className="size-6 text-text-primary-on-surface" />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 space-y-2">
|
||||
<div className={`${styles.highlight} title-3xl-semi-bold`}>
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-text-tertiary system-md-regular">
|
||||
{description}
|
||||
<Dialog open={show} disablePointerDismissal>
|
||||
<DialogContent className={cn(styles.surface, 'w-[580px] rounded-2xl !p-0')}>
|
||||
<div className="relative">
|
||||
<div
|
||||
aria-hidden
|
||||
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
|
||||
/>
|
||||
<div className="px-8 pt-8">
|
||||
{Icon && (
|
||||
<div className={`${styles.icon} flex size-12 items-center justify-center rounded-xl shadow-lg backdrop-blur-[5px]`}>
|
||||
<Icon className="size-6 text-text-primary-on-surface" />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 space-y-2">
|
||||
<DialogTitle className={cn(styles.highlight, 'title-3xl-semi-bold')}>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<div className="text-text-tertiary system-md-regular">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
{extraInfo}
|
||||
</div>
|
||||
{extraInfo}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{footer && (
|
||||
<div className="mb-8 mt-10 flex justify-end space-x-2 px-8">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
{footer && (
|
||||
<div className="mb-8 mt-10 flex justify-end space-x-2 px-8">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ const PlanUpgradeModal: FC<Props> = ({
|
|||
return (
|
||||
<UpgradeModalBase
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
Icon={Icon as any}
|
||||
title={title}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import DataSourcePage from './data-source-page-new'
|
|||
import LanguagePage from './language-page'
|
||||
import MembersPage from './members-page'
|
||||
import ModelProviderPage from './model-provider-page'
|
||||
import SandboxProviderPage from './sandbox-provider-page'
|
||||
import { useResetModelProviderListExpanded } from './model-provider-page/atoms'
|
||||
import SandboxProviderPage from './sandbox-provider-page'
|
||||
|
||||
const iconClassName = `
|
||||
w-5 h-5 mr-2
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ModelIcon from '../model-icon'
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ const BASIC_INPUT_TYPE_MAP: Record<string, string> = {
|
|||
|
||||
const FILE_INPUT_TYPES = new Set(['file-list', 'file'])
|
||||
|
||||
const WORKFLOW_FILE_VAR_TYPES = new Set([InputVarType.multiFiles, InputVarType.singleFile])
|
||||
|
||||
type InputSchemaItem = {
|
||||
label?: string
|
||||
variable?: string
|
||||
|
|
@ -95,7 +93,8 @@ function mapWorkflowVariable(
|
|||
variable: Record<string, unknown>,
|
||||
fileUploadConfig?: FileUploadConfigResponse,
|
||||
): InputSchemaItem {
|
||||
const needsFileConfig = WORKFLOW_FILE_VAR_TYPES.has(variable.type as InputVarType)
|
||||
const varType = variable.type
|
||||
const needsFileConfig = varType === InputVarType.multiFiles || varType === InputVarType.singleFile
|
||||
|
||||
return {
|
||||
...variable,
|
||||
|
|
|
|||
|
|
@ -216,18 +216,30 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
|||
return VarType.string
|
||||
}
|
||||
const getFilterVar = () => {
|
||||
if (isNumber)
|
||||
if (isNumber) {
|
||||
return (varPayload: Var) => varPayload.type === VarType.number
|
||||
else if (isString)
|
||||
return (varPayload: Var) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
else if (isFile)
|
||||
return (varPayload: Var) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
else if (isBoolean)
|
||||
}
|
||||
else if (isString) {
|
||||
return (varPayload: Var) => ([VarType.string, VarType.number, VarType.secret] as const).includes(
|
||||
varPayload.type as typeof VarType.string | typeof VarType.number | typeof VarType.secret,
|
||||
)
|
||||
}
|
||||
else if (isFile) {
|
||||
return (varPayload: Var) => ([VarType.file, VarType.arrayFile] as const).includes(
|
||||
varPayload.type as typeof VarType.file | typeof VarType.arrayFile,
|
||||
)
|
||||
}
|
||||
else if (isBoolean) {
|
||||
return (varPayload: Var) => varPayload.type === VarType.boolean
|
||||
else if (isObject)
|
||||
}
|
||||
else if (isObject) {
|
||||
return (varPayload: Var) => varPayload.type === VarType.object
|
||||
else if (isArray)
|
||||
return (varPayload: Var) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
}
|
||||
else if (isArray) {
|
||||
return (varPayload: Var) => ([VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject] as const).includes(
|
||||
varPayload.type as typeof VarType.array | typeof VarType.arrayString | typeof VarType.arrayNumber | typeof VarType.arrayObject,
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,15 @@ import { produce } from 'immer'
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/app/components/base/ui/select'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
|
|
@ -293,18 +299,26 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
|||
/>
|
||||
)}
|
||||
{isSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName="h-8 grow"
|
||||
defaultValue={varInput?.value}
|
||||
items={options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
<Select
|
||||
value={varInput?.value === undefined || varInput?.value === null ? '' : String(varInput.value)}
|
||||
onValueChange={v => handleValueChange(variable, type)(v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 grow">
|
||||
<SelectValue placeholder={placeholder?.[language] || placeholder?.en_US} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any, label: { [x: string]: any, en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
onSelect={item => handleValueChange(variable, type)(item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
return true
|
||||
}).map((option: { value: any, label: { [x: string]: any, en_US: any } }) => (
|
||||
<SelectItem key={String(option.value)} value={String(option.value)}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{isShowJSONEditor && isConstant && (
|
||||
<div className="mt-1 w-full">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||
import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor'
|
||||
import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context'
|
||||
|
||||
|
|
@ -23,38 +22,33 @@ const SchemaModal: FC<Props> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="max-w-[960px] p-0"
|
||||
wrapperClassName="z-[9999]"
|
||||
>
|
||||
<div className="pb-6">
|
||||
{/* Header */}
|
||||
<div className="relative flex p-6 pb-3 pr-14">
|
||||
<div className="grow truncate text-text-primary title-2xl-semi-bold">
|
||||
{t('nodes.agent.parameterSchema', { ns: 'workflow' })}
|
||||
<Dialog open={isShow} onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="max-w-[960px] p-0">
|
||||
<DialogCloseButton className="right-5 top-5 h-8 w-8 p-1.5" />
|
||||
<div className="pb-6">
|
||||
{/* Header */}
|
||||
<div className="relative flex p-6 pb-3 pr-14">
|
||||
<DialogTitle className="grow truncate text-text-primary title-2xl-semi-bold">
|
||||
{t('nodes.agent.parameterSchema', { ns: 'workflow' })}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div className="absolute right-5 top-5 flex h-8 w-8 items-center justify-center p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
|
||||
{/* Content */}
|
||||
<div className="flex max-h-[700px] overflow-y-auto px-6 py-2">
|
||||
<MittProvider>
|
||||
<VisualEditorContextProvider>
|
||||
<VisualEditor
|
||||
className="w-full"
|
||||
schema={schema}
|
||||
rootName={rootName}
|
||||
readOnly
|
||||
>
|
||||
</VisualEditor>
|
||||
</VisualEditorContextProvider>
|
||||
</MittProvider>
|
||||
</div>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex max-h-[700px] overflow-y-auto px-6 py-2">
|
||||
<MittProvider>
|
||||
<VisualEditorContextProvider>
|
||||
<VisualEditor
|
||||
className="w-full"
|
||||
schema={schema}
|
||||
rootName={rootName}
|
||||
readOnly
|
||||
>
|
||||
</VisualEditor>
|
||||
</VisualEditorContextProvider>
|
||||
</MittProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export default React.memo(SchemaModal)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
|
@ -36,7 +37,7 @@ const ToolAuthorizationSection: FC<ToolAuthorizationSectionProps> = ({
|
|||
provider: currentProvider.name,
|
||||
category: AuthCategory.tool,
|
||||
providerType: currentProvider.type,
|
||||
detail: currentProvider as any,
|
||||
detail: currentProvider as unknown as PluginDetail,
|
||||
}}
|
||||
credentialId={credentialId}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const usePipelineInit = () => {
|
|||
setDraftUpdatedAt(res.updated_at)
|
||||
setToolPublished(res.tool_published)
|
||||
setEnvSecrets((res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
acc[env.id] = typeof env.value === 'string' ? env.value : JSON.stringify(env.value)
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
setEnvironmentVariables(res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const usePipelineRefreshDraft = () => {
|
|||
} as WorkflowDataUpdater)
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
acc[env.id] = typeof env.value === 'string' ? env.value : JSON.stringify(env.value)
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ const RagPipeline = () => {
|
|||
return []
|
||||
}, [data])
|
||||
|
||||
const initialFeatures: FeaturesData = useMemo(() => {
|
||||
const initialFeatures = useMemo(() => {
|
||||
const features = data?.features || {}
|
||||
return {
|
||||
sandbox: features.sandbox || { enabled: false },
|
||||
}
|
||||
} as FeaturesData
|
||||
}, [data?.features])
|
||||
|
||||
if (!data || isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { JsonValue } from '@/app/components/workflow/types'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { HumanInputFormData, HumanInputFormTimeoutData, NodeTracing } from '@/types/workflow'
|
||||
import { act } from '@testing-library/react'
|
||||
|
|
@ -872,10 +873,10 @@ describe('createWorkflowStreamHandlers', () => {
|
|||
|
||||
const circularOutputSetup = setupHandlers()
|
||||
const circularOutputHandlers = circularOutputSetup.handlers as Required<Pick<IOtherOptions, 'onWorkflowFinished'>>
|
||||
const circularOutputs: Record<string, unknown> = {
|
||||
const circularOutputs: Record<string, JsonValue> = {
|
||||
answer: 'Hello',
|
||||
}
|
||||
circularOutputs.self = circularOutputs
|
||||
Object.assign(circularOutputs, { self: circularOutputs as JsonValue })
|
||||
|
||||
act(() => {
|
||||
circularOutputHandlers.onWorkflowFinished({
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import copy from 'copy-to-clipboard'
|
|||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
|
||||
type IResultHeaderProps = {
|
||||
result: string
|
||||
|
|
@ -31,7 +32,7 @@ const Header: FC<IResultHeaderProps> = ({
|
|||
className="h-7 p-[2px] pr-2"
|
||||
onClick={() => {
|
||||
copy(result)
|
||||
Toast.notify({ type: 'success', message: 'copied' })
|
||||
toast.success('copied')
|
||||
}}
|
||||
>
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -133,8 +133,11 @@ const markNodesStopped = (traces?: WorkflowProcess['tracing']) => {
|
|||
return
|
||||
|
||||
const markTrace = (trace: WorkflowProcess['tracing'][number]) => {
|
||||
if ([NodeRunningStatus.Running, NodeRunningStatus.Waiting].includes(trace.status as NodeRunningStatus))
|
||||
if (([NodeRunningStatus.Running, NodeRunningStatus.Waiting] as const).includes(
|
||||
trace.status as typeof NodeRunningStatus.Running | typeof NodeRunningStatus.Waiting,
|
||||
)) {
|
||||
trace.status = NodeRunningStatus.Stopped
|
||||
}
|
||||
|
||||
trace.details?.forEach(detailGroup => detailGroup.forEach(markTrace))
|
||||
trace.retryDetail?.forEach(markTrace)
|
||||
|
|
@ -150,8 +153,11 @@ const applyWorkflowFinishedState = (
|
|||
) => {
|
||||
return updateWorkflowProcess(current, (draft) => {
|
||||
draft.status = status
|
||||
if ([WorkflowRunningStatus.Stopped, WorkflowRunningStatus.Failed].includes(status))
|
||||
if (([WorkflowRunningStatus.Stopped, WorkflowRunningStatus.Failed] as const).includes(
|
||||
status as typeof WorkflowRunningStatus.Stopped | typeof WorkflowRunningStatus.Failed,
|
||||
)) {
|
||||
markNodesStopped(draft.tracing)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -353,13 +359,23 @@ export const createWorkflowStreamHandlers = ({
|
|||
const serializedOutputs = serializeWorkflowOutputs(data.outputs)
|
||||
setCompletionRes(serializedOutputs)
|
||||
if (data.outputs) {
|
||||
const outputKeys = Object.keys(data.outputs)
|
||||
const isStringOutput = outputKeys.length === 1 && typeof data.outputs[outputKeys[0]] === 'string'
|
||||
if (isStringOutput) {
|
||||
const out = data.outputs
|
||||
if (typeof out === 'string') {
|
||||
setWorkflowProcessData(updateWorkflowProcess(getWorkflowProcessData(), (draft) => {
|
||||
draft.resultText = data.outputs[outputKeys[0]]
|
||||
draft.resultText = out
|
||||
}))
|
||||
}
|
||||
else {
|
||||
const outputKeys = Object.keys(out)
|
||||
const firstKey = outputKeys[0]
|
||||
const firstVal = firstKey !== undefined ? out[firstKey] : undefined
|
||||
const isStringOutput = outputKeys.length === 1 && typeof firstVal === 'string'
|
||||
if (isStringOutput && typeof firstVal === 'string') {
|
||||
setWorkflowProcessData(updateWorkflowProcess(getWorkflowProcessData(), (draft) => {
|
||||
draft.resultText = firstVal
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishWithSuccess()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
} from '@/app/components/base/portal-to-follow-elem-plus'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ const mockUseInspectVarsCrudCommon = vi.fn()
|
|||
const mockSetPendingSingleRun = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/workflow', () => ({
|
||||
InteractionMode: {
|
||||
Subgraph: 'subgraph',
|
||||
},
|
||||
WorkflowWithInnerContext: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="workflow-with-inner-context">{children}</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/typ
|
|||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { InteractionMode, WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { useNodesInteractions } from '@/app/components/workflow/hooks'
|
||||
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
import { useInspectVarsCrudCommon } from '@/app/components/workflow/hooks/use-inspect-vars-crud-common'
|
||||
import { InteractionMode } from '@/app/components/workflow/interaction-mode'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ const SandboxMigrationModal: FC<Props> = ({
|
|||
return (
|
||||
<UpgradeModalBase
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
Icon={Thinking}
|
||||
title={t('sandboxMigrationModal.title', { ns: 'workflow' })}
|
||||
description={t('sandboxMigrationModal.description', { ns: 'workflow' })}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { WorkflowProps } from '@/app/components/workflow'
|
|||
import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration'
|
||||
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store/store'
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||
import {
|
||||
useCallback,
|
||||
|
|
@ -98,17 +99,18 @@ const WorkflowMain = ({
|
|||
if (features && featuresStore) {
|
||||
const { setFeatures } = featuresStore.getState()
|
||||
|
||||
const transformedFeatures: FeaturesData = {
|
||||
const defaultTransferMethods = ['local_file', 'remote_url'] as TransferMethod[]
|
||||
const transformedFeatures = {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!features.file_upload?.image?.enabled,
|
||||
number_limits: features.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
transfer_methods: (features.file_upload?.image?.transfer_methods || defaultTransferMethods) as TransferMethod[],
|
||||
},
|
||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
allowed_file_upload_methods: (features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || defaultTransferMethods) as TransferMethod[],
|
||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
||||
},
|
||||
opening: {
|
||||
|
|
@ -123,7 +125,7 @@ const WorkflowMain = ({
|
|||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||
annotationReply: features.annotation_reply || { enabled: false },
|
||||
sandbox: features.sandbox || { enabled: false },
|
||||
}
|
||||
} as FeaturesData
|
||||
|
||||
setFeatures(transformedFeatures)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const NODE_HELP_LINK_OVERRIDES: Partial<Record<BlockEnum, string>> = {
|
|||
[BlockEnum.FileUpload]: 'upload-file-to-sandbox',
|
||||
}
|
||||
|
||||
type WorkflowAppScene = NodeSelectorScene.Workflow | NodeSelectorScene.Chatflow
|
||||
type WorkflowAppScene = typeof NodeSelectorScene.Workflow | typeof NodeSelectorScene.Chatflow
|
||||
|
||||
const WORKFLOW_APP_SCENE_NODES = {
|
||||
[NodeSelectorScene.Workflow]: [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { WorkflowDraftFeaturesPayload } from '@/service/workflow'
|
||||
import type { SyncDraftCallback } from '@/app/components/workflow/hooks-store'
|
||||
import type { WorkflowDraftFeaturesPayload } from '@/service/workflow'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const useWorkflowInit = () => {
|
|||
setData(res)
|
||||
workflowStore.setState({
|
||||
envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
acc[env.id] = typeof env.value === 'string' ? env.value : JSON.stringify(env.value)
|
||||
return acc
|
||||
}, {} as Record<string, string>),
|
||||
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||
|
|
@ -128,10 +128,12 @@ export const useWorkflowInit = () => {
|
|||
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
|
||||
workflowStore.setState({
|
||||
nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => {
|
||||
if (!acc[block.type])
|
||||
acc[block.type] = { ...block.config }
|
||||
if (!acc[block.type]) {
|
||||
const cfg = block.config
|
||||
acc[block.type] = (cfg && typeof cfg === 'object' && !Array.isArray(cfg) ? { ...cfg } : {}) as Record<string, unknown>
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
}, {} as Record<string, Record<string, unknown>>),
|
||||
})
|
||||
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
|
||||
const graph = publishedWorkflow?.graph
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const useWorkflowRefreshDraft = () => {
|
|||
}
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
acc[env.id] = typeof env.value === 'string' ? env.value : JSON.stringify(env.value)
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type AudioPlayer from '@/app/components/base/audio-btn/audio'
|
||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
|
|
@ -932,20 +933,22 @@ export const useWorkflowRun = () => {
|
|||
edges,
|
||||
viewport,
|
||||
})
|
||||
const f = publishedWorkflow.features ?? {}
|
||||
const mappedFeatures = {
|
||||
opening: {
|
||||
enabled: !!publishedWorkflow.features.opening_statement || !!publishedWorkflow.features.suggested_questions.length,
|
||||
opening_statement: publishedWorkflow.features.opening_statement,
|
||||
suggested_questions: publishedWorkflow.features.suggested_questions,
|
||||
enabled: !!f.opening_statement || !!f.suggested_questions?.length,
|
||||
opening_statement: f.opening_statement,
|
||||
suggested_questions: f.suggested_questions,
|
||||
},
|
||||
suggested: publishedWorkflow.features.suggested_questions_after_answer,
|
||||
text2speech: publishedWorkflow.features.text_to_speech,
|
||||
speech2text: publishedWorkflow.features.speech_to_text,
|
||||
citation: publishedWorkflow.features.retriever_resource,
|
||||
moderation: publishedWorkflow.features.sensitive_word_avoidance,
|
||||
file: publishedWorkflow.features.file_upload,
|
||||
sandbox: publishedWorkflow.features.sandbox || { enabled: false },
|
||||
}
|
||||
suggested: f.suggested_questions_after_answer || { enabled: false },
|
||||
text2speech: f.text_to_speech,
|
||||
speech2text: f.speech_to_text,
|
||||
citation: f.retriever_resource,
|
||||
moderation: f.sensitive_word_avoidance,
|
||||
file: f.file_upload,
|
||||
annotationReply: f.annotation_reply || { enabled: false },
|
||||
sandbox: f.sandbox || { enabled: false },
|
||||
} as FeaturesData
|
||||
|
||||
featuresStore?.setState({ features: mappedFeatures })
|
||||
workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
||||
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import {
|
||||
useCallback,
|
||||
|
|
@ -36,8 +36,9 @@ import {
|
|||
initialNodes,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { upgradeAppRuntime } from '@/service/apps'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
import { upgradeAppRuntime } from '@/service/apps'
|
||||
import { fetchRunDetail } from '@/service/log'
|
||||
import { useAppTriggers } from '@/service/use-tools'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
|
@ -397,17 +398,18 @@ const WorkflowAppWithAdditionalContext = () => {
|
|||
}
|
||||
|
||||
const features = data.features || {}
|
||||
const initialFeatures: FeaturesData = {
|
||||
const defaultTransferMethods = ['local_file', 'remote_url'] as TransferMethod[]
|
||||
const initialFeatures = {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!features.file_upload?.image?.enabled,
|
||||
number_limits: features.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
transfer_methods: (features.file_upload?.image?.transfer_methods || defaultTransferMethods) as TransferMethod[],
|
||||
},
|
||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
allowed_file_upload_methods: (features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || defaultTransferMethods) as TransferMethod[],
|
||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
||||
fileUploadConfig: fileUploadConfigResponse,
|
||||
},
|
||||
|
|
@ -421,8 +423,9 @@ const WorkflowAppWithAdditionalContext = () => {
|
|||
text2speech: features.text_to_speech || { enabled: false },
|
||||
citation: features.retriever_resource || { enabled: false },
|
||||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||
annotationReply: features.annotation_reply || { enabled: false },
|
||||
sandbox: features.sandbox || { enabled: false },
|
||||
}
|
||||
} as FeaturesData
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ export function createNodeDataFactory<T extends CommonNodeType & Record<string,
|
|||
}
|
||||
|
||||
export function createTriggerNode(
|
||||
triggerType: BlockEnum.TriggerSchedule | BlockEnum.TriggerWebhook | BlockEnum.TriggerPlugin = BlockEnum.TriggerWebhook,
|
||||
triggerType:
|
||||
| typeof BlockEnum.TriggerSchedule
|
||||
| typeof BlockEnum.TriggerWebhook
|
||||
| typeof BlockEnum.TriggerPlugin = BlockEnum.TriggerWebhook,
|
||||
overrides: Omit<Partial<Node>, 'data'> & { data?: Partial<CommonNodeType> & Record<string, unknown> } = {},
|
||||
): Node {
|
||||
return createNode({
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ import {
|
|||
VariableX,
|
||||
WebhookLine,
|
||||
} from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { BlockEnum } from './types'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
import { ControlMode } from '../types'
|
||||
import { CommentCursor } from './cursor'
|
||||
|
||||
const mockState = {
|
||||
const mockState: {
|
||||
controlMode: ControlMode
|
||||
isCommentPlacing: boolean
|
||||
mousePosition: { elementX: number, elementY: number }
|
||||
} = {
|
||||
controlMode: ControlMode.Pointer,
|
||||
isCommentPlacing: false,
|
||||
mousePosition: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { UserProfile } from '@/service/workflow-comment'
|
||||
import { RiArrowUpLine, RiAtLine, RiLoader2Line } from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
|
|
@ -21,6 +20,7 @@ import Textarea from 'react-textarea-autosize'
|
|||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { EnterKey } from '@/app/components/base/icons/src/public/common'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { fetchMentionableUsers } from '@/service/workflow-comment'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
|
|
|
|||
|
|
@ -3,18 +3,19 @@
|
|||
import type { FC, ReactNode } from 'react'
|
||||
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem-plus'
|
||||
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useStore } from '../store'
|
||||
import { MentionInput } from './mention-input'
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export const RETRIEVAL_OUTPUT_STRUCT = `{
|
|||
}
|
||||
}`
|
||||
|
||||
export const SUPPORT_OUTPUT_VARS_NODE = [
|
||||
export const SUPPORT_OUTPUT_VARS_NODE: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export enum NodeSelectorScene {
|
||||
Workflow = 'workflow',
|
||||
Chatflow = 'chatflow',
|
||||
RagPipeline = 'rag-pipeline',
|
||||
Subgraph = 'subgraph',
|
||||
}
|
||||
/* eslint-disable ts/no-redeclare -- const + type share names (erasable enum replacement) */
|
||||
export const NodeSelectorScene = {
|
||||
Workflow: 'workflow',
|
||||
Chatflow: 'chatflow',
|
||||
RagPipeline: 'rag-pipeline',
|
||||
Subgraph: 'subgraph',
|
||||
} as const
|
||||
export type NodeSelectorScene = typeof NodeSelectorScene[keyof typeof NodeSelectorScene]
|
||||
|
||||
export enum NodeSelectorSandboxMode {
|
||||
Enabled = 'enabled',
|
||||
Disabled = 'disabled',
|
||||
Unsupported = 'unsupported',
|
||||
}
|
||||
export const NodeSelectorSandboxMode = {
|
||||
Enabled: 'enabled',
|
||||
Disabled: 'disabled',
|
||||
Unsupported: 'unsupported',
|
||||
} as const
|
||||
export type NodeSelectorSandboxMode = typeof NodeSelectorSandboxMode[keyof typeof NodeSelectorSandboxMode]
|
||||
|
||||
export const NODE_SELECTOR_SCENE_SUPPORTS_SANDBOX: Record<NodeSelectorScene, boolean> = {
|
||||
[NodeSelectorScene.Workflow]: true,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const DSLExportConfirmModal = ({
|
|||
</div>
|
||||
</td>
|
||||
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className="truncate text-text-secondary system-xs-regular">{env.value}</div>
|
||||
<div className="truncate text-text-secondary system-xs-regular">{String(env.value ?? '')}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import {
|
|||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
} from '@/app/components/base/portal-to-follow-elem-plus'
|
||||
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { getAvatar } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
|
|||
|
|
@ -170,7 +170,9 @@ const ViewHistory = ({
|
|||
}}
|
||||
>
|
||||
{
|
||||
!isChatMode && [WorkflowRunningStatus.Stopped, WorkflowRunningStatus.Paused].includes(item.status) && (
|
||||
!isChatMode && ([WorkflowRunningStatus.Stopped, WorkflowRunningStatus.Paused] as const).includes(
|
||||
item.status as typeof WorkflowRunningStatus.Stopped | typeof WorkflowRunningStatus.Paused,
|
||||
) && (
|
||||
<span className="i-custom-vender-line-alertsAndFeedback-alert-triangle mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#F79009]" />
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { FileUpload } from '../../base/features/types'
|
||||
import type { TriggerType } from '@/app/components/workflow/header/test-run-menu'
|
||||
import type { InteractionModeType } from '@/app/components/workflow/interaction-mode'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
|
|
@ -17,7 +18,7 @@ import {
|
|||
useStore as useZustandStore,
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { InteractionMode } from '@/app/components/workflow'
|
||||
import { InteractionMode } from '@/app/components/workflow/interaction-mode'
|
||||
import { HooksStoreContext } from './provider'
|
||||
|
||||
export type WorkflowRunOptions = {
|
||||
|
|
@ -39,7 +40,7 @@ export type SyncDraftCallback = {
|
|||
onSettled?: () => void
|
||||
}
|
||||
export type CommonHooksFnMap = {
|
||||
interactionMode?: InteractionMode
|
||||
interactionMode?: InteractionModeType
|
||||
doSyncWorkflowDraft: (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: SyncDraftCallback,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ describe('useWorkflowFinished', () => {
|
|||
|
||||
result.current.handleWorkflowFinished({
|
||||
data: { status: 'succeeded', outputs: { answer: 'hello' } },
|
||||
} as WorkflowFinishedResponse)
|
||||
} as unknown as WorkflowFinishedResponse)
|
||||
|
||||
const state = store.getState().workflowRunningData!
|
||||
expect(state.result.status).toBe('succeeded')
|
||||
|
|
@ -98,7 +98,7 @@ describe('useWorkflowFinished', () => {
|
|||
|
||||
result.current.handleWorkflowFinished({
|
||||
data: { status: 'succeeded', outputs: { a: 'hello', b: 'world' } },
|
||||
} as WorkflowFinishedResponse)
|
||||
} as unknown as WorkflowFinishedResponse)
|
||||
|
||||
expect(store.getState().workflowRunningData!.resultTabActive).toBeFalsy()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Node } from '@/app/components/workflow/types'
|
||||
import type { JsonValue, Node } from '@/app/components/workflow/types'
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
|
||||
|
|
@ -248,7 +248,7 @@ export const applyAgentSubgraphInspectVars = (nodesWithInspectVars: NodeWithVar[
|
|||
id: aliasId,
|
||||
name: mapping.aliasName,
|
||||
selector: [mapping.parentNodeId, mapping.aliasName],
|
||||
value: resolvedValue,
|
||||
value: resolvedValue as JsonValue,
|
||||
visible: true,
|
||||
aliasMeta: {
|
||||
extractorNodeId: mapping.extractorNodeId,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import type { InteractionModeType } from '@/app/components/workflow/interaction-mode'
|
||||
import type { Node, ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { InteractionMode } from '@/app/components/workflow'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { InteractionMode } from '@/app/components/workflow/interaction-mode'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
|
|
@ -22,7 +23,7 @@ import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
|
|||
type Params = {
|
||||
flowType: FlowType
|
||||
flowId: string
|
||||
interactionMode?: InteractionMode
|
||||
interactionMode?: InteractionModeType
|
||||
}
|
||||
|
||||
export const useSetWorkflowVarsWithValue = ({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { InteractionModeType } from '@/app/components/workflow/interaction-mode'
|
||||
import type { Node, ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
|
|
@ -5,9 +6,9 @@ import type { VarInInspect } from '@/types/workflow'
|
|||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { InteractionMode } from '@/app/components/workflow'
|
||||
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { InteractionMode } from '@/app/components/workflow/interaction-mode'
|
||||
import {
|
||||
isConversationVar,
|
||||
isENV,
|
||||
|
|
@ -31,7 +32,7 @@ import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
|
|||
type Params = {
|
||||
flowId: string
|
||||
flowType: FlowType
|
||||
interactionMode?: InteractionMode
|
||||
interactionMode?: InteractionModeType
|
||||
}
|
||||
export const useInspectVarsCrudCommon = ({
|
||||
flowId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { useReactFlow } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import { useWorkflowStore } from '../store'
|
||||
|
|
@ -85,15 +85,11 @@ export const useLeaderRestoreListener = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const unsubscribe = collaborationManager.onRestoreRequest((data: RestoreRequestData) => {
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
message: t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}),
|
||||
duration: 3000,
|
||||
})
|
||||
toast.info(t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}), { timeout: 3000 })
|
||||
performRestore(data)
|
||||
})
|
||||
|
||||
|
|
@ -102,15 +98,11 @@ export const useLeaderRestoreListener = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const unsubscribe = collaborationManager.onRestoreIntent((data: RestoreIntentData) => {
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
message: t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}),
|
||||
duration: 3000,
|
||||
})
|
||||
toast.info(t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}), { timeout: 3000 })
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
|
|
|
|||
|
|
@ -1965,7 +1965,7 @@ export const useNodesInteractions = () => {
|
|||
]
|
||||
|
||||
// handle disallow paste node
|
||||
if (commonNestedDisallowPasteNodes.includes(nodeToPaste.data.type))
|
||||
if ((commonNestedDisallowPasteNodes as BlockEnum[]).includes(nodeToPaste.data.type))
|
||||
return
|
||||
|
||||
// handle paste to nested block
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment'
|
||||
import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ export const useWorkflowFinished = () => {
|
|||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const isStringOutput = data.outputs && Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
|
||||
const out = data.outputs
|
||||
const outputKeys = out && typeof out === 'object' && !Array.isArray(out) ? Object.keys(out) : []
|
||||
const firstOutputKey = outputKeys[0]
|
||||
const firstOutputVal = firstOutputKey !== undefined && out && typeof out === 'object' && !Array.isArray(out)
|
||||
? (out as Record<string, unknown>)[firstOutputKey]
|
||||
: undefined
|
||||
const isSingleKeyedString = outputKeys.length === 1 && typeof firstOutputVal === 'string'
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
|
|
@ -22,9 +28,13 @@ export const useWorkflowFinished = () => {
|
|||
...data,
|
||||
files: getFilesInLogs(data.outputs),
|
||||
} as any
|
||||
if (isStringOutput) {
|
||||
if (typeof out === 'string') {
|
||||
draft.resultTabActive = true
|
||||
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
|
||||
draft.resultText = out
|
||||
}
|
||||
else if (isSingleKeyedString && typeof firstOutputVal === 'string') {
|
||||
draft.resultTabActive = true
|
||||
draft.resultText = firstOutputVal
|
||||
}
|
||||
}))
|
||||
}, [workflowStore])
|
||||
|
|
|
|||
|
|
@ -47,13 +47,17 @@ export const useWorkflowNodeFinished = () => {
|
|||
currentNode.data._runningBranchId = ErrorHandleTypeEnum.failBranch
|
||||
}
|
||||
else {
|
||||
const rawOutputs = data.outputs
|
||||
const outputRecord = rawOutputs && typeof rawOutputs === 'object' && !Array.isArray(rawOutputs)
|
||||
? rawOutputs as Record<string, string | undefined>
|
||||
: undefined
|
||||
if (data.node_type === BlockEnum.IfElse)
|
||||
currentNode.data._runningBranchId = data?.outputs?.selected_case_id
|
||||
currentNode.data._runningBranchId = outputRecord?.selected_case_id
|
||||
|
||||
if (data.node_type === BlockEnum.QuestionClassifier)
|
||||
currentNode.data._runningBranchId = data?.outputs?.class_id
|
||||
currentNode.data._runningBranchId = outputRecord?.class_id
|
||||
if (data.node_type === BlockEnum.HumanInput)
|
||||
currentNode.data._runningBranchId = data?.outputs?.__action_id
|
||||
currentNode.data._runningBranchId = outputRecord?.__action_id
|
||||
}
|
||||
})
|
||||
setNodes(newNodes)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
} from 'reactflow'
|
||||
import type { CursorPosition, OnlineUser } from './collaboration/types'
|
||||
import type { Shape as HooksStoreShape } from './hooks-store'
|
||||
import type { InteractionModeType } from './interaction-mode'
|
||||
import type { WorkflowSliceShape } from './store/workflow/workflow-slice'
|
||||
import type {
|
||||
ConversationVariable,
|
||||
|
|
@ -14,7 +15,7 @@ import type {
|
|||
EnvironmentVariable,
|
||||
Node,
|
||||
} from './types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import type { VarInInspect, WorkflowDraftFeatures } from '@/types/workflow'
|
||||
import {
|
||||
useEventListener,
|
||||
} from 'ahooks'
|
||||
|
|
@ -41,10 +42,18 @@ import ReactFlow, {
|
|||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@/app/components/base/ui/alert-dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { IS_DEV } from '@/config'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
|
|
@ -86,6 +95,7 @@ import {
|
|||
import { HooksStoreContextProvider, useHooksStore } from './hooks-store'
|
||||
import { useWorkflowComment } from './hooks/use-workflow-comment'
|
||||
import { useWorkflowSearch } from './hooks/use-workflow-search'
|
||||
import { InteractionMode } from './interaction-mode'
|
||||
import NodeContextmenu from './node-contextmenu'
|
||||
import CustomNode from './nodes'
|
||||
import useMatchSchemaType from './nodes/_base/components/variable/use-match-schema-type'
|
||||
|
|
@ -120,10 +130,6 @@ import { WorkflowHistoryProvider } from './workflow-history-store'
|
|||
import 'reactflow/dist/style.css'
|
||||
import './style.css'
|
||||
|
||||
const Confirm = dynamic(() => import('@/app/components/base/confirm'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
||||
|
|
@ -137,17 +143,12 @@ const edgeTypes = {
|
|||
[CUSTOM_EDGE]: CustomEdge,
|
||||
}
|
||||
|
||||
export enum InteractionMode {
|
||||
Default = 'default',
|
||||
Subgraph = 'subgraph',
|
||||
}
|
||||
|
||||
type WorkflowDataUpdatePayload = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport?: Viewport
|
||||
hash?: string
|
||||
features?: unknown
|
||||
features?: WorkflowDraftFeatures
|
||||
conversation_variables?: ConversationVariable[]
|
||||
environment_variables?: EnvironmentVariable[]
|
||||
}
|
||||
|
|
@ -160,7 +161,7 @@ export type WorkflowProps = {
|
|||
onWorkflowDataUpdate?: (v: WorkflowDataUpdatePayload) => void
|
||||
allowSelectionWhenReadOnly?: boolean
|
||||
canvasReadOnly?: boolean
|
||||
interactionMode?: InteractionMode
|
||||
interactionMode?: InteractionModeType
|
||||
cursors?: Record<string, CursorPosition>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: OnlineUser[]
|
||||
|
|
@ -285,10 +286,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
|
||||
useEffect(() => {
|
||||
return collaborationManager.onHistoryAction((_) => {
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
message: t('collaboration.historyAction.generic', { ns: 'workflow' }),
|
||||
})
|
||||
toast.info(t('collaboration.historyAction.generic', { ns: 'workflow' }))
|
||||
})
|
||||
}, [t])
|
||||
const {
|
||||
|
|
@ -642,14 +640,39 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
{!isSubGraph && <EdgeContextmenu />}
|
||||
{!isSubGraph && <SelectionContextmenu />}
|
||||
{!isSubGraph && <HelpLine />}
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
onCancel={() => setShowConfirm(undefined)}
|
||||
onConfirm={showConfirm.onConfirm}
|
||||
title={showConfirm.title}
|
||||
content={showConfirm.desc}
|
||||
/>
|
||||
{showConfirm && (
|
||||
<AlertDialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
setShowConfirm(undefined)
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 p-6 pb-4">
|
||||
<AlertDialogTitle className="text-text-primary title-2xl-semi-bold">
|
||||
{showConfirm.title}
|
||||
</AlertDialogTitle>
|
||||
{showConfirm.desc && (
|
||||
<AlertDialogDescription className="text-text-secondary system-sm-regular">
|
||||
{showConfirm.desc}
|
||||
</AlertDialogDescription>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
onClick={() => {
|
||||
void showConfirm.onConfirm()
|
||||
}}
|
||||
>
|
||||
{t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{controlMode === ControlMode.Comment && isMouseOverCanvas && (
|
||||
<CommentCursor />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export const InteractionMode = {
|
||||
Default: 'default',
|
||||
Subgraph: 'subgraph',
|
||||
} as const
|
||||
|
||||
export type InteractionModeType = typeof InteractionMode[keyof typeof InteractionMode]
|
||||
|
|
@ -102,7 +102,7 @@ const FormItem: FC<Props> = ({
|
|||
})()
|
||||
|
||||
const isBooleanType = type === InputVarType.checkbox
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
|
||||
const isArrayLikeType = ([InputVarType.contexts, InputVarType.iterator] as InputVarType[]).includes(type)
|
||||
const isContext = type === InputVarType.contexts
|
||||
const isIterator = type === InputVarType.iterator
|
||||
const isIteratorItemFile = isIterator && payload.isFileItem
|
||||
|
|
|
|||
|
|
@ -61,10 +61,12 @@ const Form: FC<Props> = ({
|
|||
onChange(newValues)
|
||||
}
|
||||
}, [valuesRef, onChange, mapKeysWithSameValueSelector])
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(inputs[0]?.type)
|
||||
const isIteratorItemFile = inputs[0]?.type === InputVarType.iterator && inputs[0]?.isFileItem
|
||||
const firstInputType = inputs[0]?.type
|
||||
const isArrayLikeType = firstInputType !== undefined
|
||||
&& ([InputVarType.contexts, InputVarType.iterator] as InputVarType[]).includes(firstInputType)
|
||||
const isIteratorItemFile = firstInputType === InputVarType.iterator && inputs[0]?.isFileItem
|
||||
|
||||
const isContext = inputs[0]?.type === InputVarType.contexts
|
||||
const isContext = firstInputType === InputVarType.contexts
|
||||
const handleAddContext = useCallback(() => {
|
||||
const newValues = produce(values, (draft: any) => {
|
||||
const key = inputs[0].variable
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const ConfigVision: FC<Props> = ({
|
|||
const { t } = useTranslation()
|
||||
|
||||
const filterVar = useCallback((payload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(payload.type)
|
||||
return ([VarType.file, VarType.arrayFile] as VarType[]).includes(payload.type)
|
||||
}, [])
|
||||
const handleVisionResolutionChange = useCallback((resolution: Resolution) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ const CodeEditor: FC<Props> = ({
|
|||
value = '',
|
||||
placeholder = '',
|
||||
onChange = noop,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onBlur: _onBlur,
|
||||
onFocus: _onFocus,
|
||||
title = '',
|
||||
headerRight,
|
||||
language,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ import { cn } from '@/utils/classnames'
|
|||
import { SupportUploadFileTypes } from '../../../types'
|
||||
|
||||
type Props = {
|
||||
type: SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video | SupportUploadFileTypes.custom
|
||||
type:
|
||||
| typeof SupportUploadFileTypes.image
|
||||
| typeof SupportUploadFileTypes.document
|
||||
| typeof SupportUploadFileTypes.audio
|
||||
| typeof SupportUploadFileTypes.video
|
||||
| typeof SupportUploadFileTypes.custom
|
||||
selected: boolean
|
||||
onToggle: (type: SupportUploadFileTypes) => void
|
||||
onCustomFileTypesChange?: (customFileTypes: string[]) => void
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const FileUploadSetting: FC<Props> = ({
|
|||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
type={type as typeof SupportUploadFileTypes.image | typeof SupportUploadFileTypes.document | typeof SupportUploadFileTypes.audio | typeof SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
|
|
@ -179,7 +179,7 @@ const FileUploadSetting: FC<Props> = ({
|
|||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
type={type as typeof SupportUploadFileTypes.image | typeof SupportUploadFileTypes.document | typeof SupportUploadFileTypes.audio | typeof SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { NestedNodeConfig, ResourceVarInputs } from '../types'
|
||||
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { FormOption as BaseSelectFormOption } from '@/app/components/base/form/types'
|
||||
import type { CredentialFormSchema, FormOption, FormShowOnObject, TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Event, Tool } from '@/app/components/tools/types'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
|
|
@ -34,18 +35,44 @@ import { VarKindType } from '../types'
|
|||
import FormInputBoolean from './form-input-boolean'
|
||||
import FormInputTypeSwitch from './form-input-type-switch'
|
||||
|
||||
type SelectOptionRow = FormOption | BaseSelectFormOption
|
||||
|
||||
function credentialInputMatchesShowOn(value: ResourceVarInputs, showOnItem: FormShowOnObject): boolean {
|
||||
const entry = value[showOnItem.variable]
|
||||
const comparable = entry !== undefined && entry !== null && typeof entry === 'object' && 'value' in entry
|
||||
? (entry as { value: unknown }).value
|
||||
: entry
|
||||
return comparable === showOnItem.value
|
||||
}
|
||||
|
||||
function selectOptionDisplayLabel(opt: SelectOptionRow, language: string): string {
|
||||
const { label } = opt
|
||||
if (typeof label === 'string')
|
||||
return label
|
||||
if (label && typeof label === 'object')
|
||||
return (label as Record<string, string | undefined>)[language] || (label as { en_US?: string }).en_US || opt.value
|
||||
return opt.value
|
||||
}
|
||||
|
||||
type CredentialFormSchemaRuntime = CredentialFormSchema & {
|
||||
_type?: FormTypeEnum
|
||||
multiple?: boolean
|
||||
options?: FormOption[]
|
||||
placeholder?: TypeWithI18N
|
||||
}
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema
|
||||
value: ResourceVarInputs
|
||||
onChange: (value: any) => void
|
||||
onChange: (value: ResourceVarInputs) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool | Event
|
||||
currentProvider?: ToolWithProvider | TriggerWithProvider
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
extraParams?: Record<string, any>
|
||||
extraParams?: Record<string, unknown>
|
||||
providerType?: string
|
||||
disableVariableInsertion?: boolean
|
||||
}
|
||||
|
|
@ -59,7 +86,7 @@ type VariableReferenceFieldsProps = {
|
|||
varInput: ResourceVarInputs[string]
|
||||
targetVarType: string
|
||||
filterVar?: (payload: Var, selector: ValueSelector) => boolean
|
||||
onValueChange: (newValue: any) => void
|
||||
onValueChange: (newValue: unknown) => void
|
||||
onVariableSelectorChange: (newValue: ValueSelector | string) => void
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
|
|
@ -156,7 +183,7 @@ const FormInputItem: FC<Props> = ({
|
|||
const hooksStore = useContext(HooksStoreContext)
|
||||
const workflowStore = useContext(WorkflowContext)
|
||||
const canUseWorkflowHooks = !!hooksStore && !!workflowStore
|
||||
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
|
||||
const [toolsOptions, setToolsOptions] = useState<SelectOptionRow[] | null>(null)
|
||||
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
|
||||
|
||||
const {
|
||||
|
|
@ -165,10 +192,10 @@ const FormInputItem: FC<Props> = ({
|
|||
type,
|
||||
_type,
|
||||
default: defaultValue,
|
||||
options,
|
||||
options = [],
|
||||
multiple,
|
||||
scope,
|
||||
} = schema as any
|
||||
} = schema as CredentialFormSchemaRuntime
|
||||
const varInput = value[variable]
|
||||
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
|
|
@ -215,17 +242,17 @@ const FormInputItem: FC<Props> = ({
|
|||
|
||||
const getFilterVar = () => {
|
||||
if (isNumber)
|
||||
return (varPayload: any) => varPayload.type === VarType.number
|
||||
return (varPayload: Var) => varPayload.type === VarType.number
|
||||
else if (isString)
|
||||
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
return (varPayload: Var) => ([VarType.string, VarType.number, VarType.secret] as VarType[]).includes(varPayload.type)
|
||||
else if (isFile)
|
||||
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
return (varPayload: Var) => ([VarType.file, VarType.arrayFile] as VarType[]).includes(varPayload.type)
|
||||
else if (isBoolean)
|
||||
return (varPayload: any) => varPayload.type === VarType.boolean
|
||||
return (varPayload: Var) => varPayload.type === VarType.boolean
|
||||
else if (isObject)
|
||||
return (varPayload: any) => varPayload.type === VarType.object
|
||||
return (varPayload: Var) => varPayload.type === VarType.object
|
||||
else if (isArray)
|
||||
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayMessage].includes(varPayload.type)
|
||||
return (varPayload: Var) => ([VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayMessage] as VarType[]).includes(varPayload.type)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
@ -320,17 +347,17 @@ const FormInputItem: FC<Props> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const handleValueChange = (newValue: any, newType?: VarKindType, nestedNodeConfig?: NestedNodeConfig | null) => {
|
||||
const normalizedValue = isNumber ? Number.parseFloat(newValue) : newValue
|
||||
const handleValueChange = (newValue: unknown, newType?: VarKindType, nestedNodeConfig?: NestedNodeConfig | null) => {
|
||||
const normalizedValue = isNumber ? Number.parseFloat(String(newValue)) : newValue
|
||||
const assemblePlaceholder = nodeId && variable
|
||||
? `{{#${nodeId}_ext_${variable}.result#}}`
|
||||
: ''
|
||||
const isAssembleValue = typeof normalizedValue === 'string'
|
||||
&& assemblePlaceholder
|
||||
&& normalizedValue.includes(assemblePlaceholder)
|
||||
const resolvedType = isAssembleValue
|
||||
const resolvedType: VarKindType = isAssembleValue
|
||||
? VarKindType.nested_node
|
||||
: newType ?? (varInput?.type === VarKindType.nested_node ? VarKindType.nested_node : getVarKindType())
|
||||
: newType ?? (varInput?.type === VarKindType.nested_node ? VarKindType.nested_node : getVarKindType() ?? VarKindType.constant)
|
||||
const resolvedNestedNodeConfig = resolvedType === VarKindType.nested_node
|
||||
? (nestedNodeConfig ?? varInput?.nested_node_config ?? {
|
||||
extractor_node_id: nodeId && variable ? `${nodeId}_ext_${variable}` : '',
|
||||
|
|
@ -351,25 +378,26 @@ const FormInputItem: FC<Props> = ({
|
|||
})
|
||||
}
|
||||
|
||||
const getSelectedLabels = (selectedValues: any[]) => {
|
||||
if (!selectedValues || selectedValues.length === 0)
|
||||
const getSelectedLabels = (selectedValues: unknown) => {
|
||||
if (!Array.isArray(selectedValues) || selectedValues.length === 0)
|
||||
return ''
|
||||
|
||||
const optionsList = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
|
||||
const selectedOptions = optionsList.filter((opt: any) =>
|
||||
selectedValues.includes(opt.value),
|
||||
const values = selectedValues as string[]
|
||||
const optionsList: SelectOptionRow[] = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
|
||||
const selectedOptions = optionsList.filter((opt: SelectOptionRow) =>
|
||||
values.includes(opt.value),
|
||||
)
|
||||
|
||||
if (selectedOptions.length <= 2) {
|
||||
return selectedOptions
|
||||
.map((opt: any) => opt.label?.[language] || opt.label?.en_US || opt.value)
|
||||
.map((opt: SelectOptionRow) => selectOptionDisplayLabel(opt, language))
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
return `${selectedOptions.length} selected`
|
||||
}
|
||||
|
||||
const handleAppOrModelSelect = (newValue: any) => {
|
||||
const handleAppOrModelSelect = (newValue: unknown) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
|
|
@ -391,9 +419,9 @@ const FormInputItem: FC<Props> = ({
|
|||
}
|
||||
|
||||
const availableCheckboxOptions = useMemo(() => (
|
||||
(options || []).filter((option: { show_on?: Array<{ variable: string, value: any }> }) => {
|
||||
(options || []).filter((option: { show_on?: FormShowOnObject[] }) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable]?.value === showOnItem.value || value[showOnItem.variable] === showOnItem.value)
|
||||
return option.show_on.every(showOnItem => credentialInputMatchesShowOn(value, showOnItem))
|
||||
return true
|
||||
})
|
||||
), [options, value])
|
||||
|
|
@ -496,19 +524,19 @@ const FormInputItem: FC<Props> = ({
|
|||
wrapperClassName="h-8 grow"
|
||||
disabled={readOnly}
|
||||
defaultValue={varInput?.value}
|
||||
items={options.filter((option: { show_on: any[] }) => {
|
||||
items={options.filter((option: FormOption) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return option.show_on.every(showOnItem => credentialInputMatchesShowOn(value, showOnItem))
|
||||
|
||||
return true
|
||||
}).map((option: { value: any, label: { [x: string]: any, en_US: any }, icon?: string }) => ({
|
||||
}).map((option: FormOption) => ({
|
||||
value: option.value,
|
||||
name: option.label[language] || option.label.en_US,
|
||||
name: selectOptionDisplayLabel(option, language),
|
||||
icon: option.icon,
|
||||
}))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
renderOption={options.some((opt: any) => opt.icon)
|
||||
renderOption={options.some((opt: FormOption) => opt.icon)
|
||||
? ({ item }) => (
|
||||
<div className="flex items-center">
|
||||
{item.icon && (
|
||||
|
|
@ -540,11 +568,11 @@ const FormInputItem: FC<Props> = ({
|
|||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
|
||||
{options.filter((option: { show_on: any[] }) => {
|
||||
{options.filter((option: FormOption) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return option.show_on.every(showOnItem => credentialInputMatchesShowOn(value, showOnItem))
|
||||
return true
|
||||
}).map((option: { value: any, label: { [x: string]: any, en_US: any }, icon?: string }) => (
|
||||
}).map((option: FormOption) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
|
|
@ -558,7 +586,7 @@ const FormInputItem: FC<Props> = ({
|
|||
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className={cn('block truncate', selected && 'font-normal')}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
{selectOptionDisplayLabel(option, language)}
|
||||
</span>
|
||||
</div>
|
||||
{selected && (
|
||||
|
|
@ -579,14 +607,14 @@ const FormInputItem: FC<Props> = ({
|
|||
wrapperClassName="h-8 grow"
|
||||
disabled={readOnly || isLoadingOptions}
|
||||
defaultValue={varInput?.value}
|
||||
items={(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
|
||||
items={(dynamicOptions || options || []).filter((option: SelectOptionRow) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return option.show_on.every(showOnItem => credentialInputMatchesShowOn(value, showOnItem))
|
||||
|
||||
return true
|
||||
}).map((option: { value: any, label: { [x: string]: any, en_US: any }, icon?: string }) => ({
|
||||
}).map((option: SelectOptionRow) => ({
|
||||
value: option.value,
|
||||
name: option.label[language] || option.label.en_US,
|
||||
name: selectOptionDisplayLabel(option, language),
|
||||
icon: option.icon,
|
||||
}))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
|
|
@ -632,11 +660,11 @@ const FormInputItem: FC<Props> = ({
|
|||
</span>
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm">
|
||||
{(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
|
||||
{(dynamicOptions || options || []).filter((option: SelectOptionRow) => {
|
||||
if (option.show_on?.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
return option.show_on.every(showOnItem => credentialInputMatchesShowOn(value, showOnItem))
|
||||
return true
|
||||
}).map((option: { value: any, label: { [x: string]: any, en_US: any }, icon?: string }) => (
|
||||
}).map((option: SelectOptionRow) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
|
|
@ -650,7 +678,7 @@ const FormInputItem: FC<Props> = ({
|
|||
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className={cn('block truncate', selected && 'font-normal')}>
|
||||
{option.label[language] || option.label.en_US}
|
||||
{selectOptionDisplayLabel(option, language)}
|
||||
</span>
|
||||
</div>
|
||||
{selected && (
|
||||
|
|
@ -670,7 +698,14 @@ const FormInputItem: FC<Props> = ({
|
|||
<div className="mt-1 w-full">
|
||||
<CodeEditor
|
||||
title="JSON"
|
||||
value={varInput?.value as any}
|
||||
value={(() => {
|
||||
const v = varInput?.value
|
||||
if (v === undefined || v === null)
|
||||
return undefined
|
||||
if (typeof v === 'string' || typeof v === 'object')
|
||||
return v as string | object
|
||||
return undefined
|
||||
})()}
|
||||
isExpand
|
||||
isInNode
|
||||
language={CodeLanguage.json}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ const Item: FC<ItemProps> = ({
|
|||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && (itemData.children as Var[]).length > 0)
|
||||
const isObj = (([VarType.object, VarType.file] as VarType[]).includes(itemData.type) && itemData.children && (itemData.children as Var[]).length > 0)
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
}, [pendingSingleRun, id, handleSingleRun, handleStop, setPendingSingleRun])
|
||||
|
||||
const logParams = useLogs()
|
||||
const passedLogParams = useMemo(() => [BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type) ? logParams : {}, [data.type, logParams])
|
||||
const passedLogParams = useMemo(() => ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop] as BlockEnum[]).includes(data.type) ? logParams : {}, [data.type, logParams])
|
||||
|
||||
const storeBuildInTools = useStore(s => s.buildInTools)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ const LastRun: FC<Props> = ({
|
|||
const [pageHasHide, setPageHasHide] = useState(false)
|
||||
const [pageShowed, setPageShowed] = useState(false)
|
||||
|
||||
const hidePageOneStepRunFinished = [NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(hidePageOneStepFinishedStatus!)
|
||||
const hidePageOneStepRunFinished = hidePageOneStepFinishedStatus != null
|
||||
&& ([NodeRunningStatus.Succeeded, NodeRunningStatus.Failed] as NodeRunningStatus[]).includes(hidePageOneStepFinishedStatus)
|
||||
const canRunLastRun = !isRunAfterSingleRun || isOneStepRunSucceed || isOneStepRunFailed || (pageHasHide && hidePageOneStepRunFinished)
|
||||
const { data: lastRunResult, isFetching, error } = useLastRun(configsMap?.flowType || FlowType.appFlow, configsMap?.flowId || '', nodeId, canRunLastRun)
|
||||
const isRunning = useMemo(() => {
|
||||
|
|
@ -55,7 +56,7 @@ const LastRun: FC<Props> = ({
|
|||
|
||||
if (!isRunAfterSingleRun)
|
||||
return isFetching
|
||||
return [NodeRunningStatus.Running, NodeRunningStatus.NotStart].includes(oneStepRunRunningStatus!)
|
||||
return ([NodeRunningStatus.Running, NodeRunningStatus.NotStart] as NodeRunningStatus[]).includes(oneStepRunRunningStatus!)
|
||||
}, [isFetching, isPaused, isRunAfterSingleRun, oneStepRunRunningStatus])
|
||||
|
||||
const noLastRun = (error as any)?.status === 404
|
||||
|
|
@ -87,7 +88,7 @@ const LastRun: FC<Props> = ({
|
|||
}, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if ([NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(oneStepRunRunningStatus!))
|
||||
if (([NodeRunningStatus.Succeeded, NodeRunningStatus.Failed] as NodeRunningStatus[]).includes(oneStepRunRunningStatus!))
|
||||
setHidePageOneStepFinishedStatus(oneStepRunRunningStatus!)
|
||||
}, [oneStepRunRunningStatus])
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ const varTypeToInputVarType = (type: VarType, {
|
|||
return InputVarType.number
|
||||
if (type === VarType.boolean)
|
||||
return InputVarType.checkbox
|
||||
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
|
||||
if (([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject] as VarType[]).includes(type))
|
||||
return InputVarType.json
|
||||
if (type === VarType.file)
|
||||
return InputVarType.singleFile
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
|||
// vars
|
||||
|
||||
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
|
||||
return [
|
||||
return ([
|
||||
VarKindType.arrayObject,
|
||||
VarKindType.array,
|
||||
VarKindType.number,
|
||||
|
|
@ -167,7 +167,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
|||
VarKindType.arrayNumber,
|
||||
VarKindType.file,
|
||||
VarKindType.arrayFile,
|
||||
].includes(varPayload.type)
|
||||
] as VarKindType[]).includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
|||
})
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.boolean, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayBoolean, VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.boolean, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayBoolean, VarType.file, VarType.arrayFile] as VarType[]).includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ const nodeDefault: NodeDefault<CommandNodeType> = {
|
|||
working_directory: '',
|
||||
command: '',
|
||||
},
|
||||
checkValid(payload: CommandNodeType, t: any) {
|
||||
checkValid(payload: CommandNodeType, t: Parameters<NodeDefault<CommandNodeType>['checkValid']>[1]) {
|
||||
let errorMessages = ''
|
||||
const { command } = payload
|
||||
const translate = t as (key: string, options?: Record<string, unknown>) => string
|
||||
|
||||
if (!errorMessages && !command)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.command`, { ns: 'workflow' }) })
|
||||
errorMessages = translate(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: translate(`${i18nPrefix}.fields.command`, { ns: 'workflow' }) })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const ApiInput: FC<Props> = ({
|
|||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.secret] as VarType[]).includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const Authorization: FC<Props> = ({
|
|||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.secret] as VarType[]).includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const EditBody: FC<Props> = ({
|
|||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret, VarType.arrayNumber, VarType.arrayString].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.secret, VarType.arrayNumber, VarType.arrayString] as VarType[]).includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ const EditBody: FC<Props> = ({
|
|||
}, [onChange, payload])
|
||||
|
||||
const filterOnlyFileVariable = (varPayload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
return ([VarType.file, VarType.arrayFile] as VarType[]).includes(varPayload.type)
|
||||
}
|
||||
|
||||
const handleBodyValueChange = useCallback((value: string) => {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,10 @@ const InputItem: FC<Props> = ({
|
|||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
const supportVarTypes = [VarType.string, VarType.number, VarType.secret]
|
||||
if (isSupportFile)
|
||||
const supportVarTypes: VarType[] = [VarType.string, VarType.number, VarType.secret]
|
||||
if (isSupportFile) {
|
||||
supportVarTypes.push(VarType.file, VarType.arrayFile)
|
||||
}
|
||||
|
||||
return supportVarTypes.includes(varPayload.type)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const KeyValueItem: FC<Props> = ({
|
|||
}, [onChange, payload])
|
||||
|
||||
const filterOnlyFileVariable = (varPayload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
return ([VarType.file, VarType.arrayFile] as VarType[]).includes(varPayload.type)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
|||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.secret] as VarType[]).includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// curl import panel
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
|
|||
const { availableVars, availableNodesWithParent } = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
return ([VarType.string, VarType.number, VarType.secret] as VarType[]).includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
|
|||
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayBoolean, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
return ([VarType.array, VarType.arrayString, VarType.arrayBoolean, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile] as VarType[]).includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ const panelProps: PanelProps = {
|
|||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
setRunInputData: vi.fn(),
|
||||
runResult: undefined,
|
||||
runResult: null,
|
||||
}
|
||||
|
||||
describe('KnowledgeBasePanel', () => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue