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/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/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 363b2ee920..875cee17e6 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/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/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/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) + if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) router.replace('/apps') - }, [isCurrentWorkspaceEditor, router]) + }, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, router]) - if (!isCurrentWorkspaceEditor) + if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) return return ( 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/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/list/template/template.en.mdx b/web/app/components/datasets/list/template/template.en.mdx index 4d00b7b2b5..e1ff827c96 100644 --- a/web/app/components/datasets/list/template/template.en.mdx +++ b/web/app/components/datasets/list/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/components/datasets/list/template/template.zh.mdx b/web/app/components/datasets/list/template/template.zh.mdx index d121a93df2..3994356b51 100644 --- a/web/app/components/datasets/list/template/template.zh.mdx +++ b/web/app/components/datasets/list/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/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 fb794f6b56..b5ed5bb6d1 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]) 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 edb4ee45c2..6500445fdf 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 0ebdff8576..1093bc4ae1 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, navigationItems, createText, isApp, onCreate, onL }, 50), []) return ( -
- - {({ open }) => ( - <> - -
{curNav?.name}
-
- -
- { - navigationItems.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}
+
+ +
+ { + navigationItems.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' />