Merge branch 'langgenius:main' into add-turbo-pack

pull/20696/head
GuanMu 1 year ago committed by GitHub
commit 803de7ed51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,28 @@
name: Deploy RAG Dev
permissions:
contents: read
on:
workflow_run:
workflows: ["Build and Push API & Web"]
branches:
- "deploy/rag-dev"
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/rag-dev'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
with:
host: ${{ secrets.RAG_SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}

@ -10,6 +10,7 @@ yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-com
yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml
yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml
yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml
yq eval '.services.oceanbase.ports += ["2881:2881"]' -i docker/docker-compose.yaml
yq eval '.services.opengauss.ports += ["6600:6600"]' -i docker/docker-compose.yaml
echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase, opengauss"

@ -31,6 +31,13 @@ jobs:
with:
persist-credentials: false
- name: Free Disk Space
uses: endersonmenezes/free-disk-space@v2
with:
remove_dotnet: true
remove_haskell: true
remove_tool_cache: true
- name: Setup UV and Python
uses: ./.github/actions/setup-uv
with:
@ -59,7 +66,7 @@ jobs:
tidb
tiflash
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase)
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase)
uses: hoverkraft-tech/compose-action@v2.0.2
with:
compose-file: |
@ -75,9 +82,12 @@ jobs:
pgvector
chroma
elasticsearch
oceanbase
- name: Check TiDB Ready
run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
- name: Check VDB Ready (TiDB, Oceanbase)
run: |
uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
uv run --project api python api/tests/integration_tests/vdb/oceanbase/check_oceanbase_ready.py
- name: Test Vector Stores
run: uv run --project api bash dev/pytest/pytest_vdb.sh

3
.gitignore vendored

@ -210,3 +210,6 @@ mise.toml
# Next.js build output
.next/
# AI Assistant
.roo/

@ -27,7 +27,7 @@ from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, D
from models.dataset import Document as DatasetDocument
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
from models.provider import Provider, ProviderModel
from services.account_service import RegisterService, TenantService
from services.account_service import AccountService, RegisterService, TenantService
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
from services.plugin.data_migration import PluginDataMigration
from services.plugin.plugin_migration import PluginMigration
@ -68,6 +68,7 @@ def reset_password(email, new_password, password_confirm):
account.password = base64_password_hashed
account.password_salt = base64_salt
db.session.commit()
AccountService.reset_login_error_rate_limit(email)
click.echo(click.style("Password reset successfully.", fg="green"))

@ -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(

@ -47,7 +47,13 @@ class AppInfoApi(Resource):
def get(self, app_model: App):
"""Get app information"""
tags = [tag.name for tag in app_model.tags]
return {"name": app_model.name, "description": app_model.description, "tags": tags, "mode": app_model.mode}
return {
"name": app_model.name,
"description": app_model.description,
"tags": tags,
"mode": app_model.mode,
"author_name": app_model.author_name,
}
api.add_resource(AppParameterApi, "/parameters")

@ -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,

@ -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)

@ -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)

@ -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,

@ -80,6 +80,23 @@ class OceanBaseVector(BaseVector):
self.delete()
vals = []
params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'")
for row in params:
val = int(row[6])
vals.append(val)
if len(vals) == 0:
raise ValueError("ob_vector_memory_limit_percentage not found in parameters.")
if any(val == 0 for val in vals):
try:
self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30")
except Exception as e:
raise Exception(
"Failed to set ob_vector_memory_limit_percentage. "
+ "Maybe the database user has insufficient privilege.",
e,
)
cols = [
Column("id", String(36), primary_key=True, autoincrement=False),
Column("vector", VECTOR(self._vec_dim)),
@ -110,22 +127,6 @@ class OceanBaseVector(BaseVector):
+ "to support fulltext index and vector index in the same table",
e,
)
vals = []
params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'")
for row in params:
val = int(row[6])
vals.append(val)
if len(vals) == 0:
raise ValueError("ob_vector_memory_limit_percentage not found in parameters.")
if any(val == 0 for val in vals):
try:
self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30")
except Exception as e:
raise Exception(
"Failed to set ob_vector_memory_limit_percentage. "
+ "Maybe the database user has insufficient privilege.",
e,
)
redis_client.set(collection_exist_cache_key, 1, ex=3600)
def _check_hybrid_search_support(self) -> bool:

@ -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

@ -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,

@ -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 = (

@ -6,7 +6,6 @@ from pydantic import BaseModel, Field
from core.model_runtime.entities.llm_entities import LLMUsage
from core.rag.entities.citation_metadata import RetrievalSourceMetadata
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
class RunCompletedEvent(BaseModel):
@ -39,11 +38,3 @@ class RunRetryEvent(BaseModel):
error: str = Field(..., description="error")
retry_index: int = Field(..., description="Retry attempt number")
start_at: datetime = Field(..., description="Retry start time")
class SingleStepRetryEvent(NodeRunResult):
"""Single step retry event"""
status: WorkflowNodeExecutionStatus = WorkflowNodeExecutionStatus.RETRY
elapsed_time: float = Field(..., description="elapsed time")

@ -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

@ -39,10 +39,6 @@ from core.variables.variables import (
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID
class InvalidSelectorError(ValueError):
pass
class UnsupportedSegmentTypeError(Exception):
pass

@ -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

@ -4,7 +4,6 @@ from . import (
app_model_config,
audio,
base,
completion,
conversation,
dataset,
document,
@ -19,7 +18,6 @@ __all__ = [
"app_model_config",
"audio",
"base",
"completion",
"conversation",
"dataset",
"document",

@ -55,7 +55,3 @@ class MemberNotInTenantError(BaseServiceError):
class RoleAlreadyAssignedError(BaseServiceError):
pass
class RateLimitExceededError(BaseServiceError):
pass

@ -1,5 +0,0 @@
from services.errors.base import BaseServiceError
class CompletionStoppedError(BaseServiceError):
pass

@ -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

@ -30,11 +30,11 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str]):
logging.info(click.style("Dataset not found: {}".format(dataset_id), fg="red"))
db.session.close()
return
tenant_id = dataset.tenant_id
for document_id in document_ids:
retry_indexing_cache_key = "document_{}_is_retried".format(document_id)
# check document limit
features = FeatureService.get_features(dataset.tenant_id)
features = FeatureService.get_features(tenant_id)
try:
if features.billing.enabled:
vector_space = features.vector_space

@ -0,0 +1,49 @@
import time
import pymysql
def check_oceanbase_ready() -> bool:
try:
connection = pymysql.connect(
host="localhost",
port=2881,
user="root",
password="difyai123456",
)
affected_rows = connection.query("SELECT 1")
return affected_rows == 1
except Exception as e:
print(f"Oceanbase is not ready. Exception: {e}")
return False
finally:
if connection:
connection.close()
def main():
max_attempts = 50
retry_interval_seconds = 2
is_oceanbase_ready = False
for attempt in range(max_attempts):
try:
is_oceanbase_ready = check_oceanbase_ready()
except Exception as e:
print(f"Oceanbase is not ready. Exception: {e}")
is_oceanbase_ready = False
if is_oceanbase_ready:
break
else:
print(f"Attempt {attempt + 1} failed, retry in {retry_interval_seconds} seconds...")
time.sleep(retry_interval_seconds)
if is_oceanbase_ready:
print("Oceanbase is ready.")
else:
print(f"Oceanbase is not ready after {max_attempts} attempting checks.")
exit(1)
if __name__ == "__main__":
main()

@ -1,15 +1,11 @@
from unittest.mock import MagicMock, patch
import pytest
from core.rag.datasource.vdb.oceanbase.oceanbase_vector import (
OceanBaseVector,
OceanBaseVectorConfig,
)
from tests.integration_tests.vdb.__mock.tcvectordb import setup_tcvectordb_mock
from tests.integration_tests.vdb.test_vector_store import (
AbstractVectorTest,
get_example_text,
setup_mock_redis,
)
@ -20,10 +16,11 @@ def oceanbase_vector():
"dify_test_collection",
config=OceanBaseVectorConfig(
host="127.0.0.1",
port="2881",
user="root@test",
port=2881,
user="root",
database="test",
password="test",
password="difyai123456",
enable_hybrid_search=True,
),
)
@ -33,39 +30,13 @@ class OceanBaseVectorTest(AbstractVectorTest):
super().__init__()
self.vector = vector
def search_by_vector(self):
hits_by_vector = self.vector.search_by_vector(query_vector=self.example_embedding)
assert len(hits_by_vector) == 0
def search_by_full_text(self):
hits_by_full_text = self.vector.search_by_full_text(query=get_example_text())
assert len(hits_by_full_text) == 0
def text_exists(self):
exist = self.vector.text_exists(self.example_doc_id)
assert exist == True
def get_ids_by_metadata_field(self):
ids = self.vector.get_ids_by_metadata_field(key="document_id", value=self.example_doc_id)
assert len(ids) == 0
@pytest.fixture
def setup_mock_oceanbase_client():
with patch("core.rag.datasource.vdb.oceanbase.oceanbase_vector.ObVecClient", new_callable=MagicMock) as mock_client:
yield mock_client
@pytest.fixture
def setup_mock_oceanbase_vector(oceanbase_vector):
with patch.object(oceanbase_vector, "_client"):
yield oceanbase_vector
assert len(ids) == 1
def test_oceanbase_vector(
setup_mock_redis,
setup_mock_oceanbase_client,
setup_mock_oceanbase_vector,
oceanbase_vector,
):
OceanBaseVectorTest(oceanbase_vector).run_all_tests()

@ -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"

@ -1067,6 +1067,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets
# Plugin oss bucket
PLUGIN_STORAGE_OSS_BUCKET=
# Plugin oss s3 credentials
PLUGIN_S3_USE_AWS=
PLUGIN_S3_USE_AWS_MANAGED_IAM=false
PLUGIN_S3_ENDPOINT=
PLUGIN_S3_USE_PATH_STYLE=false

@ -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:-}
@ -168,6 +168,7 @@ services:
PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets}
PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-}
S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false}
S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-}
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
@ -434,7 +435,7 @@ services:
# OceanBase vector database
oceanbase:
image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818
image: oceanbase/oceanbase-ce:4.3.5-lts
container_name: oceanbase
profiles:
- oceanbase
@ -449,9 +450,7 @@ services:
OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456}
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OB_SERVER_IP: 127.0.0.1
MODE: MINI
ports:
- "${OCEANBASE_VECTOR_PORT:-2881}:2881"
MODE: mini
# Oracle vector database
oracle:

@ -104,6 +104,7 @@ services:
PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages}
PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets}
PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-}
S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-}
S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false}
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}

@ -467,6 +467,7 @@ x-shared-env: &shared-api-worker-env
PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages}
PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets}
PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-}
PLUGIN_S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-}
PLUGIN_S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false}
PLUGIN_S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
PLUGIN_S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
@ -508,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.
@ -537,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.
@ -563,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:-}
@ -674,6 +675,7 @@ services:
PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets}
PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-}
S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false}
S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-}
S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-}
S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false}
AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-}
@ -940,7 +942,7 @@ services:
# OceanBase vector database
oceanbase:
image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818
image: oceanbase/oceanbase-ce:4.3.5-lts
container_name: oceanbase
profiles:
- oceanbase
@ -955,9 +957,7 @@ services:
OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456}
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OB_SERVER_IP: 127.0.0.1
MODE: MINI
ports:
- "${OCEANBASE_VECTOR_PORT:-2881}:2881"
MODE: mini
# Oracle vector database
oracle:

@ -133,6 +133,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets
PLUGIN_STORAGE_OSS_BUCKET=
# Plugin oss s3 credentials
PLUGIN_S3_USE_AWS_MANAGED_IAM=false
PLUGIN_S3_USE_AWS=
PLUGIN_S3_ENDPOINT=
PLUGIN_S3_USE_PATH_STYLE=false
PLUGIN_AWS_ACCESS_KEY=

@ -15,7 +15,7 @@ const Overview = async (props: IDevelopProps) => {
} = params
return (
<div className="h-full overflow-scroll bg-chatbot-bg px-4 py-6 sm:px-12">
<div className="h-full overflow-y-auto bg-chatbot-bg px-4 py-6 sm:px-12">
<ApikeyInfoPanel />
<ChartView
appId={appId}

@ -25,9 +25,8 @@ import Loading from '@/app/components/base/loading'
import DatasetDetailContext from '@/context/dataset-detail'
import { DataSourceType } from '@/models/datasets'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { LanguagesSupported } from '@/i18n/language'
import { useStore } from '@/app/components/app/store'
import { getLocaleOnClient } from '@/i18n'
import { useDocLink } from '@/context/i18n'
import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
@ -45,9 +44,9 @@ type IExtraInfoProps = {
}
const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
const locale = getLocaleOnClient()
const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
const { t } = useTranslation()
const docLink = useDocLink()
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
const relatedAppsTotal = relatedApps?.data?.length || 0
@ -97,11 +96,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
<div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div>
<a
className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent'
href={
locale === LanguagesSupported[1]
? 'https://docs.dify.ai/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
}
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
target='_blank' rel='noopener noreferrer'
>
<RiBookOpenLine className='mr-1 text-text-accent' />

@ -1,9 +1,23 @@
'use client'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { ExternalApiPanelProvider } from '@/context/external-api-panel-context'
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const router = useRouter()
useEffect(() => {
if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator)
router.replace('/apps')
}, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, router])
if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator)
return <Loading type='app' />
return (
<ExternalKnowledgeApiProvider>
<ExternalApiPanelProvider>

@ -54,7 +54,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</Property>
<Property name='indexing_technique' type='string' key='indexing_technique'>
Index mode
- <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
- <code>high_quality</code> High quality: Embedding using embedding model, built as vector database index
- <code>economy</code> Economy: Build using inverted index of keyword table index
</Property>
<Property name='doc_form' type='string' key='doc_form'>

@ -55,7 +55,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='indexing_technique' type='string' key='indexing_technique'>
索引方式
- <code>high_quality</code> 高质量:使用
ding 模型进行嵌入,构建为向量数据库索引
Embedding 模型进行嵌入,构建为向量数据库索引
- <code>economy</code> 经济:使用 keyword table index 的倒排索引进行构建
</Property>
<Property name='doc_form' type='string' key='doc_form'>

@ -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 <div className='flex h-full w-full items-center justify-center'>
<Loading />

@ -1,13 +1,11 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import { useDocLink } from '@/context/i18n'
type Props = {
showWarning: boolean
@ -19,7 +17,7 @@ const HistoryPanel: FC<Props> = ({
onShowEditModal,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
return (
<Panel
@ -45,9 +43,8 @@ const HistoryPanel: FC<Props> = ({
{showWarning && (
<div className='flex justify-between rounded-b-xl bg-background-section-burn px-3 py-2 text-xs text-text-secondary'>
<div>{t('appDebug.feature.conversationHistory.tip')}
<a href={`${locale === LanguagesSupported[1]
? 'https://docs.dify.ai/zh-hans/learn-more/extended-reading/prompt-engineering/README'
: 'https://docs.dify.ai/en/features/prompt-engineering'}`}
<a href={docLink('/learn-more/extended-reading/what-is-llmops',
{ 'zh-Hans': '/learn-more/extended-reading/prompt-engineering/README' })}
target='_blank' rel='noopener noreferrer'
className='text-[#155EEF]'>{t('appDebug.feature.conversationHistory.learnMore')}
</a>

@ -31,6 +31,7 @@ import {
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
import { useDocLink } from '@/context/i18n'
type SettingsModalProps = {
currentDataset: DataSet
@ -58,6 +59,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
currentModel: isRerankDefaultModelValid,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const { t } = useTranslation()
const docLink = useDocLink()
const { notify } = useToastContext()
const ref = useRef(null)
const isExternal = currentDataset.provider === 'external'
@ -328,7 +330,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
<div>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
<a target='_blank' rel='noopener noreferrer' href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting')} className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
{t('datasetSettings.form.retrievalSetting.description')}
</div>
</div>

@ -2,9 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import { useDocLink } from '@/context/i18n'
type Props = {
onReturnToSimpleMode: () => void
}
@ -13,7 +11,7 @@ const AdvancedModeWarning: FC<Props> = ({
onReturnToSimpleMode,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const [show, setShow] = React.useState(true)
if (!show)
return null
@ -25,7 +23,7 @@ const AdvancedModeWarning: FC<Props> = ({
<span className='text-gray-700'>{t('appDebug.promptMode.advancedWarning.description')}</span>
<a
className='font-medium text-[#155EEF]'
href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? '/guides/features/prompt-engineering' : 'features/prompt-engineering'}`}
href={docLink('/guides/features/prompt-engineering')}
target='_blank' rel='noopener noreferrer'
>
{t('appDebug.promptMode.advancedWarning.learnMore')}

@ -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<ExternalDataToolModalProps> = ({
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<ExternalDataToolModalProps> = ({
<div className='flex h-9 items-center justify-between text-sm font-medium text-gray-900'>
{t('common.apiBasedExtension.selector.title')}
<a
href={t('common.apiBasedExtension.linkUrl') || '/'}
href={docLink('/guides/extension/api-based-extension/README')}
target='_blank' rel='noopener noreferrer'
className='group flex items-center text-xs font-normal text-gray-500 hover:text-primary-600'
>

@ -29,6 +29,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import FullScreenModal from '@/app/components/base/fullscreen-modal'
import useTheme from '@/hooks/use-theme'
import { useDocLink } from '@/context/i18n'
type CreateAppProps = {
onSuccess: () => void
@ -303,31 +304,41 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
function AppPreview({ mode }: { mode: AppMode }) {
const { t } = useTranslation()
const docLink = useDocLink()
const modeToPreviewInfoMap = {
'chat': {
title: t('app.types.chatbot'),
description: t('app.newApp.chatbotUserDescription'),
link: 'https://docs.dify.ai/guides/application-orchestrate/readme',
link: docLink('/guides/application-orchestrate/chatbot-application'),
},
'advanced-chat': {
title: t('app.types.advanced'),
description: t('app.newApp.advancedUserDescription'),
link: 'https://docs.dify.ai/en/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'),
description: t('app.newApp.agentUserDescription'),
link: 'https://docs.dify.ai/en/guides/application-orchestrate/agent',
link: docLink('/guides/application-orchestrate/agent'),
},
'completion': {
title: t('app.newApp.completeApp'),
description: t('app.newApp.completionUserDescription'),
link: null,
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: 'https://docs.dify.ai/en/guides/workflow/README',
link: docLink('/guides/workflow/README', {
'zh-Hans': '/guides/workflow/readme',
'ja-JP': '/guides/workflow/concepts',
}),
},
}
const previewInfo = modeToPreviewInfoMap[mode]

@ -354,7 +354,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
}
useEffect(() => {
adjustModalWidth()
const raf = requestAnimationFrame(adjustModalWidth)
return () => cancelAnimationFrame(raf)
}, [])
return (

@ -3,13 +3,11 @@ import type { FC } from 'react'
import React from 'react'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useDocLink } from '@/context/i18n'
import type { AppMode } from '@/types/app'
import I18n from '@/context/i18n'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import Tag from '@/app/components/base/tag'
import { LanguagesSupported } from '@/i18n/language'
type IShareLinkProps = {
isShow: boolean
@ -43,7 +41,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
mode,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const isChatApp = mode === 'chat' || mode === 'advanced-chat'
return <Modal
@ -101,10 +99,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
className='mt-2'
onClick={() =>
window.open(
`https://docs.dify.ai/${locale !== LanguagesSupported[1]
? 'user-guide/launching-dify-apps/developing-with-apis'
: `${locale.toLowerCase()}/guides/application-publishing/developing-with-apis`
}`,
docLink('/guides/application-publishing/developing-with-apis'),
'_blank',
)
}

@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react'
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
import Link from 'next/link'
import { Trans, useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import Modal from '@/app/components/base/modal'
import ActionButton from '@/app/components/base/action-button'
@ -19,14 +18,14 @@ import { SimpleSelect } from '@/app/components/base/select'
import type { AppDetailResponse } from '@/models/app'
import type { AppIconType, AppSSO, Language } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast'
import { LanguagesSupported, languages } from '@/i18n/language'
import { languages } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
import { useDocLink } from '@/context/i18n'
export type ISettingsModalProps = {
isChat: boolean
@ -98,7 +97,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
const [language, setLanguage] = useState(default_language)
const [saveLoading, setSaveLoading] = useState(false)
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [appIcon, setAppIcon] = useState<AppIconSelection>(
@ -238,7 +237,10 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
<div className='system-xs-regular mt-0.5 text-text-tertiary'>
<span>{t(`${prefixSettings}.modalTip`)}</span>
<Link href={`${locale === LanguagesSupported[1] ? 'https://docs.dify.ai/zh-hans/guides/application-publishing/launch-your-webapp-quickly#she-zhi-ni-de-ai-zhan-dian' : 'https://docs.dify.ai/en/guides/application-publishing/launch-your-webapp-quickly/README'}`} target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link>
<Link href={docLink('/guides/application-publishing/launch-your-webapp-quickly/README', {
'zh-Hans': '/guides/application-publishing/launch-your-webapp-quickly/readme',
})}
target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link>
</div>
</div>
{/* form body */}

@ -1,6 +1,5 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { RiCloseLine, RiInformation2Fill } from '@remixicon/react'
import DialogWrapper from '@/app/components/base/features/new-feature-panel/dialog-wrapper'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
@ -19,8 +18,7 @@ import Moderation from '@/app/components/base/features/new-feature-panel/moderat
import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply'
import type { PromptVariable } from '@/models/debug'
import type { InputVar } from '@/app/components/workflow/types'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import { useDocLink } from '@/context/i18n'
type Props = {
show: boolean
@ -48,7 +46,7 @@ const NewFeaturePanel = ({
onAutoAddPromptVariable,
}: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
@ -80,7 +78,7 @@ const NewFeaturePanel = ({
<span>{isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')}</span>
<a
className='text-text-accent'
href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}guides/workflow/bulletin`}
href={docLink('/guides/workflow/bulletin')}
target='_blank' rel='noopener noreferrer'
>{t('workflow.common.featuresDocLink')}</a>
</div>

@ -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<ModerationSettingModalProps> = ({
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<ModerationSettingModalProps> = ({
<div className='flex h-9 items-center justify-between'>
<div className='text-sm font-medium text-text-primary'>{t('common.apiBasedExtension.selector.title')}</div>
<a
href={t('common.apiBasedExtension.linkUrl') || '/'}
href={docLink('/guides/extension/api-based-extension/README')}
target='_blank' rel='noopener noreferrer'
className='group flex items-center text-xs text-text-tertiary hover:text-primary-600'
>

@ -165,6 +165,7 @@ const ComponentPicker = ({
isSupportFileVar={isSupportFileVar}
onClose={handleClose}
onBlur={handleClose}
autoFocus={false}
/>
</div>
)

@ -63,6 +63,7 @@ import CustomDialog from '@/app/components/base/dialog'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { noop } from 'lodash-es'
import { useDocLink } from '@/context/i18n'
const TextLabel: FC<PropsWithChildren> = (props) => {
return <label className='system-sm-semibold text-text-secondary'>{props.children}</label>
@ -146,6 +147,7 @@ const StepTwo = ({
updateRetrievalMethodCache,
}: StepTwoProps) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { locale } = useContext(I18n)
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
@ -962,7 +964,9 @@ const StepTwo = ({
<div className={'mb-1'}>
<div className='system-md-semibold mb-0.5 text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='body-xs-regular text-text-tertiary'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
<a target='_blank' rel='noopener noreferrer'
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents')}
className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
{t('datasetSettings.form.retrievalSetting.longDescription')}
</div>
</div>

@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from './input'
import Button from '@/app/components/base/button'
import { useDocLink } from '@/context/i18n'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -17,6 +18,7 @@ const UrlInput: FC<Props> = ({
onRun,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
const [url, setUrl] = useState('')
const handleUrlChange = useCallback((url: string | number) => {
setUrl(url as string)
@ -32,7 +34,7 @@ const UrlInput: FC<Props> = ({
<Input
value={url}
onChange={handleUrlChange}
placeholder='https://docs.dify.ai'
placeholder={docLink()}
/>
<Button
variant='primary'

@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from './input'
import Button from '@/app/components/base/button'
import { useDocLink } from '@/context/i18n'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -17,6 +18,7 @@ const UrlInput: FC<Props> = ({
onRun,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
const [url, setUrl] = useState('')
const handleUrlChange = useCallback((url: string | number) => {
setUrl(url as string)
@ -32,7 +34,7 @@ const UrlInput: FC<Props> = ({
<Input
value={url}
onChange={handleUrlChange}
placeholder='https://docs.dify.ai'
placeholder={docLink()}
/>
<Button
variant='primary'

@ -29,8 +29,7 @@ import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/u
import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata'
import DatasetMetadataDrawer from '../metadata/metadata-dataset/dataset-metadata-drawer'
import StatusWithAction from '../common/document-status-with-action/status-with-action'
import { LanguagesSupported } from '@/i18n/language'
import { getLocaleOnClient } from '@/i18n'
import { useDocLink } from '@/context/i18n'
const FolderPlusIcon = ({ className }: React.SVGProps<SVGElement>) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
@ -86,6 +85,7 @@ const DEFAULT_LIMIT = 10
const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { plan } = useProviderContext()
const isFreePlan = plan.type === 'sandbox'
const [inputValue, setInputValue] = useState<string>('') // the input value
@ -100,7 +100,6 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const isDataSourceWeb = dataset?.data_source_type === DataSourceType.WEB
const isDataSourceFile = dataset?.data_source_type === DataSourceType.FILE
const embeddingAvailable = !!dataset?.embedding_available
const locale = getLocaleOnClient()
const debouncedSearchValue = useDebounce(searchValue, { wait: 500 })
const { data: documentsRes, isFetching: isListLoading } = useDocumentList({
@ -262,11 +261,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
<a
className='flex items-center text-text-accent'
target='_blank'
href={
locale === LanguagesSupported[1]
? 'https://docs.dify.ai/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
: 'https://docs.dify.ai/en/guides/knowledge-base/integrate-knowledge-within-application'
}
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
>
<span>{t('datasetDocuments.list.learnMore')}</span>
<RiExternalLinkLine className='h-3 w-3' />

@ -5,6 +5,7 @@ import { RiBookOpenLine } from '@remixicon/react'
import type { CreateExternalAPIReq, FormSchema } from '../declarations'
import Input from '@/app/components/base/input'
import cn from '@/utils/classnames'
import { useDocLink } from '@/context/i18n'
type FormProps = {
className?: string
@ -26,6 +27,7 @@ const Form: FC<FormProps> = React.memo(({
inputClassName,
}) => {
const { t, i18n } = useTranslation()
const docLink = useDocLink()
const [changeKey, setChangeKey] = useState('')
const handleFormChange = (key: string, val: string) => {
@ -57,7 +59,7 @@ const Form: FC<FormProps> = React.memo(({
</label>
{variable === 'endpoint' && (
<a
href={'https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' || '/'}
href={docLink('/guides/knowledge-base/connect-external-knowledge-base') || '/'}
target='_blank'
rel='noopener noreferrer'
className='body-xs-regular flex items-center text-text-accent'

@ -12,6 +12,7 @@ import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { useModalContext } from '@/context/modal-context'
import { useDocLink } from '@/context/i18n'
type ExternalAPIPanelProps = {
onClose: () => void
@ -19,6 +20,7 @@ type ExternalAPIPanelProps = {
const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { setShowExternalKnowledgeAPIModal } = useModalContext()
const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } = useExternalKnowledgeApi()
@ -50,7 +52,8 @@ const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose }) => {
<div className='flex grow flex-col items-start gap-1'>
<div className='system-xl-semibold self-stretch text-text-primary'>{t('dataset.externalAPIPanelTitle')}</div>
<div className='body-xs-regular self-stretch text-text-tertiary'>{t('dataset.externalAPIPanelDescription')}</div>
<a className='flex cursor-pointer items-center justify-center gap-1 self-stretch' href='https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' target='_blank'>
<a className='flex cursor-pointer items-center justify-center gap-1 self-stretch'
href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} target='_blank'>
<RiBookOpenLine className='h-3 w-3 text-text-accent' />
<div className='body-xs-regular grow text-text-accent'>{t('dataset.externalAPIPanelDocumentation')}</div>
</a>

@ -1,8 +1,10 @@
import { RiBookOpenLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
const InfoPanel = () => {
const { t } = useTranslation()
const docLink = useDocLink()
return (
<div className='flex w-[360px] flex-col items-start pb-2 pr-8 pt-[108px]'>
@ -16,12 +18,15 @@ const InfoPanel = () => {
</span>
<span className='system-sm-regular text-text-tertiary'>
{t('dataset.connectDatasetIntro.content.front')}
<a className='system-sm-regular ml-1 text-text-accent' href='https://docs.dify.ai/en/guides/knowledge-base/external-knowledge-api' target='_blank' rel="noopener noreferrer">
<a className='system-sm-regular ml-1 text-text-accent' href={docLink('/guides/knowledge-base/external-knowledge-api')} target='_blank' rel="noopener noreferrer">
{t('dataset.connectDatasetIntro.content.link')}
</a>
{t('dataset.connectDatasetIntro.content.end')}
</span>
<a className='system-sm-regular self-stretch text-text-accent' href='https://docs.dify.ai/en/guides/knowledge-base/connect-external-knowledge-base' target='_blank' rel="noopener noreferrer">
<a className='system-sm-regular self-stretch text-text-accent'
href={docLink('/guides/knowledge-base/connect-external-knowledge-base')}
target='_blank'
rel="noopener noreferrer">
{t('dataset.connectDatasetIntro.learnMore')}
</a>
</p>

@ -11,6 +11,7 @@ import InfoPanel from './InfoPanel'
import type { CreateKnowledgeBaseReq } from './declarations'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import { useDocLink } from '@/context/i18n'
type ExternalKnowledgeBaseCreateProps = {
onConnect: (formValue: CreateKnowledgeBaseReq) => void
@ -19,6 +20,7 @@ type ExternalKnowledgeBaseCreateProps = {
const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = ({ onConnect, loading }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const router = useRouter()
const [formData, setFormData] = useState<CreateKnowledgeBaseReq>({
name: '',
@ -59,7 +61,7 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
<span>{t('dataset.connectHelper.helper1')}</span>
<span className='system-sm-medium text-text-secondary'>{t('dataset.connectHelper.helper2')}</span>
<span>{t('dataset.connectHelper.helper3')}</span>
<a className='system-sm-regular self-stretch text-text-accent' href='https://docs.dify.ai/en/guides/knowledge-base/connect-external-knowledge-base' target='_blank' rel="noopener noreferrer">
<a className='system-sm-regular self-stretch text-text-accent' href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} target='_blank' rel="noopener noreferrer">
{t('dataset.connectHelper.helper4')}
</a>
<span>{t('dataset.connectHelper.helper5')} </span>

@ -11,6 +11,7 @@ import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/ec
import Button from '@/app/components/base/button'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDocLink } from '@/context/i18n'
type Props = {
indexMethod: string
@ -29,6 +30,7 @@ const ModifyRetrievalModal: FC<Props> = ({
}) => {
const ref = useRef(null)
const { t } = useTranslation()
const docLink = useDocLink()
const [retrievalConfig, setRetrievalConfig] = useState(value)
// useClickAway(() => {
@ -72,7 +74,10 @@ const ModifyRetrievalModal: FC<Props> = ({
<a
target='_blank'
rel='noopener noreferrer'
href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings'
href={docLink('/guides/knowledge-base/retrieval-test-and-citation#modify-text-retrieval-setting', {
'zh-Hans': '/guides/knowledge-base/retrieval-test-and-citation#修改文本检索方式',
'ja-JP': '/guides/knowledge-base/retrieval-test-and-citation',
})}
className='text-text-accent'
>
{t('datasetSettings.form.retrievalSetting.learnMore')}

@ -32,6 +32,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle'
import { useDocLink } from '@/context/i18n'
const rowClass = 'flex'
const labelClass = `
@ -46,6 +47,7 @@ const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
const Form = () => {
const { t } = useTranslation()
const docLink = useDocLink()
const { notify } = useContext(ToastContext)
const { mutate } = useSWRConfig()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
@ -308,7 +310,16 @@ const Form = () => {
<div>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='body-xs-regular text-text-tertiary'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
<a
target='_blank'
rel='noopener noreferrer'
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting', {
'zh-Hans': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#指定检索方式',
'ja-JP': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#検索方法の指定',
})}
className='text-text-accent'>
{t('datasetSettings.form.retrievalSetting.learnMore')}
</a>
{t('datasetSettings.form.retrievalSetting.description')}
</div>
</div>

@ -57,8 +57,8 @@ The text generation application offers non-session support and is ideal for tran
<i>Due to Cloudflare restrictions, the request will be interrupted without a return after 100 seconds.</i>
</Property>
<Property name='user' type='string' key='user'>
User identifier, used to define the identity of the end-user for retrieval and statistics.
Should be uniquely defined by the developer within the application.
User identifier, used to define the identity of the end-user, convenient for retrieval and statistics.
The rules are defined by the developer and need to ensure that the user identifier is unique within the application. The Service API does not share conversations created by the WebApp.
</Property>
<Property name='files' type='array[object]' key='files'>
File list, suitable for inputting files (images) combined with text understanding and answering questions, available only when the model supports Vision capability.
@ -220,7 +220,7 @@ The text generation application offers non-session support and is ideal for tran
- `file` (File) Required
The file to be uploaded.
- `user` (string) Required
User identifier, defined by the developer's rules, must be unique within the application.
User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp.
### Response
After a successful upload, the server will return the file's ID and related information.
@ -290,7 +290,7 @@ The text generation application offers non-session support and is ideal for tran
- `task_id` (string) Task ID, can be obtained from the streaming chunk return
Request Body
- `user` (string) Required
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface.
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface. The Service API does not share conversations created by the WebApp.
### Response
- `result` (string) Always returns "success"
</Col>
@ -512,6 +512,8 @@ The text generation application offers non-session support and is ideal for tran
- `name` (string) application name
- `description` (string) application description
- `tags` (array[string]) application tags
- `mode` (string) application mode
- `author_name` (string) author name
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -528,7 +530,9 @@ The text generation application offers non-session support and is ideal for tran
"tags": [
"tag1",
"tag2"
]
],
"mode": "chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -220,7 +220,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `file` (File) 必須
アップロードするファイル。
- `user` (string) 必須
開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。
開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。サービス API は WebApp によって作成された会話を共有しません。
### レスポンス
アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
@ -289,7 +289,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `task_id` (string) タスク ID、ストリーミングチャンクの返信から取得可能
リクエストボディ
- `user` (string) 必須
ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。
ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。サービス API は WebApp によって作成された会話を共有しません。
### レスポンス
- `result` (string) 常に"success"を返します
</Col>
@ -510,6 +510,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `name` (string) アプリケーションの名前
- `description` (string) アプリケーションの説明
- `tags` (array[string]) アプリケーションのタグ
- `mode` (string) アプリケーションのモード
- `author_name` (string) 作者の名前
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -526,7 +528,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
"tags": [
"tag1",
"tag2"
]
],
"mode": "chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -266,7 +266,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### Request Body
- `user` (string) Required
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。
### Response
- `result` (string) 固定返回 success
</Col>
@ -485,6 +485,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `name` (string) 应用名称
- `description` (string) 应用描述
- `tags` (array[string]) 应用标签
- `mode` (string) 应用模式
- 'author_name' (string) 作者名称
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -501,7 +503,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
"tags": [
"tag1",
"tag2"
]
],
"mode": "chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -59,7 +59,7 @@ Chat applications support session persistence, allowing previous chat history to
</Property>
<Property name='user' type='string' key='user'>
User identifier, used to define the identity of the end-user for retrieval and statistics.
Should be uniquely defined by the developer within the application.
Should be uniquely defined by the developer within the application. The Service API does not share conversations created by the WebApp.
</Property>
<Property name='conversation_id' type='string' key='conversation_id'>
Conversation ID, to continue the conversation based on previous chat records, it is necessary to pass the previous message's conversation_id.
@ -324,7 +324,7 @@ Chat applications support session persistence, allowing previous chat history to
- `file` (File) Required
The file to be uploaded.
- `user` (string) Required
User identifier, defined by the developer's rules, must be unique within the application.
User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp.
### Response
After a successful upload, the server will return the file's ID and related information.
@ -394,7 +394,7 @@ Chat applications support session persistence, allowing previous chat history to
- `task_id` (string) Task ID, can be obtained from the streaming chunk return
### Request Body
- `user` (string) Required
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface.
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp.
### Response
- `result` (string) Always returns "success"
</Col>
@ -448,7 +448,7 @@ Chat applications support session persistence, allowing previous chat history to
Upvote as `like`, downvote as `dislike`, revoke upvote as `null`
</Property>
<Property name='user' type='string' key='user'>
User identifier, defined by the developer's rules, must be unique within the application.
User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp.
</Property>
<Property name='content' type='string' key='content'>
The specific content of message feedback.
@ -1123,6 +1123,8 @@ Chat applications support session persistence, allowing previous chat history to
- `name` (string) application name
- `description` (string) application description
- `tags` (array[string]) application tags
- `mode` (string) application mode
- `author_name` (string) application author name
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1139,7 +1141,9 @@ Chat applications support session persistence, allowing previous chat history to
"tags": [
"tag1",
"tag2"
]
],
"mode": "advanced-chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -59,7 +59,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Property>
<Property name='user' type='string' key='user'>
ユーザー識別子、エンドユーザーの身元を定義するために使用され、統計のために使用されます。
アプリケーション内で開発者によって一意に定義されるべきです。
アプリケーション内で開発者によって一意に定義されるべきです。サービス API は WebApp によって作成された会話を共有しません。
</Property>
<Property name='conversation_id' type='string' key='conversation_id'>
会話ID、以前のチャット記録に基づいて会話を続けるには、以前のメッセージのconversation_idを渡す必要があります。
@ -324,7 +324,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `file` (File) 必須
アップロードするファイル。
- `user` (string) 必須
ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。
ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。サービス API は WebApp によって作成された会話を共有しません。
### 応答
アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
@ -394,7 +394,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます
### リクエストボディ
- `user` (string) 必須
ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。
ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。サービス API は WebApp によって作成された会話を共有しません。
### 応答
- `result` (string) 常に"success"を返します
</Col>
@ -1123,6 +1123,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `name` (string) アプリケーションの名前
- `description` (string) アプリケーションの説明
- `tags` (array[string]) アプリケーションのタグ
- `mode` (string) アプリケーションのモード
- `author_name` (string) 作者の名前
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1139,7 +1141,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
"tags": [
"tag1",
"tag2"
]
],
"mode": "advanced-chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -56,7 +56,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Property>
<Property name='user' type='string' key='user'>
用户标识,用于定义终端用户的身份,方便检索、统计。
由开发者定义规则,需保证用户标识在应用内唯一。
由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。
</Property>
<Property name='conversation_id' type='string' key='conversation_id'>
(选填)会话 ID需要基于之前的聊天记录继续对话必须传之前消息的 conversation_id。
@ -402,7 +402,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### Request Body
- `user` (string) Required
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。
### Response
- `result` (string) 固定返回 success
</Col>
@ -1173,7 +1173,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
"tags": [
"tag1",
"tag2"
]
],
"mode": "advanced-chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -287,7 +287,7 @@ Chat applications support session persistence, allowing previous chat history to
- `file` (File) Required
The file to be uploaded.
- `user` (string) Required
User identifier, defined by the developer's rules, must be unique within the application.
User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp.
### Response
After a successful upload, the server will return the file's ID and related information.
@ -357,7 +357,7 @@ Chat applications support session persistence, allowing previous chat history to
- `task_id` (string) Task ID, can be obtained from the streaming chunk return
### Request Body
- `user` (string) Required
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface.
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp.
### Response
- `result` (string) Always returns "success"
</Col>
@ -1151,6 +1151,8 @@ Chat applications support session persistence, allowing previous chat history to
- `name` (string) application name
- `description` (string) application description
- `tags` (array[string]) application tags
- `mode` (string) application mode
- `author_name` (string) application author name
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1167,7 +1169,9 @@ Chat applications support session persistence, allowing previous chat history to
"tags": [
"tag1",
"tag2"
]
],
"mode": "advanced-chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -287,7 +287,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `file` (File) 必須
アップロードするファイル。
- `user` (string) 必須
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。サービス API は WebApp によって作成された会話を共有しません。
### 応答
アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
@ -357,7 +357,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます
### リクエストボディ
- `user` (string) 必須
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。サービス API は WebApp によって作成された会話を共有しません。
### 応答
- `result` (string) 常に"success"を返します
</Col>
@ -1150,6 +1150,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `name` (string) アプリケーションの名前
- `description` (string) アプリケーションの説明
- `tags` (array[string]) アプリケーションのタグ
- `mode` (string) アプリケーションのモード
- `author_name` (string) 作者の名前
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1166,7 +1168,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
"tags": [
"tag1",
"tag2"
]
],
"mode": "chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -56,7 +56,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Property>
<Property name='user' type='string' key='user'>
用户标识,用于定义终端用户的身份,方便检索、统计。
由开发者定义规则,需保证用户标识在应用内唯一。
由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。
</Property>
<Property name='conversation_id' type='string' key='conversation_id'>
(选填)会话 ID需要基于之前的聊天记录继续对话必须传之前消息的 conversation_id。
@ -306,7 +306,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
要上传的文件。
</Property>
<Property name='user' type='string' key='user'>
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。服务 API 不会共享 WebApp 创建的对话。
</Property>
</Properties>
@ -373,7 +373,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### Request Body
- `user` (string) Required
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。
### Response
- `result` (string) 固定返回 success
</Col>
@ -425,7 +425,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
点赞 like, 点踩 dislike, 撤销点赞 null
</Property>
<Property name='user' type='string' key='user'>
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。
</Property>
<Property name='content' type='string' key='content'>
消息反馈的具体信息。
@ -1162,6 +1162,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `name` (string) 应用名称
- `description` (string) 应用描述
- `tags` (array[string]) 应用标签
- `mode` (string) 应用模式
- 'author_name' (string) 作者名称
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1178,7 +1180,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
"tags": [
"tag1",
"tag2"
]
],
"mode": "chat",
"author_name": "Dify"
}
```
</CodeGroup>

@ -64,6 +64,8 @@ Workflow applications offers non-session support and is ideal for translation, a
- `user` (string) Required
User identifier, used to define the identity of the end-user for retrieval and statistics.
Should be uniquely defined by the developer within the application.
<br/>
<i>The user identifier should be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp.</i>
### Response
When `response_mode` is `blocking`, return a CompletionResponse object.
@ -401,7 +403,7 @@ Workflow applications offers non-session support and is ideal for translation, a
- `task_id` (string) Task ID, can be obtained from the streaming chunk return
### Request Body
- `user` (string) Required
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface.
User identifier, used to define the identity of the end-user, must be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp.
### Response
- `result` (string) Always returns "success"
</Col>
@ -448,7 +450,7 @@ Workflow applications offers non-session support and is ideal for translation, a
- `file` (File) Required
The file to be uploaded.
- `user` (string) Required
User identifier, defined by the developer's rules, must be unique within the application.
User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp.
### Response
After a successful upload, the server will return the file's ID and related information.
@ -625,6 +627,8 @@ Workflow applications offers non-session support and is ideal for translation, a
- `name` (string) application name
- `description` (string) application description
- `tags` (array[string]) application tags
- `mode` (string) application mode
- `author_name` (string) application author name
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -641,7 +645,9 @@ Workflow applications offers non-session support and is ideal for translation, a
"tags": [
"tag1",
"tag2"
]
],
"mode": "workflow",
"author_name": "Dify"
}
```
</CodeGroup>

@ -404,7 +404,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得可能
### リクエストボディ
- `user` (string) 必須
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。サービス API は WebApp によって作成された会話を共有しません。
### 応答
- `result` (string) 常に"success"を返します
</Col>
@ -451,7 +451,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `file` (File) 必須
アップロードするファイル。
- `user` (string) 必須
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。サービス API は WebApp によって作成された会話を共有しません。
### 応答
アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
@ -628,6 +628,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `name` (string) アプリケーションの名前
- `description` (string) アプリケーションの説明
- `tags` (array[string]) アプリケーションのタグ
- `mode` (string) アプリケーションのモード
- `author_name` (string) 作者の名前
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -644,7 +646,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
"tags": [
"tag1",
"tag2"
]
],
"mode": "workflow",
"author_name": "Dify"
}
```
</CodeGroup>

@ -59,7 +59,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
<i>由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。</i>
- `user` (string) Required
用户标识,用于定义终端用户的身份,方便检索、统计。
由开发者定义规则,需保证用户标识在应用内唯一。
由开发者定义规则,需保证用户标识在应用内唯一。API 无法访问 WebApp 创建的会话。
### Response
@ -394,7 +394,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `task_id` (string) 任务 ID可在流式返回 Chunk 中获取
### Request Body
- `user` (string) Required
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。
### Response
- `result` (string) 固定返回 "success"
</Col>
@ -443,7 +443,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
要上传的文件。
</Property>
<Property name='user' type='string' key='user'>
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。服务 API 不会共享 WebApp 创建的对话。
</Property>
</Properties>
@ -615,6 +615,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `name` (string) 应用名称
- `description` (string) 应用描述
- `tags` (array[string]) 应用标签
- `mode` (string) 应用模式
- 'author_name' (string) 作者名称
</Col>
<Col>
<CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -631,7 +633,9 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
"tags": [
"tag1",
"tag2"
]
],
"mode": "workflow",
"author_name": "Dify"
}
```
</CodeGroup>

@ -23,7 +23,6 @@ import GithubStar from '../github-star'
import Support from './support'
import Compliance from './compliance'
import PremiumBadge from '@/app/components/base/premium-badge'
import { useGetDocLanguage } from '@/context/i18n'
import Avatar from '@/app/components/base/avatar'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { logout } from '@/service/common'
@ -33,6 +32,7 @@ import { useModalContext } from '@/context/modal-context'
import { IS_CLOUD_EDITION } from '@/config'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
export default function AppSelector() {
const itemClassName = `
@ -44,10 +44,10 @@ export default function AppSelector() {
const { systemFeatures } = useGlobalPublicStore()
const { t } = useTranslation()
const docLink = useDocLink()
const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext()
const docLanguage = useGetDocLanguage()
const handleLogout = async () => {
await logout({
@ -133,7 +133,7 @@ export default function AppSelector() {
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href={`https://docs.dify.ai/${docLanguage}/introduction`}
href={docLink('/introduction')}
target='_blank' rel='noopener noreferrer'>
<RiBookOpenLine className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.helpCenter')}</div>

@ -31,22 +31,22 @@ const WorkplaceSelector = () => {
}
return (
<Menu as="div" className="relative h-full w-full">
<Menu as="div" className="min-w-0">
{
({ open }) => (
<>
<MenuButton className={cn(
`
group flex w-full cursor-pointer items-center
gap-1.5 p-0.5 hover:bg-state-base-hover ${open && 'bg-state-base-hover'} rounded-[10px]
p-0.5 hover:bg-state-base-hover ${open && 'bg-state-base-hover'} rounded-[10px]
`,
)}>
<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]'>
<div className='mr-1.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px] max-[800px]:mr-0'>
<span className='h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle font-semibold uppercase leading-6 text-shadow-shadow-1 opacity-90'>{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span>
</div>
<div className='flex flex-row'>
<div className={'system-sm-medium max-w-[160px] truncate text-text-secondary'}>{currentWorkspace?.name}</div>
<RiArrowDownSLine className='h-4 w-4 text-text-secondary' />
<div className='flex min-w-0 items-center'>
<div className={'system-sm-medium min-w-0 max-w-[149px] truncate text-text-secondary max-[800px]:hidden'}>{currentWorkspace?.name}</div>
<RiArrowDownSLine className='h-4 w-4 shrink-0 text-text-secondary' />
</div>
</MenuButton>
<Transition
@ -59,10 +59,11 @@ const WorkplaceSelector = () => {
leaveTo="transform opacity-0 scale-95"
>
<MenuItems
anchor="bottom start"
className={cn(
`
shadows-shadow-lg absolute left-[-15px] mt-1 flex max-h-[400px] w-[280px] flex-col items-start overflow-y-auto rounded-xl
bg-components-panel-bg-blur backdrop-blur-[5px]
shadows-shadow-lg absolute left-[-15px] z-[1000] mt-1 flex max-h-[400px] w-[280px] flex-col items-start overflow-y-auto
rounded-xl bg-components-panel-bg-blur backdrop-blur-[5px]
`,
)}
>
@ -73,7 +74,7 @@ const WorkplaceSelector = () => {
{
workspaces.map(workspace => (
<div className='flex items-center gap-2 self-stretch rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]'>
<div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]'>
<span className='h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle font-semibold uppercase leading-6 text-shadow-shadow-1 opacity-90'>{workspace?.name[0]?.toLocaleUpperCase()}</span>
</div>
<div className='system-md-regular line-clamp-1 grow cursor-pointer overflow-hidden text-ellipsis text-text-secondary'>{workspace.name}</div>

@ -3,9 +3,11 @@ import {
RiExternalLinkLine,
RiPuzzle2Line,
} from '@remixicon/react'
import { useDocLink } from '@/context/i18n'
const Empty = () => {
const { t } = useTranslation()
const docLink = useDocLink()
return (
<div className='mb-2 rounded-xl bg-background-section p-6'>
@ -15,7 +17,7 @@ const Empty = () => {
<div className='system-sm-medium mb-1 text-text-secondary'>{t('common.apiBasedExtension.title')}</div>
<a
className='system-xs-regular flex items-center text-text-accent'
href={t('common.apiBasedExtension.linkUrl') || '/'}
href={docLink('/guides/extension/api-based-extension/README')}
target='_blank' rel='noopener noreferrer'
>
{t('common.apiBasedExtension.link')}

@ -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<ApiBasedExtensionModalProps> = ({
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<ApiBasedExtensionModalProps> = ({
<div className='flex h-9 items-center justify-between text-sm font-medium text-text-primary'>
{t('common.apiBasedExtension.modal.apiEndpoint.title')}
<a
href={t('common.apiBasedExtension.linkUrl') || '/'}
href={docLink('/guides/extension/api-based-extension/README')}
target='_blank' rel='noopener noreferrer'
className='group flex items-center text-xs font-normal text-text-accent'
>

@ -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}

@ -48,7 +48,7 @@ const DatasetNav = () => {
text={t('common.menus.datasets')}
activeSegment='datasets'
link='/datasets'
curNav={currentDataset as Omit<NavItem, 'link'>}
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}
/>
)
}

@ -20,22 +20,22 @@ const EnvNav = () => {
return (
<div className={`
mr-4 flex h-[22px] items-center rounded-md border px-2 text-xs font-medium
mr-1 flex h-[22px] items-center rounded-md border px-2 text-xs font-medium
${headerEnvClassName[langeniusVersionInfo.current_env]}
`}>
{
langeniusVersionInfo.current_env === 'TESTING' && (
<>
<Beaker02 className='mr-1 h-3 w-3' />
{t('common.environment.testing')}
<Beaker02 className='h-3 w-3' />
<div className='ml-1 max-[1280px]:hidden'>{t('common.environment.testing')}</div>
</>
)
}
{
langeniusVersionInfo.current_env === 'DEVELOPMENT' && (
<>
<TerminalSquare className='mr-1 h-3 w-3' />
{t('common.environment.development')}
<TerminalSquare className='h-3 w-3' />
<div className='ml-1 max-[1280px]:hidden'>{t('common.environment.development')}</div>
</>
)
}

@ -27,10 +27,12 @@ const ExploreNav = ({
)}>
{
activated
? <RiPlanetFill className='mr-2 h-4 w-4' />
: <RiPlanetLine className='mr-2 h-4 w-4' />
? <RiPlanetFill className='h-4 w-4' />
: <RiPlanetLine className='h-4 w-4' />
}
{t('common.menus.explore')}
<div className='ml-2 max-[1024px]:hidden'>
{t('common.menus.explore')}
</div>
</Link>
)
}

@ -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 (
<div className='relative flex flex-1 items-center justify-between bg-background-body'>
<div className='flex items-center'>
{isMobile && <div
className='flex h-8 w-8 cursor-pointer items-center justify-center'
onClick={toggle}
>
<Bars3Icon className="h-4 w-4 text-gray-500" />
</div>}
{
!isMobile
&& <div className='flex shrink-0 items-center gap-1.5 self-stretch pl-3'>
<Link href="/apps" className='flex h-8 shrink-0 items-center justify-center gap-2 px-0.5'>
if (isMobile) {
return (
<div className=''>
<div className='flex items-center justify-between px-2'>
<div className='flex items-center'>
<Link href="/apps" className='flex h-8 shrink-0 items-center justify-center px-0.5'>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img
src={systemFeatures.branding.workspace_logo}
@ -71,59 +55,61 @@ const Header = () => {
/>
: <DifyLogo />}
</Link>
<div className='font-light text-divider-deep'>/</div>
<div className='flex items-center gap-0.5'>
<WorkspaceProvider>
<WorkplaceSelector />
</WorkspaceProvider>
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div>
<div className='mx-1.5 shrink-0 font-light text-divider-deep'>/</div>
<WorkspaceProvider>
<WorkplaceSelector />
</WorkspaceProvider>
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div>
}
</div >
{isMobile && (
<div className='flex'>
<Link href="/apps" className='mr-4 flex items-center'>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img
src={systemFeatures.branding.workspace_logo}
className='block h-[22px] w-auto object-contain'
alt='logo'
/>
: <DifyLogo />}
</Link>
<div className='font-light text-divider-deep'>/</div>
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div >
)}
{
!isMobile && (
<div className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
<div className='flex items-center'>
<div className='mr-2'>
<PluginsNav />
</div>
<AccountDropdown />
</div>
)
}
<div className='flex shrink-0 items-center pr-3'>
</div>
<div className='my-1 flex items-center justify-center space-x-1'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
</div>
)
}
return (
<div className='flex h-[60px] items-center'>
<div className='flex min-w-0 flex-[1] items-center pl-3 pr-2 min-[1280px]:pr-3'>
<Link href="/apps" className='flex h-8 shrink-0 items-center justify-center px-0.5'>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img
src={systemFeatures.branding.workspace_logo}
className='block h-[22px] w-auto object-contain'
alt='logo'
/>
: <DifyLogo />}
</Link>
<div className='mx-1.5 shrink-0 font-light text-divider-deep'>/</div>
<WorkspaceProvider>
<WorkplaceSelector />
</WorkspaceProvider>
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div>
<div className='flex items-center space-x-2'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
<div className='flex min-w-0 flex-[1] items-center justify-end pl-2 pr-3 min-[1280px]:pl-3'>
<EnvNav />
<div className='mr-2'>
<PluginsNav />
</div>
<AccountDropdown />
</div>
{
(isMobile && isShowNavMenu) && (
<div className='flex w-full flex-col gap-y-1 p-2'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
)
}
</div >
</div>
)
}
export default Header

@ -46,7 +46,7 @@ const Nav = ({
return (
<div className={`
mr-0 flex h-8 shrink-0 items-center rounded-xl px-0.5 text-sm font-medium sm:mr-3
flex h-8 max-w-[670px] shrink-0 items-center rounded-xl px-0.5 text-sm font-medium max-[1024px]:max-w-[400px]
${isActivated && 'bg-components-main-nav-nav-button-bg-active font-semibold shadow-md'}
${!curNav && !isActivated && 'hover:bg-components-main-nav-nav-button-bg-hover'}
`}>
@ -61,7 +61,7 @@ const Nav = ({
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<div className='mr-2'>
<div>
{
(hovered && curNav)
? <ArrowNarrowLeft className='h-4 w-4' />
@ -70,7 +70,9 @@ const Nav = ({
: icon
}
</div>
{text}
<div className='ml-2 max-[1024px]:hidden'>
{text}
</div>
</div>
</Link>
{

@ -53,136 +53,134 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
}, 50), [])
return (
<div className="">
<Menu as="div" className="relative inline-block text-left">
{({ open }) => (
<>
<MenuButton className={cn(
'hover:hover:bg-components-main-nav-nav-button-bg-active-hover group inline-flex h-7 w-full items-center justify-center rounded-[10px] pl-2 pr-2.5 text-[14px] font-semibold text-components-main-nav-nav-button-text-active',
open && 'bg-components-main-nav-nav-button-bg-active',
)}>
<div className='max-w-[180px] truncate' title={curNav?.name}>{curNav?.name}</div>
<RiArrowDownSLine
className={cn('ml-1 h-3 w-3 shrink-0 opacity-50 group-hover:opacity-100', open && '!opacity-100')}
aria-hidden="true"
/>
</MenuButton>
<MenuItems
className="
absolute -left-11 right-0 mt-1.5 w-60 max-w-80
origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur
shadow-lg
"
>
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
{
navs.map(nav => (
<MenuItem key={nav.id}>
<div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-text-secondary hover:bg-state-base-hover' onClick={() => {
if (curNav?.id === nav.id)
return
setAppDetail()
router.push(nav.link)
}} title={nav.name}>
<div className='relative mr-2 h-6 w-6 rounded-md'>
<AppIcon size='tiny' iconType={nav.icon_type} icon={nav.icon} background={nav.icon_background} imageUrl={nav.icon_url} />
{!!nav.mode && (
<span className={cn(
'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm',
)}>
{nav.mode === 'advanced-chat' && (
<ChatBot className='h-2.5 w-2.5 text-[#1570EF]' />
)}
{nav.mode === 'agent-chat' && (
<CuteRobot className='h-2.5 w-2.5 text-indigo-600' />
)}
{nav.mode === 'chat' && (
<ChatBot className='h-2.5 w-2.5 text-[#1570EF]' />
)}
{nav.mode === 'completion' && (
<AiText className='h-2.5 w-2.5 text-[#0E9384]' />
)}
{nav.mode === 'workflow' && (
<Route className='h-2.5 w-2.5 text-[#f79009]' />
)}
</span>
)}
</div>
<div className='truncate'>
{nav.name}
</div>
<Menu as="div" className="relative">
{({ open }) => (
<>
<MenuButton className={cn(
'hover:hover:bg-components-main-nav-nav-button-bg-active-hover group inline-flex h-7 w-full items-center justify-center rounded-[10px] pl-2 pr-2.5 text-[14px] font-semibold text-components-main-nav-nav-button-text-active',
open && 'bg-components-main-nav-nav-button-bg-active',
)}>
<div className='max-w-[157px] truncate' title={curNav?.name}>{curNav?.name}</div>
<RiArrowDownSLine
className={cn('ml-1 h-3 w-3 shrink-0 opacity-50 group-hover:opacity-100', open && '!opacity-100')}
aria-hidden="true"
/>
</MenuButton>
<MenuItems
className="
absolute -left-11 right-0 mt-1.5 w-60 max-w-80
origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur
shadow-lg
"
>
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
{
navs.map(nav => (
<MenuItem key={nav.id}>
<div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-text-secondary hover:bg-state-base-hover' onClick={() => {
if (curNav?.id === nav.id)
return
setAppDetail()
router.push(nav.link)
}} title={nav.name}>
<div className='relative mr-2 h-6 w-6 rounded-md'>
<AppIcon size='tiny' iconType={nav.icon_type} icon={nav.icon} background={nav.icon_background} imageUrl={nav.icon_url} />
{!!nav.mode && (
<span className={cn(
'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm',
)}>
{nav.mode === 'advanced-chat' && (
<ChatBot className='h-2.5 w-2.5 text-[#1570EF]' />
)}
{nav.mode === 'agent-chat' && (
<CuteRobot className='h-2.5 w-2.5 text-indigo-600' />
)}
{nav.mode === 'chat' && (
<ChatBot className='h-2.5 w-2.5 text-[#1570EF]' />
)}
{nav.mode === 'completion' && (
<AiText className='h-2.5 w-2.5 text-[#0E9384]' />
)}
{nav.mode === 'workflow' && (
<Route className='h-2.5 w-2.5 text-[#f79009]' />
)}
</span>
)}
</div>
<div className='truncate'>
{nav.name}
</div>
</MenuItem>
))
}
</div>
{!isApp && isCurrentWorkspaceEditor && (
<MenuItem as="div" className='w-full p-1'>
<div onClick={() => onCreate('')} className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover ',
)}>
<div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'>
<RiAddLine className='h-4 w-4 text-text-primary' />
</div>
<div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div>
</MenuItem>
))
}
</div>
{!isApp && isCurrentWorkspaceEditor && (
<MenuItem as="div" className='w-full p-1'>
<div onClick={() => onCreate('')} className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover ',
)}>
<div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'>
<RiAddLine className='h-4 w-4 text-text-primary' />
</div>
</MenuItem>
)}
{isApp && isCurrentWorkspaceEditor && (
<Menu as="div" className="relative h-full w-full">
{({ open }) => (
<>
<MenuButton className='w-full p-1'>
<div className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover',
open && '!bg-state-base-hover',
)}>
<div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'>
<RiAddLine className='h-4 w-4 text-text-primary' />
</div>
<div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div>
<RiArrowRightSLine className='h-3.5 w-3.5 shrink-0 text-text-primary' />
<div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div>
</div>
</MenuItem>
)}
{isApp && isCurrentWorkspaceEditor && (
<Menu as="div" className="relative h-full w-full">
{({ open }) => (
<>
<MenuButton className='w-full p-1'>
<div className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover',
open && '!bg-state-base-hover',
)}>
<div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'>
<RiAddLine className='h-4 w-4 text-text-primary' />
</div>
</MenuButton>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems className={cn(
'absolute right-[-198px] top-[3px] z-10 min-w-[200px] rounded-lg bg-components-panel-bg-blur shadow-lg',
)}>
<div className='p-1'>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('blank')}>
<FilePlus01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' />
{t('app.newApp.startFromBlank')}
</div>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('template')}>
<FilePlus02 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' />
{t('app.newApp.startFromTemplate')}
</div>
<div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div>
<RiArrowRightSLine className='h-3.5 w-3.5 shrink-0 text-text-primary' />
</div>
</MenuButton>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems className={cn(
'absolute right-[-198px] top-[3px] z-10 min-w-[200px] rounded-lg bg-components-panel-bg-blur shadow-lg',
)}>
<div className='p-1'>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('blank')}>
<FilePlus01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' />
{t('app.newApp.startFromBlank')}
</div>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('template')}>
<FilePlus02 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' />
{t('app.newApp.startFromTemplate')}
</div>
<div className='border-t border-divider-regular p-1'>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('dsl')}>
<FileArrow01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' />
{t('app.importDSL')}
</div>
</div>
<div className='border-t border-divider-regular p-1'>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('dsl')}>
<FileArrow01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' />
{t('app.importDSL')}
</div>
</MenuItems>
</Transition>
</>
)}
</Menu>
)}
</MenuItems>
</>
)}
</Menu>
</div>
</div>
</MenuItems>
</Transition>
</>
)}
</Menu>
)}
</MenuItems>
</>
)}
</Menu>
)
}

@ -28,10 +28,12 @@ const ToolsNav = ({
)}>
{
activated
? <RiHammerFill className='mr-2 h-4 w-4' />
: <RiHammerLine className='mr-2 h-4 w-4' />
? <RiHammerFill className='h-4 w-4' />
: <RiHammerLine className='h-4 w-4' />
}
{t('common.menus.tools')}
<div className='ml-2 max-[1024px]:hidden'>
{t('common.menus.tools')}
</div>
</Link>
)
}

@ -83,6 +83,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
installedValue={updatePayload?.originalPackageInfo.version}
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''}
popupClassName='w-[512px] z-[1001]'
triggerClassName='text-components-input-text-filled'
/>
<label
htmlFor='package'
@ -97,6 +98,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
readonly={!selectedVersion}
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''}
popupClassName='w-[512px] z-[1001]'
triggerClassName='text-components-input-text-filled'
/>
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
{!isEdit

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useDocLink } from '@/context/i18n'
import { useBoolean } from 'ahooks'
import {
RiAddLine,
@ -20,8 +20,6 @@ import {
useInvalidateEndpointList,
} from '@/service/use-endpoints'
import type { PluginDetail } from '@/app/components/plugins/types'
import { LanguagesSupported } from '@/i18n/language'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
type Props = {
@ -29,7 +27,7 @@ type Props = {
}
const EndpointList = ({ detail }: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const pluginUniqueID = detail.plugin_unique_identifier
const declaration = detail.declaration.endpoint
const showTopBorder = detail.declaration.tool
@ -79,7 +77,7 @@ const EndpointList = ({ detail }: Props) => {
</div>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.endpointsTip')}</div>
<a
href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}plugins/schema-definition/endpoint`}
href={docLink('/plugins/schema-definition/endpoint')}
target='_blank'
rel='noopener noreferrer'
>

@ -31,10 +31,13 @@ const TestApi: FC<Props> = ({
const language = getLanguage(locale)
const [credentialsModalShow, setCredentialsModalShow] = useState(false)
const [tempCredential, setTempCredential] = React.useState<Credential>(customCollection.credentials)
const [testing, setTesting] = useState(false)
const [result, setResult] = useState<string>('')
const { operation_id: toolName, parameters } = tool
const [parametersValue, setParametersValue] = useState<Record<string, string>>({})
const handleTest = async () => {
if (testing) return
setTesting(true)
// clone test schema
const credentials = JSON.parse(JSON.stringify(tempCredential)) as Credential
if (credentials.auth_type === AuthType.none) {
@ -52,6 +55,7 @@ const TestApi: FC<Props> = ({
}
const res = await testAPIAvailable(data) as any
setResult(res.error || res.result)
setTesting(false)
}
return (
@ -107,7 +111,7 @@ const TestApi: FC<Props> = ({
</div>
</div>
<Button variant='primary' className=' mt-4 h-10 w-full' onClick={handleTest}>{t('tools.test.title')}</Button>
<Button variant='primary' className=' mt-4 h-10 w-full' loading={testing} disabled={testing} onClick={handleTest}>{t('tools.test.title')}</Button>
<div className='mt-6'>
<div className='flex items-center space-x-3'>
<div className='system-xs-semibold text-text-tertiary'>{t('tools.test.testResult')}</div>

@ -14,6 +14,7 @@ import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-m
import { createCustomCollection } from '@/service/tools'
import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import { useDocLink } from '@/context/i18n'
type Props = {
onRefreshData: () => void
@ -25,10 +26,11 @@ const Contribute = ({ onRefreshData }: Props) => {
const language = getLanguage(locale)
const { isCurrentWorkspaceManager } = useAppContext()
const docLink = useDocLink()
const linkUrl = useMemo(() => {
if (language.startsWith('zh_'))
return 'https://docs.dify.ai/zh-hans/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju'
return 'https://docs.dify.ai/en/guides/tools#how-to-create-custom-tools'
return docLink('/guides/tools#how-to-create-custom-tools', {
'zh-Hans': '/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju',
})
}, [language])
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)

@ -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()

@ -19,9 +19,7 @@ import { useWorkflowStore } from '../../../store'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import type { NodeOutPutVar } from '../../../types'
import type { Node } from 'reactflow'
import { useContext } from 'use-context-selector'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import { useDocLink } from '@/context/i18n'
export type Strategy = {
agent_strategy_provider_name: string
@ -52,7 +50,7 @@ type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const renderI18nObject = useRenderI18nObject()
const workflowStore = useWorkflowStore()
@ -223,11 +221,11 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
title={t('workflow.nodes.agent.strategy.configureTip')}
description={<div className='text-xs text-text-tertiary'>
{t('workflow.nodes.agent.strategy.configureTipDesc')} <br />
<Link href={
locale === LanguagesSupported[1]
? 'https://docs.dify.ai/zh-hans/guides/workflow/node/agent#xuan-ze-agent-ce-le'
: 'https://docs.dify.ai/en/guides/workflow/node/agent#select-an-agent-strategy'
} className='text-text-accent-secondary' target='_blank'>
<Link href={docLink('/guides/workflow/node/agent#select-an-agent-strategy', {
'zh-Hans': '/guides/workflow/node/agent#选择-agent-策略',
'ja-JP': '/guides/workflow/node/agent#エージェント戦略の選択',
})}
className='text-text-accent-secondary' target='_blank'>
{t('workflow.nodes.agent.learnMore')}
</Link>
</div>}

@ -140,6 +140,7 @@ const CodeEditor: FC<Props> = ({
language={languageMap[language] || 'javascript'}
theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
value={outPutValue}
loading={<span className='text-text-primary'>Loading...</span>}
onChange={handleEditorChange}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
options={{

@ -5,6 +5,7 @@ import Input from '@/app/components/base/input'
import { VarType } from '@/app/components/workflow/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { useDocLink } from '@/context/i18n'
type DefaultValueProps = {
forms: DefaultValueForm[]
@ -15,6 +16,7 @@ const DefaultValue = ({
onFormChange,
}: DefaultValueProps) => {
const { t } = useTranslation()
const docLink = useDocLink()
const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => {
return (payload: any) => {
let value
@ -34,7 +36,9 @@ const DefaultValue = ({
{t('workflow.nodes.common.errorHandle.defaultValue.desc')}
&nbsp;
<a
href='https://docs.dify.ai/en/guides/workflow/error-handling/README'
href={docLink('/guides/workflow/error-handling/README', {
'zh-Hans': '/guides/workflow/error-handling/readme',
})}
target='_blank'
className='text-text-accent'
>

@ -1,8 +1,10 @@
import { RiMindMap } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
const FailBranchCard = () => {
const { t } = useTranslation()
const docLink = useDocLink()
return (
<div className='px-4 pt-2'>
@ -17,7 +19,7 @@ const FailBranchCard = () => {
{t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}
&nbsp;
<a
href='https://docs.dify.ai/guides/workflow/error-handling'
href={docLink('/guides/workflow/error-handling/error-type')}
target='_blank'
className='text-text-accent'
>

@ -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<Props> = ({
const handleVarNameChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => {
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)
}

@ -2,12 +2,10 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import VarReferenceVars from './var-reference-vars'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import ListEmpty from '@/app/components/base/list-empty'
import { LanguagesSupported } from '@/i18n/language'
import I18n from '@/context/i18n'
import { useDocLink } from '@/context/i18n'
type Props = {
vars: NodeOutPutVar[]
@ -24,7 +22,7 @@ const VarReferencePopup: FC<Props> = ({
isSupportFileVar = true,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
return (
<div className='space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg' style={{
@ -47,7 +45,12 @@ const VarReferencePopup: FC<Props> = ({
{t('workflow.variableReference.assignedVarsDescription')}
<a target='_blank' rel='noopener noreferrer'
className='text-text-accent-secondary'
href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/guides/workflow/variables#conversation-variables' : `https://docs.dify.ai/${locale.toLowerCase()}/guides/workflow/variables#hui-hua-bian-liang`}>{t('workflow.variableReference.conversationVars')}</a>
href={docLink('/guides/workflow/variables#conversation-variables', {
'zh-Hans': '/guides/workflow/variables#会话变量',
'ja-JP': '/guides/workflow/variables#会話変数',
})}>
{t('workflow.variableReference.conversationVars')}
</a>
</div>}
/>
))

@ -260,6 +260,7 @@ type Props = {
maxHeightClass?: string
onClose?: () => void
onBlur?: () => void
autoFocus?: boolean
}
const VarReferenceVars: FC<Props> = ({
hideSearch,
@ -271,6 +272,7 @@ const VarReferenceVars: FC<Props> = ({
maxHeightClass,
onClose,
onBlur,
autoFocus = true,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
@ -323,7 +325,7 @@ const VarReferenceVars: FC<Props> = ({
onKeyDown={handleKeyDown}
onClear={() => setSearchText('')}
onBlur={onBlur}
autoFocus
autoFocus={autoFocus}
/>
</div>
<div className='relative left-[-4px] h-[0.5px] bg-black/5' style={{

@ -1,14 +1,12 @@
import { useMemo } from 'react'
import { useGetLanguage } from '@/context/i18n'
import { useDocLink, useGetLanguage } from '@/context/i18n'
import { BlockEnum } from '@/app/components/workflow/types'
export const useNodeHelpLink = (nodeType: BlockEnum) => {
const language = useGetLanguage()
const docLink = useDocLink()
const prefixLink = useMemo(() => {
if (language === 'zh_Hans')
return 'https://docs.dify.ai/zh-hans/guides/workflow/node/'
return 'https://docs.dify.ai/en/guides/workflow/node/'
return docLink('/guides/workflow/node/')
}, [language])
const linkMap = useMemo(() => {
if (language === 'zh_Hans') {

@ -21,8 +21,8 @@ import { MittProvider, VisualEditorContextProvider, useMittContext } from './vis
import ErrorMessage from './error-message'
import { useVisualEditorStore } from './visual-editor/store'
import Toast from '@/app/components/base/toast'
import { useGetDocLanguage } from '@/context/i18n'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
import { useDocLink } from '@/context/i18n'
type JsonSchemaConfigProps = {
defaultSchema?: SchemaRoot
@ -53,7 +53,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
onClose,
}) => {
const { t } = useTranslation()
const docLanguage = useGetDocLanguage()
const docLink = useDocLink()
const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor)
const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA)
const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2))
@ -252,7 +252,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
<div className='flex items-center gap-x-2 p-6 pt-5'>
<a
className='flex grow items-center gap-x-1 text-text-accent'
href={`https://docs.dify.ai/${docLanguage}/guides/workflow/structured-outputs`}
href={docLink('/guides/workflow/structured-outputs')}
target='_blank'
rel='noopener noreferrer'
>

@ -3,7 +3,6 @@ import {
useCallback,
useState,
} from 'react'
import { useContext } from 'use-context-selector'
import {
useStoreApi,
} from 'reactflow'
@ -22,13 +21,12 @@ import type {
import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
import { BlockEnum } from '@/app/components/workflow/types'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import { useDocLink } from '@/context/i18n'
import cn from '@/utils/classnames'
const ChatVariablePanel = () => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const docLink = useDocLink()
const store = useStoreApi()
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
@ -139,7 +137,13 @@ const ChatVariablePanel = () => {
<div className='system-2xs-medium-uppercase inline-block rounded-[5px] border border-divider-deep px-[5px] py-[3px] text-text-tertiary'>TIPS</div>
<div className='system-sm-regular mb-4 mt-1 text-text-secondary'>
{t('workflow.chatVariable.panelDescription')}
<a target='_blank' rel='noopener noreferrer' className='text-text-accent' href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/guides/workflow/variables#conversation-variables' : `https://docs.dify.ai/${locale.toLowerCase()}/guides/workflow/variables#hui-hua-bian-liang`}>{t('workflow.chatVariable.docLink')}</a>
<a target='_blank' rel='noopener noreferrer' className='text-text-accent'
href={docLink('/guides/workflow/variables#conversation-variables', {
'zh-Hans': '/guides/workflow/variables#会话变量',
'ja-JP': '/guides/workflow/variables#会話変数',
})}>
{t('workflow.chatVariable.docLink')}
</a>
</div>
<div className='flex items-center gap-2'>
<div className='radius-lg flex flex-col border border-workflow-block-border bg-workflow-block-bg p-3 pb-4 shadow-md'>
@ -166,7 +170,7 @@ const ChatVariablePanel = () => {
</div>
</div>
</div>
<div className='absolute right-[38px] top-[-4px] z-10 h-3 w-3 rotate-45 bg-background-section-burn'/>
<div className='absolute right-[38px] top-[-4px] z-10 h-3 w-3 rotate-45 bg-background-section-burn' />
</div>
</div>
)}

@ -139,7 +139,7 @@ const VariableModal = ({
<div className='flex'>
{
type !== 'number' ? <textarea
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
value={value}
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
onChange={e => setValue(e.target.value)}

@ -28,6 +28,7 @@ import type {
} from '@/types/workflow'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import { hasRetryNode } from '@/app/components/workflow/utils'
import { useDocLink } from '@/context/i18n'
type Props = {
className?: string
@ -65,6 +66,7 @@ const NodePanel: FC<Props> = ({
doSetCollapseState(state)
}, [hideProcessDetail])
const { t } = useTranslation()
const docLink = useDocLink()
const getTime = (time: number) => {
if (time < 1)
@ -195,7 +197,7 @@ const NodePanel: FC<Props> = ({
<StatusContainer status='stopped'>
{nodeInfo.error}
<a
href='https://docs.dify.ai/guides/workflow/error-handling/error-type'
href={docLink('/guides/workflow/error-handling/error-type')}
target='_blank'
className='text-text-accent'
>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save