diff --git a/.gitignore b/.gitignore index 74a9ef63ef..4c938b7682 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,6 @@ mise.toml # Next.js build output .next/ + +# AI Assistant +.roo/ diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 5f209736a0..0107df22c5 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description="Dify version", - default="1.4.2", + default="1.4.3", ) COMMIT_SHA: str = Field( diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py index 9d229185f3..10c3cdcf0e 100644 --- a/api/controllers/web/passport.py +++ b/api/controllers/web/passport.py @@ -163,7 +163,7 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: ) db.session.add(end_user) db.session.commit() - exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24) + exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES) exp = int(exp_dt.timestamp()) payload = { "iss": site.id, diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 8c85f91d7e..9e6adc4b08 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping from typing import Any, Literal, Optional, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from sqlalchemy.orm import sessionmaker @@ -31,6 +31,7 @@ from core.workflow.repositories.workflow_execution_repository import WorkflowExe from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom from services.conversation_service import ConversationService @@ -399,20 +400,17 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation_id=conversation.id, - message_id=message.id, - context=context, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "conversation_id": conversation.id, + "message_id": message.id, + "context": context, + }, + ) worker_thread.start() @@ -449,24 +447,9 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): :param message_id: message ID :return: """ - for var, val in context.items(): - var.set(val) - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index 158196f24d..a448bf8a94 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping from typing import Any, Literal, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from configs import dify_config @@ -23,6 +23,7 @@ from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser from services.conversation_service import ConversationService from services.errors.message import MessageNotExistsError @@ -182,20 +183,17 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - context=context, - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation_id=conversation.id, - message_id=message.id, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "context": context, + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "conversation_id": conversation.id, + "message_id": message.id, + }, + ) worker_thread.start() @@ -229,24 +227,9 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): :param message_id: message ID :return: """ - for var, val in context.items(): - var.set(val) - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index f4aec3479b..7f4770fc97 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping, Sequence from typing import Any, Literal, Optional, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from sqlalchemy.orm import sessionmaker @@ -29,6 +29,7 @@ from core.workflow.repositories.workflow_execution_repository import WorkflowExe from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom @@ -209,19 +210,16 @@ class WorkflowAppGenerator(BaseAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - context=context, - workflow_thread_pool_id=workflow_thread_pool_id, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "context": context, + "workflow_thread_pool_id": workflow_thread_pool_id, + }, + ) worker_thread.start() @@ -408,24 +406,9 @@ class WorkflowAppGenerator(BaseAppGenerator): :param workflow_thread_pool_id: workflow thread pool id :return: """ - for var, val in context.items(): - var.set(val) - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # workflow app runner = WorkflowAppRunner( application_generate_entity=application_generate_entity, diff --git a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py index 8fe6199517..c6cf0d2b27 100644 --- a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py +++ b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py @@ -41,6 +41,12 @@ class WeaviateVector(BaseVector): weaviate.connect.connection.has_grpc = False + # Fix to minimize the performance impact of the deprecation check in weaviate-client 3.24.0, + # by changing the connection timeout to pypi.org from 1 second to 0.001 seconds. + # TODO: This can be removed once weaviate-client is updated to 3.26.7 or higher, + # which does not contain the deprecation check. + weaviate.connect.connection.PYPI_TIMEOUT = 0.001 + try: client = weaviate.Client( url=config.endpoint, auth_client_secret=auth_config, timeout_config=(5, 60), startup_period=None diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 86654e6fac..8f11225f28 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -9,7 +9,7 @@ from copy import copy, deepcopy from datetime import UTC, datetime from typing import Any, Optional, cast -from flask import Flask, current_app, has_request_context +from flask import Flask, current_app from configs import dify_config from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError @@ -53,6 +53,7 @@ from core.workflow.nodes.end.end_stream_processor import EndStreamProcessor from core.workflow.nodes.enums import ErrorStrategy, FailBranchSourceHandle from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING +from libs.flask_utils import preserve_flask_contexts from models.enums import UserFrom from models.workflow import WorkflowType @@ -537,24 +538,9 @@ class GraphEngine: """ Run parallel nodes """ - for var, val in context.items(): - var.set(val) - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - q.put( ParallelBranchRunStartedEvent( parallel_id=parallel_id, diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index faa8f90bea..22c564c1fc 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -214,7 +214,7 @@ class AgentNode(ToolNode): ) if tool_runtime.entity.description: tool_runtime.entity.description.llm = ( - extra.get("descrption", "") or tool_runtime.entity.description.llm + extra.get("description", "") or tool_runtime.entity.description.llm ) for tool_runtime_params in tool_runtime.entity.parameters: tool_runtime_params.form = ( diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index 2592823540..42b6795fb0 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -7,7 +7,7 @@ from datetime import UTC, datetime from queue import Empty, Queue from typing import TYPE_CHECKING, Any, Optional, cast -from flask import Flask, current_app, has_request_context +from flask import Flask, current_app from configs import dify_config from core.variables import ArrayVariable, IntegerVariable, NoneVariable @@ -37,6 +37,7 @@ from core.workflow.nodes.base import BaseNode from core.workflow.nodes.enums import NodeType from core.workflow.nodes.event import NodeEvent, RunCompletedEvent from core.workflow.nodes.iteration.entities import ErrorHandleMode, IterationNodeData +from libs.flask_utils import preserve_flask_contexts from .exc import ( InvalidIteratorValueError, @@ -583,23 +584,8 @@ class IterationNode(BaseNode[IterationNodeData]): """ run single iteration in parallel mode """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user + with preserve_flask_contexts(flask_app, context_vars=context): parallel_mode_run_id = uuid.uuid4().hex graph_engine_copy = graph_engine.create_copy() variable_pool_copy = graph_engine_copy.graph_runtime_state.variable_pool diff --git a/api/libs/flask_utils.py b/api/libs/flask_utils.py new file mode 100644 index 0000000000..4ea2779584 --- /dev/null +++ b/api/libs/flask_utils.py @@ -0,0 +1,65 @@ +import contextvars +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TypeVar + +from flask import Flask, g, has_request_context + +T = TypeVar("T") + + +@contextmanager +def preserve_flask_contexts( + flask_app: Flask, + context_vars: contextvars.Context, +) -> Iterator[None]: + """ + A context manager that handles: + 1. flask-login's UserProxy copy + 2. ContextVars copy + 3. flask_app.app_context() + + This context manager ensures that the Flask application context is properly set up, + the current user is preserved across context boundaries, and any provided context variables + are set within the new context. + + Note: + This manager aims to allow use current_user cross thread and app context, + but it's not the recommend use, it's better to pass user directly in parameters. + + Args: + flask_app: The Flask application instance + context_vars: contextvars.Context object containing context variables to be set in the new context + + Yields: + None + + Example: + ```python + with preserve_flask_contexts(flask_app, context_vars=context_vars): + # Code that needs Flask app context and context variables + # Current user will be preserved if available + ``` + """ + # Set context variables if provided + if context_vars: + for var, val in context_vars.items(): + var.set(val) + + # Save current user before entering new app context + saved_user = None + if has_request_context() and hasattr(g, "_login_user"): + saved_user = g._login_user + + # Enter Flask app context + with flask_app.app_context(): + try: + # Restore user in new app context if it was saved + if saved_user is not None: + g._login_user = saved_user + + # Yield control back to the caller + yield + finally: + # Any cleanup can be added here if needed + pass diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 3ccd14415d..58a4b2f179 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from configs import dify_config from core.helper.position_helper import is_filtered from core.model_runtime.utils.encoders import jsonable_encoder -from core.plugin.entities.plugin import GenericProviderID, ToolProviderID +from core.plugin.entities.plugin import ToolProviderID from core.plugin.impl.exc import PluginDaemonClientSideError from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity @@ -290,7 +290,7 @@ class BuiltinToolManageService: def _fetch_builtin_provider(provider_name: str, tenant_id: str) -> BuiltinToolProvider | None: try: full_provider_name = provider_name - provider_id_entity = GenericProviderID(provider_name) + provider_id_entity = ToolProviderID(provider_name) provider_name = provider_id_entity.provider_name if provider_id_entity.organization != "langgenius": provider_obj = ( @@ -315,7 +315,7 @@ class BuiltinToolManageService: if provider_obj is None: return None - provider_obj.provider = GenericProviderID(provider_obj.provider).to_string() + provider_obj.provider = ToolProviderID(provider_obj.provider).to_string() return provider_obj except Exception: # it's an old provider without organization diff --git a/api/tests/unit_tests/libs/test_flask_utils.py b/api/tests/unit_tests/libs/test_flask_utils.py new file mode 100644 index 0000000000..fb46ba50f3 --- /dev/null +++ b/api/tests/unit_tests/libs/test_flask_utils.py @@ -0,0 +1,124 @@ +import contextvars +import threading +from typing import Optional + +import pytest +from flask import Flask +from flask_login import LoginManager, UserMixin, current_user, login_user + +from libs.flask_utils import preserve_flask_contexts + + +class User(UserMixin): + """Simple User class for testing.""" + + def __init__(self, id: str): + self.id = id + + def get_id(self) -> str: + return self.id + + +@pytest.fixture +def login_app(app: Flask) -> Flask: + """Set up a Flask app with flask-login.""" + # Set a secret key for the app + app.config["SECRET_KEY"] = "test-secret-key" + + login_manager = LoginManager() + login_manager.init_app(app) + + @login_manager.user_loader + def load_user(user_id: str) -> Optional[User]: + if user_id == "test_user": + return User("test_user") + return None + + return app + + +@pytest.fixture +def test_user() -> User: + """Create a test user.""" + return User("test_user") + + +def test_current_user_not_accessible_across_threads(login_app: Flask, test_user: User): + """ + Test that current_user is not accessible in a different thread without preserve_flask_contexts. + + This test demonstrates that without the preserve_flask_contexts, we cannot access + current_user in a different thread, even with app_context. + """ + # Log in the user in the main thread + with login_app.test_request_context(): + login_user(test_user) + assert current_user.is_authenticated + assert current_user.id == "test_user" + + # Store the result of the thread execution + result = {"user_accessible": True, "error": None} + + # Define a function to run in a separate thread + def check_user_in_thread(): + try: + # Try to access current_user in a different thread with app_context + with login_app.app_context(): + # This should fail because current_user is not accessible across threads + # without preserve_flask_contexts + result["user_accessible"] = current_user.is_authenticated + except Exception as e: + result["error"] = str(e) # type: ignore + + # Run the function in a separate thread + thread = threading.Thread(target=check_user_in_thread) + thread.start() + thread.join() + + # Verify that we got an error or current_user is not authenticated + assert result["error"] is not None or (result["user_accessible"] is not None and not result["user_accessible"]) + + +def test_current_user_accessible_with_preserve_flask_contexts(login_app: Flask, test_user: User): + """ + Test that current_user is accessible in a different thread with preserve_flask_contexts. + + This test demonstrates that with the preserve_flask_contexts, we can access + current_user in a different thread. + """ + # Log in the user in the main thread + with login_app.test_request_context(): + login_user(test_user) + assert current_user.is_authenticated + assert current_user.id == "test_user" + + # Save the context variables + context_vars = contextvars.copy_context() + + # Store the result of the thread execution + result = {"user_accessible": False, "user_id": None, "error": None} + + # Define a function to run in a separate thread + def check_user_in_thread_with_manager(): + try: + # Use preserve_flask_contexts to access current_user in a different thread + with preserve_flask_contexts(login_app, context_vars): + from flask_login import current_user + + if current_user: + result["user_accessible"] = True + result["user_id"] = current_user.id + else: + result["user_accessible"] = False + except Exception as e: + result["error"] = str(e) # type: ignore + + # Run the function in a separate thread + thread = threading.Thread(target=check_user_in_thread_with_manager) + thread.start() + thread.join() + + # Verify that current_user is accessible and has the correct ID + assert result["error"] is None + assert result["user_accessible"] is True + assert result["user_id"] == "test_user" diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 1462957a92..55e1b55599 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.2 + image: langgenius/dify-web:1.4.3 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 1f66017340..dddce106b9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -509,7 +509,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -538,7 +538,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.2 + image: langgenius/dify-api:1.4.3 restart: always environment: # Use the shared environment variables. @@ -564,7 +564,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.2 + image: langgenius/dify-web:1.4.3 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx index fc97f5e669..e0c09e739e 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx @@ -15,7 +15,7 @@ const Overview = async (props: IDevelopProps) => { } = params return ( -
+
{ + if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) + router.replace('/apps') + }, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, router]) + + if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) + return return ( diff --git a/web/app/(commonLayout)/datasets/template/template.en.mdx b/web/app/(commonLayout)/datasets/template/template.en.mdx index 4d00b7b2b5..e1ff827c96 100644 --- a/web/app/(commonLayout)/datasets/template/template.en.mdx +++ b/web/app/(commonLayout)/datasets/template/template.en.mdx @@ -54,7 +54,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi Index mode - - high_quality High quality: embedding using embedding model, built as vector database index + - high_quality High quality: Embedding using embedding model, built as vector database index - economy Economy: Build using inverted index of keyword table index diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index d121a93df2..3994356b51 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -55,7 +55,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi 索引方式 - high_quality 高质量:使用 - ding 模型进行嵌入,构建为向量数据库索引 + Embedding 模型进行嵌入,构建为向量数据库索引 - economy 经济:使用 keyword table index 的倒排索引进行构建 diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index 7de5d51edb..78b8835009 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -19,7 +19,7 @@ const Layout: FC<{ const [isLoading, setIsLoading] = useState(true) useEffect(() => { (async () => { - if (!systemFeatures.webapp_auth.enabled) { + if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { setIsLoading(false) return } @@ -37,7 +37,7 @@ const Layout: FC<{ setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) setIsLoading(false) })() - }, [pathname, redirectUrl, setWebAppAccessMode]) + }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) if (isLoading || isGlobalPending) { return
diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index ee4bd57325..3fd020f60f 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -20,6 +20,7 @@ import type { import { useToastContext } from '@/app/components/base/toast' import AppIcon from '@/app/components/base/app-icon' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const systemTypes = ['api'] type ExternalDataToolModalProps = { @@ -40,6 +41,7 @@ const ExternalDataToolModal: FC = ({ onValidateBeforeSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const { locale } = useContext(I18n) const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' }) @@ -243,7 +245,7 @@ const ExternalDataToolModal: FC = ({
{t('common.apiBasedExtension.selector.title')} diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index bfb7c43c0d..f0a0da41a5 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -314,7 +314,10 @@ function AppPreview({ mode }: { mode: AppMode }) { 'advanced-chat': { title: t('app.types.advanced'), description: t('app.newApp.advancedUserDescription'), - link: docLink('/guides/workflow/readme'), + link: docLink('/guides/workflow/README', { + 'zh-Hans': '/guides/workflow/readme', + 'ja-JP': '/guides/workflow/concepts', + }), }, 'agent-chat': { title: t('app.types.agent'), @@ -324,13 +327,18 @@ function AppPreview({ mode }: { mode: AppMode }) { 'completion': { title: t('app.newApp.completeApp'), description: t('app.newApp.completionUserDescription'), - link: docLink('/guides/application-orchestrate/text-generator', - { 'zh-Hans': '/guides/application-orchestrate/readme' }), + link: docLink('/guides/application-orchestrate/text-generator', { + 'zh-Hans': '/guides/application-orchestrate/readme', + 'ja-JP': '/guides/application-orchestrate/README', + }), }, 'workflow': { title: t('app.types.workflow'), description: t('app.newApp.workflowUserDescription'), - link: docLink('/guides/workflow/readme'), + link: docLink('/guides/workflow/README', { + 'zh-Hans': '/guides/workflow/readme', + 'ja-JP': '/guides/workflow/concepts', + }), }, } const previewInfo = modeToPreviewInfoMap[mode] diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index c2d98383c4..524c340a53 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -237,7 +237,9 @@ const SettingsModal: FC = ({
{t(`${prefixSettings}.modalTip`)} - {t('common.operation.learnMore')}
diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index ab5200f38f..3ede2d7c6b 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -25,6 +25,7 @@ import { useModalContext } from '@/context/modal-context' import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import cn from '@/utils/classnames' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const systemTypes = ['openai_moderation', 'keywords', 'api'] @@ -46,6 +47,7 @@ const ModerationSettingModal: FC = ({ onSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const { locale } = useContext(I18n) const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders) @@ -316,7 +318,7 @@ const ModerationSettingModal: FC = ({ ) diff --git a/web/app/components/datasets/external-api/external-api-modal/Form.tsx b/web/app/components/datasets/external-api/external-api-modal/Form.tsx index 7d244cce4f..8884cb787f 100644 --- a/web/app/components/datasets/external-api/external-api-modal/Form.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/Form.tsx @@ -59,7 +59,7 @@ const Form: FC = React.memo(({ {variable === 'endpoint' && ( = ({ onClose }) => {
{t('dataset.externalAPIPanelTitle')}
{t('dataset.externalAPIPanelDescription')}
+ href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} target='_blank'>
{t('dataset.externalAPIPanelDocumentation')}
diff --git a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx index 0b869be079..f65f395e30 100644 --- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx @@ -74,7 +74,10 @@ const ModifyRetrievalModal: FC = ({ {t('datasetSettings.form.retrievalSetting.learnMore')} diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index 4f68229dae..b90e65a85b 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -310,7 +310,16 @@ const Form = () => { diff --git a/web/app/components/header/account-dropdown/workplace-selector/index.tsx b/web/app/components/header/account-dropdown/workplace-selector/index.tsx index 98668c61d2..f384cbc0bc 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -31,22 +31,22 @@ const WorkplaceSelector = () => { } return ( - + { ({ open }) => ( <> -
+
{currentWorkspace?.name[0]?.toLocaleUpperCase()}
-
-
{currentWorkspace?.name}
- +
+
{currentWorkspace?.name}
+
{ leaveTo="transform opacity-0 scale-95" > @@ -73,7 +74,7 @@ const WorkplaceSelector = () => { { workspaces.map(workspace => (
handleSwitchWorkspace(workspace.id)}> -
+
{workspace?.name[0]?.toLocaleUpperCase()}
{workspace.name}
diff --git a/web/app/components/header/account-setting/api-based-extension-page/empty.tsx b/web/app/components/header/account-setting/api-based-extension-page/empty.tsx index a7c73917bb..c95e8d8d0c 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/empty.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/empty.tsx @@ -3,9 +3,11 @@ import { RiExternalLinkLine, RiPuzzle2Line, } from '@remixicon/react' +import { useDocLink } from '@/context/i18n' const Empty = () => { const { t } = useTranslation() + const docLink = useDocLink() return (
@@ -15,7 +17,7 @@ const Empty = () => {
{t('common.apiBasedExtension.title')}
{t('common.apiBasedExtension.link')} diff --git a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx index 53f7673b15..f6c0d93db0 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' @@ -29,6 +30,7 @@ const ApiBasedExtensionModal: FC = ({ onSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const [localeData, setLocaleData] = useState(data) const [loading, setLoading] = useState(false) const { notify } = useToastContext() @@ -100,7 +102,7 @@ const ApiBasedExtensionModal: FC = ({
{t('common.apiBasedExtension.modal.apiEndpoint.title')} diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx index 3b902811fc..4934ed3105 100644 --- a/web/app/components/header/app-nav/index.tsx +++ b/web/app/components/header/app-nav/index.tsx @@ -96,7 +96,7 @@ const AppNav = () => { link, } }) - setNavItems(navItems) + setNavItems(navItems as any) } }, [appsData, isCurrentWorkspaceEditor, setNavItems]) @@ -122,7 +122,7 @@ const AppNav = () => { text={t('common.menus.apps')} activeSegment={['apps', 'app']} link='/apps' - curNav={appDetail} + curNav={appDetail as any} navs={navItems} createText={t('common.menus.newApp')} onCreate={openModal} diff --git a/web/app/components/header/dataset-nav/index.tsx b/web/app/components/header/dataset-nav/index.tsx index 4894a62484..85223f9f37 100644 --- a/web/app/components/header/dataset-nav/index.tsx +++ b/web/app/components/header/dataset-nav/index.tsx @@ -48,7 +48,7 @@ const DatasetNav = () => { text={t('common.menus.datasets')} activeSegment='datasets' link='/datasets' - curNav={currentDataset as Omit} + curNav={currentDataset as any} navs={datasetItems.map(dataset => ({ id: dataset.id, name: dataset.name, @@ -59,6 +59,7 @@ const DatasetNav = () => { createText={t('common.menus.newDataset')} onCreate={() => router.push(`${basePath}/datasets/create`)} onLoadmore={handleLoadmore} + isApp={false} /> ) } diff --git a/web/app/components/header/env-nav/index.tsx b/web/app/components/header/env-nav/index.tsx index cec933a4c5..3f0b0f01dd 100644 --- a/web/app/components/header/env-nav/index.tsx +++ b/web/app/components/header/env-nav/index.tsx @@ -20,22 +20,22 @@ const EnvNav = () => { return (
{ langeniusVersionInfo.current_env === 'TESTING' && ( <> - - {t('common.environment.testing')} + +
{t('common.environment.testing')}
) } { langeniusVersionInfo.current_env === 'DEVELOPMENT' && ( <> - - {t('common.environment.development')} + +
{t('common.environment.development')}
) } diff --git a/web/app/components/header/explore-nav/index.tsx b/web/app/components/header/explore-nav/index.tsx index b6ebf5d1a9..6896722a84 100644 --- a/web/app/components/header/explore-nav/index.tsx +++ b/web/app/components/header/explore-nav/index.tsx @@ -27,10 +27,12 @@ const ExploreNav = ({ )}> { activated - ? - : + ? + : } - {t('common.menus.explore')} +
+ {t('common.menus.explore')} +
) } diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index a9c26e0070..48973e50a8 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -1,9 +1,6 @@ 'use client' -import { useCallback, useEffect } from 'react' +import { useCallback } from 'react' import Link from 'next/link' -import { useBoolean } from 'ahooks' -import { useSelectedLayoutSegment } from 'next/navigation' -import { Bars3Icon } from '@heroicons/react/20/solid' import AccountDropdown from './account-dropdown' import AppNav from './app-nav' import DatasetNav from './dataset-nav' @@ -24,17 +21,15 @@ import { Plan } from '../billing/type' import { useGlobalPublicStore } from '@/context/global-public-context' const navClassName = ` - flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl + flex items-center relative px-3 h-8 rounded-xl font-medium text-sm cursor-pointer ` const Header = () => { const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() - const selectedSegment = useSelectedLayoutSegment() const media = useBreakpoints() const isMobile = media === MediaType.mobile - const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false) const { enableBilling, plan } = useProviderContext() const { setShowPricingModal, setShowAccountSettingModal } = useModalContext() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -46,23 +41,12 @@ const Header = () => { setShowAccountSettingModal({ payload: 'billing' }) }, [isFreePlan, setShowAccountSettingModal, setShowPricingModal]) - useEffect(() => { - hideNavMenu() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedSegment]) - return ( -
-
- {isMobile &&
- -
} - { - !isMobile - &&
- + if (isMobile) { + return ( +
+
+
+ {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo ? { /> : } -
/
-
- - - - {enableBilling ? : } +
/
+ + + + {enableBilling ? : } +
+
+
+
+
- } -
- {isMobile && ( -
- - {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo - ? logo - : } - -
/
- {enableBilling ? : } -
- )} - { - !isMobile && ( -
- {!isCurrentWorkspaceDatasetOperator && } - {!isCurrentWorkspaceDatasetOperator && } - {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } - {!isCurrentWorkspaceDatasetOperator && } -
- ) - } -
+
+
+ {!isCurrentWorkspaceDatasetOperator && } + {!isCurrentWorkspaceDatasetOperator && } + {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } + {!isCurrentWorkspaceDatasetOperator && } +
+
+ ) + } + + return ( +
+
+ + {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : } + +
/
+ + + + {enableBilling ? : } +
+
+ {!isCurrentWorkspaceDatasetOperator && } + {!isCurrentWorkspaceDatasetOperator && } + {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } + {!isCurrentWorkspaceDatasetOperator && } +
+
- { - (isMobile && isShowNavMenu) && ( -
- {!isCurrentWorkspaceDatasetOperator && } - {!isCurrentWorkspaceDatasetOperator && } - {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } - {!isCurrentWorkspaceDatasetOperator && } -
- ) - } -
+
) } export default Header diff --git a/web/app/components/header/nav/index.tsx b/web/app/components/header/nav/index.tsx index 293c66a7b0..dc68b712cb 100644 --- a/web/app/components/header/nav/index.tsx +++ b/web/app/components/header/nav/index.tsx @@ -46,7 +46,7 @@ const Nav = ({ return (
@@ -61,7 +61,7 @@ const Nav = ({ onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} > -
+
{ (hovered && curNav) ? @@ -70,7 +70,9 @@ const Nav = ({ : icon }
- {text} +
+ {text} +
{ diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index 473abf03c1..77cf348da2 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -53,136 +53,134 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: }, 50), []) return ( -
- - {({ open }) => ( - <> - -
{curNav?.name}
-
- -
- { - navs.map(nav => ( - -
{ - if (curNav?.id === nav.id) - return - setAppDetail() - router.push(nav.link) - }} title={nav.name}> -
- - {!!nav.mode && ( - - {nav.mode === 'advanced-chat' && ( - - )} - {nav.mode === 'agent-chat' && ( - - )} - {nav.mode === 'chat' && ( - - )} - {nav.mode === 'completion' && ( - - )} - {nav.mode === 'workflow' && ( - - )} - - )} -
-
- {nav.name} -
+ + {({ open }) => ( + <> + +
{curNav?.name}
+
+ +
+ { + navs.map(nav => ( + +
{ + if (curNav?.id === nav.id) + return + setAppDetail() + router.push(nav.link) + }} title={nav.name}> +
+ + {!!nav.mode && ( + + {nav.mode === 'advanced-chat' && ( + + )} + {nav.mode === 'agent-chat' && ( + + )} + {nav.mode === 'chat' && ( + + )} + {nav.mode === 'completion' && ( + + )} + {nav.mode === 'workflow' && ( + + )} + + )} +
+
+ {nav.name}
- - )) - } -
- {!isApp && isCurrentWorkspaceEditor && ( - -
onCreate('')} className={cn( - 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover ', - )}> -
-
-
{createText}
+ + )) + } +
+ {!isApp && isCurrentWorkspaceEditor && ( + +
onCreate('')} className={cn( + 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover ', + )}> +
+
- - )} - {isApp && isCurrentWorkspaceEditor && ( - - {({ open }) => ( - <> - -
-
- -
-
{createText}
- +
{createText}
+
+ + )} + {isApp && isCurrentWorkspaceEditor && ( + + {({ open }) => ( + <> + +
+
+
- - - -
-
onCreate('blank')}> - - {t('app.newApp.startFromBlank')} -
-
onCreate('template')}> - - {t('app.newApp.startFromTemplate')} -
+
{createText}
+ +
+ + + +
+
onCreate('blank')}> + + {t('app.newApp.startFromBlank')}
-
-
onCreate('dsl')}> - - {t('app.importDSL')} -
+
onCreate('template')}> + + {t('app.newApp.startFromTemplate')}
- - - - )} -
- )} - - - )} -
-
+
+
+
onCreate('dsl')}> + + {t('app.importDSL')} +
+
+
+ + + )} +
+ )} + + + )} +
) } diff --git a/web/app/components/header/tools-nav/index.tsx b/web/app/components/header/tools-nav/index.tsx index b87398708c..bc9351cf70 100644 --- a/web/app/components/header/tools-nav/index.tsx +++ b/web/app/components/header/tools-nav/index.tsx @@ -28,10 +28,12 @@ const ToolsNav = ({ )}> { activated - ? - : + ? + : } - {t('common.menus.tools')} +
+ {t('common.menus.tools')} +
) } diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx index 24e1e39ff5..c77d0d0a70 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx @@ -83,6 +83,7 @@ const SelectPackage: React.FC = ({ installedValue={updatePayload?.originalPackageInfo.version} placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''} popupClassName='w-[512px] z-[1001]' + triggerClassName='text-components-input-text-filled' />
- +
{t('tools.test.testResult')}
diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 8b1003e89c..0d0ba80fe1 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -61,7 +61,7 @@ export const useShortcuts = (): void => { return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) }, [workflowStore]) - useKeyPress(['delete', 'backspace'], (e) => { + useKeyPress(['delete'], (e) => { if (shouldHandleShortcut(e)) { e.preventDefault() handleNodesDelete() diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 912d0b5853..4ca8746137 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -118,6 +118,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { title={<> {renderI18nObject(def.label)} {def.required && *} } + key={def.variable} tooltip={def.tooltip && renderI18nObject(def.tooltip)} inline > @@ -222,7 +223,8 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { description={
{t('workflow.nodes.agent.strategy.configureTipDesc')}
{t('workflow.nodes.agent.learnMore')} diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 1e195c5d40..3540c60a39 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -140,6 +140,7 @@ const CodeEditor: FC = ({ language={languageMap[language] || 'javascript'} theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme value={outPutValue} + loading={Loading...} onChange={handleEditorChange} // https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html options={{ diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx index 6bfb7755dc..f9292be477 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx @@ -36,7 +36,9 @@ const DefaultValue = ({ {t('workflow.nodes.common.errorHandle.defaultValue.desc')}   diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index 72e9384d5f..181b278051 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -8,6 +8,8 @@ import VarReferencePicker from './var-reference-picker' import Input from '@/app/components/base/input' import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { checkKeys } from '@/utils/var' +import Toast from '@/app/components/base/toast' type Props = { nodeId: string @@ -36,9 +38,27 @@ const VarList: FC = ({ const handleVarNameChange = useCallback((index: number) => { return (e: React.ChangeEvent) => { - onVarNameChange?.(list[index].variable, e.target.value) + const newKey = e.target.value + const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) + if (!isValid) { + Toast.notify({ + type: 'error', + message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + }) + return + } + + if (list.map(item => item.variable?.trim()).includes(newKey.trim())) { + Toast.notify({ + type: 'error', + message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), + }) + return + } + + onVarNameChange?.(list[index].variable, newKey) const newList = produce(list, (draft) => { - draft[index].variable = e.target.value + draft[index].variable = newKey }) onChange(newList) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index d51e293a04..9398ae7361 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -44,8 +44,11 @@ const VarReferencePopup: FC = ({ description={} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 9acbdf4ff7..27063a2ba3 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -260,6 +260,7 @@ type Props = { maxHeightClass?: string onClose?: () => void onBlur?: () => void + autoFocus?: boolean } const VarReferenceVars: FC = ({ hideSearch, @@ -271,6 +272,7 @@ const VarReferenceVars: FC = ({ maxHeightClass, onClose, onBlur, + autoFocus = true, }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') @@ -323,7 +325,7 @@ const VarReferenceVars: FC = ({ onKeyDown={handleKeyDown} onClear={() => setSearchText('')} onBlur={onBlur} - autoFocus + autoFocus={autoFocus} />
> = (props) => { const field = param.name const value = inputs.agent_parameters?.[field]?.value if (value) { - (value as unknown as any[]).forEach((item) => { + (value as unknown as any[]).forEach((item, idx) => { tools.push({ - id: `${param.name}-${i}`, + id: `${param.name}-${idx}`, providerName: item.provider_name, }) }) diff --git a/web/app/components/workflow/panel/chat-variable-panel/index.tsx b/web/app/components/workflow/panel/chat-variable-panel/index.tsx index be9ef36a6b..bbf39489dd 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/index.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/index.tsx @@ -138,10 +138,13 @@ const ChatVariablePanel = () => { +
@@ -167,7 +170,7 @@ const ChatVariablePanel = () => {
-
+
)} diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index 4546aabae6..c842554948 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -139,7 +139,7 @@ const VariableModal = ({
{ type !== 'number' ?