Compare commits

..

15 Commits

Author SHA1 Message Date
Wu Tianwei
0baccb9e82 chore(version): bump version to 1.7.2 (#23740) 2025-08-11 17:12:44 +08:00
lyzno1
2c81db5a1c feat: enhance GotoAnything UX with @ command selector (#23738) 2025-08-11 15:47:19 +08:00
-LAN-
43411d7a9e chore: remove debug log statements from DifyAPIRepositoryFactory (#23734) 2025-08-11 15:39:20 +08:00
-LAN-
2dbf20a3e9 fix: resolve circular import in AppGenerateEntity (#23731) 2025-08-11 15:38:28 +08:00
-LAN-
aaf9fc1562 fix: add @property decorator to pydantic computed_field for compatibility (#23728) 2025-08-11 15:34:19 +08:00
Wu Tianwei
d30f898274 fix: model selector language undefined error (#23723)
Some checks are pending
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
Check i18n Files and Create PR / check-and-update (push) Waiting to run
2025-08-11 14:39:22 +08:00
NeatGuyCoding
4a72fa6268 fix: Enhance doc_form null checking, exception handling, and rollback logic (#23713) 2025-08-11 13:53:40 +08:00
lyzno1
0c5e66bccb fix: unified error handling for GotoAnything search actions (#23715) 2025-08-11 11:57:06 +08:00
HyaCinth
ff791efe18 fix: Optimize the event handling for inserting variable shortcuts, resolving incorrect blur issues (#22981) (#23707) 2025-08-11 11:08:12 +08:00
NeatGuyCoding
6083b1d618 Feat add testcontainers test for message service (#23703)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-08-11 10:49:32 +08:00
github-actions[bot]
69c3439c3a chore: translate i18n files (#23704)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-08-11 10:35:29 +08:00
crazywoola
7ee170f0a7 Feat node search (#23685)
Co-authored-by: GuanMu <ballmanjq@gmail.com>
Co-authored-by: zhujiruo <zhujiruo@foxmail.com>
Co-authored-by: Matri Qi <matrixdom@126.com>
Co-authored-by: croatialu <wuli.croatia@foxmail.com>
Co-authored-by: HyaCinth <88471803+HyaCiovo@users.noreply.github.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
2025-08-11 10:19:52 +08:00
Yongtao Huang
36b221b170 Fix model_setting_map key mismatch (#23699)
Signed-off-by: Yongtao Huang <yongtaoh2022@gmail.com>
2025-08-11 09:33:26 +08:00
Guangdong Liu
d1fc98200c fix: update HTTP request timeout fields to use Field with default values (#23694) 2025-08-11 09:30:16 +08:00
lyzno1
bb852ef6d2 fix: improve dark mode UI consistency in signin page (#23684)
Some checks are pending
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
2025-08-10 17:21:05 +08:00
85 changed files with 3606 additions and 397 deletions

View File

@@ -330,17 +330,17 @@ class HttpConfig(BaseSettings):
def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]:
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",")
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: Annotated[
PositiveInt, Field(ge=10, description="Maximum connection timeout in seconds for HTTP requests")
] = 10
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: int = Field(
ge=1, description="Maximum connection timeout in seconds for HTTP requests", default=10
)
HTTP_REQUEST_MAX_READ_TIMEOUT: Annotated[
PositiveInt, Field(ge=60, description="Maximum read timeout in seconds for HTTP requests")
] = 60
HTTP_REQUEST_MAX_READ_TIMEOUT: int = Field(
ge=1, description="Maximum read timeout in seconds for HTTP requests", default=60
)
HTTP_REQUEST_MAX_WRITE_TIMEOUT: Annotated[
PositiveInt, Field(ge=10, description="Maximum write timeout in seconds for HTTP requests")
] = 20
HTTP_REQUEST_MAX_WRITE_TIMEOUT: int = Field(
ge=1, description="Maximum write timeout in seconds for HTTP requests", default=20
)
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: PositiveInt = Field(
description="Maximum allowed size in bytes for binary data in HTTP requests",
@@ -655,11 +655,6 @@ class ToolConfig(BaseSettings):
default=3600,
)
TOOL_FILE_MAX_SIZE: PositiveInt = Field(
description="Maximum size in bytes for tool generated files",
default=30 * 1024 * 1024,
)
class MailConfig(BaseSettings):
"""

View File

@@ -144,7 +144,8 @@ class DatabaseConfig(BaseSettings):
default="postgresql",
)
@computed_field
@computed_field # type: ignore[misc]
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
db_extras = (
f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS

View File

@@ -9,7 +9,6 @@ from core.app.app_config.entities import EasyUIBasedAppConfig, WorkflowUIBasedAp
from core.entities.provider_configuration import ProviderModelBundle
from core.file import File, FileUploadConfig
from core.model_runtime.entities.model_entities import AIModelEntity
from core.ops.ops_trace_manager import TraceQueueManager
class InvokeFrom(Enum):
@@ -114,7 +113,8 @@ class AppGenerateEntity(BaseModel):
extras: dict[str, Any] = Field(default_factory=dict)
# tracing instance
trace_manager: Optional[TraceQueueManager] = None
# Using Any to avoid circular import with TraceQueueManager
trace_manager: Optional[Any] = None
class EasyUIBasedAppGenerateEntity(AppGenerateEntity):

View File

@@ -843,7 +843,7 @@ class ProviderConfiguration(BaseModel):
continue
status = ModelStatus.ACTIVE
if m.model in model_setting_map:
if m.model_type in model_setting_map and m.model in model_setting_map[m.model_type]:
model_setting = model_setting_map[m.model_type][m.model]
if model_setting.enabled is False:
status = ModelStatus.DISABLED

View File

@@ -1,50 +1,14 @@
from collections.abc import Generator
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic import BaseModel
from configs import dify_config
from core.plugin.entities.plugin import GenericProviderID, ToolProviderID
from core.plugin.entities.plugin_daemon import PluginBasicBooleanResponse, PluginToolProviderEntity
from core.plugin.impl.base import BasePluginClient
from core.tools.entities.tool_entities import CredentialType, ToolInvokeMessage, ToolParameter
class FileChunk(BaseModel):
"""File chunk buffer for assembling blob data from chunks."""
bytes_written: int = 0
total_length: int
data: bytearray = Field(default_factory=bytearray)
def __iadd__(self, other: bytes) -> "FileChunk":
self.data[self.bytes_written : self.bytes_written + len(other)] = other
self.bytes_written += len(other)
if self.bytes_written > self.total_length:
raise ValueError(f"File chunk is too large which reached the limit of {self.total_length} bytes")
return self
model_config = ConfigDict(arbitrary_types_allowed=True)
@field_validator("total_length")
@classmethod
def validate_total_length(cls, v: int) -> int:
if v <= 0:
raise ValueError("total_length must be positive")
if v > dify_config.TOOL_FILE_MAX_SIZE:
raise ValueError(f"total_length exceeds maximum file size of {dify_config.TOOL_FILE_MAX_SIZE} bytes")
return v
@model_validator(mode="before")
@classmethod
def initialize_data_buffer(cls, values):
if isinstance(values, dict):
if "data" not in values or values["data"] is None:
if "total_length" in values:
values["data"] = bytearray(values["total_length"])
return values
class PluginToolManager(BasePluginClient):
def fetch_tool_providers(self, tenant_id: str) -> list[PluginToolProviderEntity]:
"""
@@ -77,59 +41,6 @@ class PluginToolManager(BasePluginClient):
return response
def _process_blob_chunks(
self,
response: Generator[ToolInvokeMessage, None, None],
chunk_size_limit: int = 8192,
) -> Generator[ToolInvokeMessage, None, None]:
"""
Process blob chunks from tool invocation responses.
Args:
response: Generator yielding ToolInvokeMessage instances
chunk_size_limit: Maximum size for a single chunk (default 8KB)
Yields:
ToolInvokeMessage: Processed messages with complete blobs assembled from chunks
Raises:
ValueError: If chunk or file size limits are exceeded
"""
files: dict[str, FileChunk] = {}
for resp in response:
if resp.type != ToolInvokeMessage.MessageType.BLOB_CHUNK:
yield resp
continue
assert isinstance(resp.message, ToolInvokeMessage.BlobChunkMessage)
# Get blob chunk information
chunk_id = resp.message.id
total_length = resp.message.total_length
blob_data = resp.message.blob
is_end = resp.message.end
# Initialize buffer for this file if it doesn't exist
if chunk_id not in files:
if total_length > dify_config.TOOL_FILE_MAX_SIZE:
raise ValueError(
f"File is too large which reached the limit of {dify_config.TOOL_FILE_MAX_SIZE} bytes"
)
files[chunk_id] = FileChunk(total_length=total_length)
# Append the blob data to the buffer
files[chunk_id] += blob_data
# If this is the final chunk, yield a complete blob message
if is_end:
yield ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB,
message=ToolInvokeMessage.BlobMessage(blob=files[chunk_id].data),
meta=resp.meta,
)
del files[chunk_id]
def fetch_tool_provider(self, tenant_id: str, provider: str) -> PluginToolProviderEntity:
"""
Fetch tool provider for the given tenant and plugin.
@@ -202,8 +113,61 @@ class PluginToolManager(BasePluginClient):
},
)
# Process blob chunks using the handler method
return self._process_blob_chunks(response)
class FileChunk:
"""
Only used for internal processing.
"""
bytes_written: int
total_length: int
data: bytearray
def __init__(self, total_length: int):
self.bytes_written = 0
self.total_length = total_length
self.data = bytearray(total_length)
files: dict[str, FileChunk] = {}
for resp in response:
if resp.type == ToolInvokeMessage.MessageType.BLOB_CHUNK:
assert isinstance(resp.message, ToolInvokeMessage.BlobChunkMessage)
# Get blob chunk information
chunk_id = resp.message.id
total_length = resp.message.total_length
blob_data = resp.message.blob
is_end = resp.message.end
# Initialize buffer for this file if it doesn't exist
if chunk_id not in files:
files[chunk_id] = FileChunk(total_length)
# If this is the final chunk, yield a complete blob message
if is_end:
yield ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB,
message=ToolInvokeMessage.BlobMessage(blob=files[chunk_id].data),
meta=resp.meta,
)
else:
# Check if file is too large (30MB limit)
if files[chunk_id].bytes_written + len(blob_data) > 30 * 1024 * 1024:
# Delete the file if it's too large
del files[chunk_id]
# Skip yielding this message
raise ValueError("File is too large which reached the limit of 30MB")
# Check if single chunk is too large (8KB limit)
if len(blob_data) > 8192:
# Skip yielding this message
raise ValueError("File chunk is too large which reached the limit of 8KB")
# Append the blob data to the buffer
files[chunk_id].data[
files[chunk_id].bytes_written : files[chunk_id].bytes_written + len(blob_data)
] = blob_data
files[chunk_id].bytes_written += len(blob_data)
else:
yield resp
def validate_provider_credentials(
self, tenant_id: str, user_id: str, provider: str, credentials: dict[str, Any]

View File

@@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.7.1"
version = "1.7.2"
requires-python = ">=3.11,<3.13"
dependencies = [
@@ -66,7 +66,7 @@ dependencies = [
"pycryptodome==3.19.1",
"pydantic~=2.11.4",
"pydantic-extra-types~=2.10.3",
"pydantic-settings~=2.10.1",
"pydantic-settings~=2.9.1",
"pyjwt~=2.8.0",
"pypdfium2==4.30.0",
"python-docx~=1.1.0",

View File

@@ -48,7 +48,6 @@ class DifyAPIRepositoryFactory(DifyCoreRepositoryFactory):
RepositoryImportError: If the configured repository cannot be imported or instantiated
"""
class_path = dify_config.API_WORKFLOW_NODE_EXECUTION_REPOSITORY
logger.debug("Creating DifyAPIWorkflowNodeExecutionRepository from: %s", class_path)
try:
repository_class = cls._import_class(class_path)
@@ -86,7 +85,6 @@ class DifyAPIRepositoryFactory(DifyCoreRepositoryFactory):
RepositoryImportError: If the configured repository cannot be imported or instantiated
"""
class_path = dify_config.API_WORKFLOW_RUN_REPOSITORY
logger.debug("Creating APIWorkflowRunRepository from: %s", class_path)
try:
repository_class = cls._import_class(class_path)

View File

@@ -56,19 +56,29 @@ def clean_dataset_task(
documents = db.session.query(Document).where(Document.dataset_id == dataset_id).all()
segments = db.session.query(DocumentSegment).where(DocumentSegment.dataset_id == dataset_id).all()
# Fix: Always clean vector database resources regardless of document existence
# This ensures all 33 vector databases properly drop tables/collections/indices
if doc_form is None:
# Use default paragraph index type for empty datasets to enable vector database cleanup
# Enhanced validation: Check if doc_form is None, empty string, or contains only whitespace
# This ensures all invalid doc_form values are properly handled
if doc_form is None or (isinstance(doc_form, str) and not doc_form.strip()):
# Use default paragraph index type for empty/invalid datasets to enable vector database cleanup
from core.rag.index_processor.constant.index_type import IndexType
doc_form = IndexType.PARAGRAPH_INDEX
logging.info(
click.style(f"No documents found, using default index type for cleanup: {doc_form}", fg="yellow")
click.style(f"Invalid doc_form detected, using default index type for cleanup: {doc_form}", fg="yellow")
)
index_processor = IndexProcessorFactory(doc_form).init_index_processor()
index_processor.clean(dataset, None, with_keywords=True, delete_child_chunks=True)
# Add exception handling around IndexProcessorFactory.clean() to prevent single point of failure
# This ensures Document/Segment deletion can continue even if vector database cleanup fails
try:
index_processor = IndexProcessorFactory(doc_form).init_index_processor()
index_processor.clean(dataset, None, with_keywords=True, delete_child_chunks=True)
logging.info(click.style(f"Successfully cleaned vector database for dataset: {dataset_id}", fg="green"))
except Exception as index_cleanup_error:
logging.exception(click.style(f"Failed to clean vector database for dataset {dataset_id}", fg="red"))
# Continue with document and segment deletion even if vector cleanup fails
logging.info(
click.style(f"Continuing with document and segment deletion for dataset: {dataset_id}", fg="yellow")
)
if documents is None or len(documents) == 0:
logging.info(click.style(f"No documents found for dataset: {dataset_id}", fg="green"))
@@ -128,6 +138,14 @@ def clean_dataset_task(
click.style(f"Cleaned dataset when dataset deleted: {dataset_id} latency: {end_at - start_at}", fg="green")
)
except Exception:
# Add rollback to prevent dirty session state in case of exceptions
# This ensures the database session is properly cleaned up
try:
db.session.rollback()
logging.info(click.style(f"Rolled back database session for dataset: {dataset_id}", fg="yellow"))
except Exception as rollback_error:
logging.exception("Failed to rollback database session")
logging.exception("Cleaned dataset when dataset deleted failed")
finally:
db.session.close()

View File

@@ -0,0 +1,775 @@
from unittest.mock import patch
import pytest
from faker import Faker
from models.model import MessageFeedback
from services.app_service import AppService
from services.errors.message import (
FirstMessageNotExistsError,
LastMessageNotExistsError,
MessageNotExistsError,
SuggestedQuestionsAfterAnswerDisabledError,
)
from services.message_service import MessageService
class TestMessageService:
"""Integration tests for MessageService using testcontainers."""
@pytest.fixture
def mock_external_service_dependencies(self):
"""Mock setup for external service dependencies."""
with (
patch("services.account_service.FeatureService") as mock_account_feature_service,
patch("services.message_service.ModelManager") as mock_model_manager,
patch("services.message_service.WorkflowService") as mock_workflow_service,
patch("services.message_service.AdvancedChatAppConfigManager") as mock_app_config_manager,
patch("services.message_service.LLMGenerator") as mock_llm_generator,
patch("services.message_service.TraceQueueManager") as mock_trace_manager_class,
patch("services.message_service.TokenBufferMemory") as mock_token_buffer_memory,
):
# Setup default mock returns
mock_account_feature_service.get_features.return_value.billing.enabled = False
# Mock ModelManager
mock_model_instance = mock_model_manager.return_value.get_default_model_instance.return_value
mock_model_instance.get_tts_voices.return_value = [{"value": "test-voice"}]
# Mock get_model_instance method as well
mock_model_manager.return_value.get_model_instance.return_value = mock_model_instance
# Mock WorkflowService
mock_workflow = mock_workflow_service.return_value.get_published_workflow.return_value
mock_workflow_service.return_value.get_draft_workflow.return_value = mock_workflow
# Mock AdvancedChatAppConfigManager
mock_app_config = mock_app_config_manager.get_app_config.return_value
mock_app_config.additional_features.suggested_questions_after_answer = True
# Mock LLMGenerator
mock_llm_generator.generate_suggested_questions_after_answer.return_value = ["Question 1", "Question 2"]
# Mock TraceQueueManager
mock_trace_manager_instance = mock_trace_manager_class.return_value
# Mock TokenBufferMemory
mock_memory_instance = mock_token_buffer_memory.return_value
mock_memory_instance.get_history_prompt_text.return_value = "Mocked history prompt"
yield {
"account_feature_service": mock_account_feature_service,
"model_manager": mock_model_manager,
"workflow_service": mock_workflow_service,
"app_config_manager": mock_app_config_manager,
"llm_generator": mock_llm_generator,
"trace_manager_class": mock_trace_manager_class,
"trace_manager_instance": mock_trace_manager_instance,
"token_buffer_memory": mock_token_buffer_memory,
# "current_user": mock_current_user,
}
def _create_test_app_and_account(self, db_session_with_containers, mock_external_service_dependencies):
"""
Helper method to create a test app and account for testing.
Args:
db_session_with_containers: Database session from testcontainers infrastructure
mock_external_service_dependencies: Mock dependencies
Returns:
tuple: (app, account) - Created app and account instances
"""
fake = Faker()
# Setup mocks for account creation
mock_external_service_dependencies[
"account_feature_service"
].get_system_features.return_value.is_allow_register = True
# Create account and tenant first
from services.account_service import AccountService, TenantService
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=fake.password(length=12),
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
tenant = account.current_tenant
# Setup app creation arguments
app_args = {
"name": fake.company(),
"description": fake.text(max_nb_chars=100),
"mode": "advanced-chat", # Use advanced-chat mode to use mocked workflow
"icon_type": "emoji",
"icon": "🤖",
"icon_background": "#FF6B6B",
"api_rph": 100,
"api_rpm": 10,
}
# Create app
app_service = AppService()
app = app_service.create_app(tenant.id, app_args, account)
# Setup current_user mock
self._mock_current_user(mock_external_service_dependencies, account.id, tenant.id)
return app, account
def _mock_current_user(self, mock_external_service_dependencies, account_id, tenant_id):
"""
Helper method to mock the current user for testing.
"""
# mock_external_service_dependencies["current_user"].id = account_id
# mock_external_service_dependencies["current_user"].current_tenant_id = tenant_id
def _create_test_conversation(self, app, account, fake):
"""
Helper method to create a test conversation with all required fields.
"""
from extensions.ext_database import db
from models.model import Conversation
conversation = Conversation(
app_id=app.id,
app_model_config_id=None,
model_provider=None,
model_id="",
override_model_configs=None,
mode=app.mode,
name=fake.sentence(),
inputs={},
introduction="",
system_instruction="",
system_instruction_tokens=0,
status="normal",
invoke_from="console",
from_source="console",
from_end_user_id=None,
from_account_id=account.id,
)
db.session.add(conversation)
db.session.flush()
return conversation
def _create_test_message(self, app, conversation, account, fake):
"""
Helper method to create a test message with all required fields.
"""
import json
from extensions.ext_database import db
from models.model import Message
message = Message(
app_id=app.id,
model_provider=None,
model_id="",
override_model_configs=None,
conversation_id=conversation.id,
inputs={},
query=fake.sentence(),
message=json.dumps([{"role": "user", "text": fake.sentence()}]),
message_tokens=0,
message_unit_price=0,
message_price_unit=0.001,
answer=fake.text(max_nb_chars=200),
answer_tokens=0,
answer_unit_price=0,
answer_price_unit=0.001,
parent_message_id=None,
provider_response_latency=0,
total_price=0,
currency="USD",
invoke_from="console",
from_source="console",
from_end_user_id=None,
from_account_id=account.id,
)
db.session.add(message)
db.session.commit()
return message
def test_pagination_by_first_id_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful pagination by first ID.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and multiple messages
conversation = self._create_test_conversation(app, account, fake)
messages = []
for i in range(5):
message = self._create_test_message(app, conversation, account, fake)
messages.append(message)
# Test pagination by first ID
result = MessageService.pagination_by_first_id(
app_model=app,
user=account,
conversation_id=conversation.id,
first_id=messages[2].id, # Use middle message as first_id
limit=2,
order="asc",
)
# Verify results
assert result.limit == 2
assert len(result.data) == 2
# total 5, from the middle, no more
assert result.has_more is False
# Verify messages are in ascending order
assert result.data[0].created_at <= result.data[1].created_at
def test_pagination_by_first_id_no_user(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test pagination by first ID when no user is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Test pagination with no user
result = MessageService.pagination_by_first_id(
app_model=app, user=None, conversation_id=fake.uuid4(), first_id=None, limit=10
)
# Verify empty result
assert result.limit == 10
assert len(result.data) == 0
assert result.has_more is False
def test_pagination_by_first_id_no_conversation_id(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test pagination by first ID when no conversation ID is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Test pagination with no conversation ID
result = MessageService.pagination_by_first_id(
app_model=app, user=account, conversation_id="", first_id=None, limit=10
)
# Verify empty result
assert result.limit == 10
assert len(result.data) == 0
assert result.has_more is False
def test_pagination_by_first_id_invalid_first_id(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test pagination by first ID with invalid first_id.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
self._create_test_message(app, conversation, account, fake)
# Test pagination with invalid first_id
with pytest.raises(FirstMessageNotExistsError):
MessageService.pagination_by_first_id(
app_model=app,
user=account,
conversation_id=conversation.id,
first_id=fake.uuid4(), # Non-existent message ID
limit=10,
)
def test_pagination_by_last_id_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful pagination by last ID.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and multiple messages
conversation = self._create_test_conversation(app, account, fake)
messages = []
for i in range(5):
message = self._create_test_message(app, conversation, account, fake)
messages.append(message)
# Test pagination by last ID
result = MessageService.pagination_by_last_id(
app_model=app,
user=account,
last_id=messages[2].id, # Use middle message as last_id
limit=2,
conversation_id=conversation.id,
)
# Verify results
assert result.limit == 2
assert len(result.data) == 2
# total 5, from the middle, no more
assert result.has_more is False
# Verify messages are in descending order
assert result.data[0].created_at >= result.data[1].created_at
def test_pagination_by_last_id_with_include_ids(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test pagination by last ID with include_ids filter.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and multiple messages
conversation = self._create_test_conversation(app, account, fake)
messages = []
for i in range(5):
message = self._create_test_message(app, conversation, account, fake)
messages.append(message)
# Test pagination with include_ids
include_ids = [messages[0].id, messages[1].id, messages[2].id]
result = MessageService.pagination_by_last_id(
app_model=app, user=account, last_id=messages[1].id, limit=2, include_ids=include_ids
)
# Verify results
assert result.limit == 2
assert len(result.data) <= 2
# Verify all returned messages are in include_ids
for message in result.data:
assert message.id in include_ids
def test_pagination_by_last_id_no_user(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test pagination by last ID when no user is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Test pagination with no user
result = MessageService.pagination_by_last_id(app_model=app, user=None, last_id=None, limit=10)
# Verify empty result
assert result.limit == 10
assert len(result.data) == 0
assert result.has_more is False
def test_pagination_by_last_id_invalid_last_id(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test pagination by last ID with invalid last_id.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
self._create_test_message(app, conversation, account, fake)
# Test pagination with invalid last_id
with pytest.raises(LastMessageNotExistsError):
MessageService.pagination_by_last_id(
app_model=app,
user=account,
last_id=fake.uuid4(), # Non-existent message ID
limit=10,
conversation_id=conversation.id,
)
def test_create_feedback_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful creation of feedback.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Create feedback
rating = "like"
content = fake.text(max_nb_chars=100)
feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=rating, content=content
)
# Verify feedback was created correctly
assert feedback.app_id == app.id
assert feedback.conversation_id == conversation.id
assert feedback.message_id == message.id
assert feedback.rating == rating
assert feedback.content == content
assert feedback.from_source == "admin"
assert feedback.from_account_id == account.id
assert feedback.from_end_user_id is None
def test_create_feedback_no_user(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test creating feedback when no user is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Test creating feedback with no user
with pytest.raises(ValueError, match="user cannot be None"):
MessageService.create_feedback(
app_model=app, message_id=message.id, user=None, rating="like", content=fake.text(max_nb_chars=100)
)
def test_create_feedback_update_existing(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test updating existing feedback.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Create initial feedback
initial_rating = "like"
initial_content = fake.text(max_nb_chars=100)
feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=initial_rating, content=initial_content
)
# Update feedback
updated_rating = "dislike"
updated_content = fake.text(max_nb_chars=100)
updated_feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=updated_rating, content=updated_content
)
# Verify feedback was updated correctly
assert updated_feedback.id == feedback.id
assert updated_feedback.rating == updated_rating
assert updated_feedback.content == updated_content
assert updated_feedback.rating != initial_rating
assert updated_feedback.content != initial_content
def test_create_feedback_delete_existing(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test deleting existing feedback by setting rating to None.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Create initial feedback
feedback = MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating="like", content=fake.text(max_nb_chars=100)
)
# Delete feedback by setting rating to None
MessageService.create_feedback(app_model=app, message_id=message.id, user=account, rating=None, content=None)
# Verify feedback was deleted
from extensions.ext_database import db
deleted_feedback = db.session.query(MessageFeedback).filter(MessageFeedback.id == feedback.id).first()
assert deleted_feedback is None
def test_create_feedback_no_rating_when_not_exists(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test creating feedback with no rating when feedback doesn't exist.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Test creating feedback with no rating when no feedback exists
with pytest.raises(ValueError, match="rating cannot be None when feedback not exists"):
MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating=None, content=None
)
def test_get_all_messages_feedbacks_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful retrieval of all message feedbacks.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create multiple conversations and messages with feedbacks
feedbacks = []
for i in range(3):
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
feedback = MessageService.create_feedback(
app_model=app,
message_id=message.id,
user=account,
rating="like" if i % 2 == 0 else "dislike",
content=f"Feedback {i}: {fake.text(max_nb_chars=50)}",
)
feedbacks.append(feedback)
# Get all feedbacks
result = MessageService.get_all_messages_feedbacks(app, page=1, limit=10)
# Verify results
assert len(result) == 3
# Verify feedbacks are ordered by created_at desc
for i in range(len(result) - 1):
assert result[i]["created_at"] >= result[i + 1]["created_at"]
def test_get_all_messages_feedbacks_pagination(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test pagination of message feedbacks.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create multiple conversations and messages with feedbacks
for i in range(5):
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
MessageService.create_feedback(
app_model=app, message_id=message.id, user=account, rating="like", content=f"Feedback {i}"
)
# Get feedbacks with pagination
result_page_1 = MessageService.get_all_messages_feedbacks(app, page=1, limit=3)
result_page_2 = MessageService.get_all_messages_feedbacks(app, page=2, limit=3)
# Verify pagination results
assert len(result_page_1) == 3
assert len(result_page_2) == 2
# Verify no overlap between pages
page_1_ids = {feedback["id"] for feedback in result_page_1}
page_2_ids = {feedback["id"] for feedback in result_page_2}
assert len(page_1_ids.intersection(page_2_ids)) == 0
def test_get_message_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful retrieval of message.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Get message
retrieved_message = MessageService.get_message(app_model=app, user=account, message_id=message.id)
# Verify message was retrieved correctly
assert retrieved_message.id == message.id
assert retrieved_message.app_id == app.id
assert retrieved_message.conversation_id == conversation.id
assert retrieved_message.from_source == "console"
assert retrieved_message.from_account_id == account.id
def test_get_message_not_exists(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test getting message that doesn't exist.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Test getting non-existent message
with pytest.raises(MessageNotExistsError):
MessageService.get_message(app_model=app, user=account, message_id=fake.uuid4())
def test_get_message_wrong_user(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test getting message with wrong user (different account).
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Create another account
from services.account_service import AccountService, TenantService
other_account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=fake.password(length=12),
)
TenantService.create_owner_tenant_if_not_exist(other_account, name=fake.company())
# Test getting message with different user
with pytest.raises(MessageNotExistsError):
MessageService.get_message(app_model=app, user=other_account, message_id=message.id)
def test_get_suggested_questions_after_answer_success(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test successful generation of suggested questions after answer.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Mock the LLMGenerator to return specific questions
mock_questions = ["What is AI?", "How does machine learning work?", "Tell me about neural networks"]
mock_external_service_dependencies[
"llm_generator"
].generate_suggested_questions_after_answer.return_value = mock_questions
# Get suggested questions
from core.app.entities.app_invoke_entities import InvokeFrom
result = MessageService.get_suggested_questions_after_answer(
app_model=app, user=account, message_id=message.id, invoke_from=InvokeFrom.SERVICE_API
)
# Verify results
assert result == mock_questions
# Verify LLMGenerator was called
mock_external_service_dependencies[
"llm_generator"
].generate_suggested_questions_after_answer.assert_called_once()
# Verify TraceQueueManager was called
mock_external_service_dependencies["trace_manager_instance"].add_trace_task.assert_called_once()
def test_get_suggested_questions_after_answer_no_user(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test getting suggested questions when no user is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Test getting suggested questions with no user
from core.app.entities.app_invoke_entities import InvokeFrom
with pytest.raises(ValueError, match="user cannot be None"):
MessageService.get_suggested_questions_after_answer(
app_model=app, user=None, message_id=message.id, invoke_from=InvokeFrom.SERVICE_API
)
def test_get_suggested_questions_after_answer_disabled(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test getting suggested questions when feature is disabled.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Mock the feature to be disabled
mock_external_service_dependencies[
"app_config_manager"
].get_app_config.return_value.additional_features.suggested_questions_after_answer = False
# Test getting suggested questions when feature is disabled
from core.app.entities.app_invoke_entities import InvokeFrom
with pytest.raises(SuggestedQuestionsAfterAnswerDisabledError):
MessageService.get_suggested_questions_after_answer(
app_model=app, user=account, message_id=message.id, invoke_from=InvokeFrom.SERVICE_API
)
def test_get_suggested_questions_after_answer_no_workflow(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test getting suggested questions when no workflow exists.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Mock no workflow
mock_external_service_dependencies["workflow_service"].return_value.get_published_workflow.return_value = None
# Get suggested questions (should return empty list)
from core.app.entities.app_invoke_entities import InvokeFrom
result = MessageService.get_suggested_questions_after_answer(
app_model=app, user=account, message_id=message.id, invoke_from=InvokeFrom.SERVICE_API
)
# Verify empty result
assert result == []
def test_get_suggested_questions_after_answer_debugger_mode(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test getting suggested questions in debugger mode.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Create a conversation and message
conversation = self._create_test_conversation(app, account, fake)
message = self._create_test_message(app, conversation, account, fake)
# Mock questions
mock_questions = ["Debug question 1", "Debug question 2"]
mock_external_service_dependencies[
"llm_generator"
].generate_suggested_questions_after_answer.return_value = mock_questions
# Get suggested questions in debugger mode
from core.app.entities.app_invoke_entities import InvokeFrom
result = MessageService.get_suggested_questions_after_answer(
app_model=app, user=account, message_id=message.id, invoke_from=InvokeFrom.DEBUGGER
)
# Verify results
assert result == mock_questions
# Verify draft workflow was used instead of published workflow
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app_model=app
)
# Verify TraceQueueManager was called
mock_external_service_dependencies["trace_manager_instance"].add_trace_task.assert_called_once()

View File

@@ -1,235 +0,0 @@
import unittest
from unittest.mock import patch
import pytest
from core.plugin.impl.tool import FileChunk, PluginToolManager
from core.tools.entities.tool_entities import ToolInvokeMessage
class TestFileChunk(unittest.TestCase):
def test_file_chunk_creation(self):
"""Test FileChunk creation with specified total length."""
chunk = FileChunk(total_length=1024)
assert chunk.total_length == 1024
assert chunk.bytes_written == 0
assert len(chunk.data) == 1024
assert isinstance(chunk.data, bytearray)
def test_file_chunk_pydantic_model(self):
"""Test FileChunk as a Pydantic model."""
chunk = FileChunk(total_length=512, bytes_written=100, data=bytearray(512))
assert chunk.total_length == 512
assert chunk.bytes_written == 100
assert len(chunk.data) == 512
class TestBlobChunkProcessing(unittest.TestCase):
def setUp(self):
self.manager = PluginToolManager()
def test_process_non_blob_chunk_messages(self):
"""Test that non-blob chunk messages are passed through unchanged."""
# Create test messages
text_message = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.TEXT, message=ToolInvokeMessage.TextMessage(text="Test message")
)
def response_generator():
yield text_message
# Process the response
result = list(self.manager._process_blob_chunks(response_generator()))
assert len(result) == 1
assert result[0] == text_message
def test_process_single_blob_chunk(self):
"""Test processing a complete blob in a single chunk (marked as end)."""
test_data = b"Test file content"
# Create a blob chunk message marked as end
chunk_message = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=0, total_length=len(test_data), blob=test_data, end=True
),
meta={"test": "meta"},
)
def response_generator():
yield chunk_message
# Process the response
result = list(self.manager._process_blob_chunks(response_generator()))
assert len(result) == 1
assert result[0].type == ToolInvokeMessage.MessageType.BLOB
assert isinstance(result[0].message, ToolInvokeMessage.BlobMessage)
# The blob should be the complete file buffer, not just the chunk data
assert len(result[0].message.blob) == len(test_data)
assert result[0].meta == {"test": "meta"}
def test_process_multiple_blob_chunks(self):
"""Test assembling a blob from multiple chunks."""
chunk1_data = b"First part"
chunk2_data = b" Second part"
total_data = chunk1_data + chunk2_data
# Create multiple chunk messages
chunk1 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=0, total_length=len(total_data), blob=chunk1_data, end=False
),
)
chunk2 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=1, total_length=len(total_data), blob=chunk2_data, end=True
),
)
def response_generator():
yield chunk1
yield chunk2
# Process the response
result = list(self.manager._process_blob_chunks(response_generator()))
# Should only yield one complete blob message
assert len(result) == 1
assert result[0].type == ToolInvokeMessage.MessageType.BLOB
assert isinstance(result[0].message, ToolInvokeMessage.BlobMessage)
assert result[0].message.blob[: len(total_data)] == total_data
def test_chunk_size_limit_exceeded(self):
"""Test that chunks exceeding size limit raise an error."""
# Create a chunk that exceeds the 12KB limit
oversized_data = b"x" * 12222 # 12KB
chunk_message = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=0, total_length=10000, blob=oversized_data, end=False
),
)
def response_generator():
yield chunk_message
# Should raise ValueError for oversized chunk
with pytest.raises(ValueError) as exc_info:
list(self.manager._process_blob_chunks(response_generator()))
assert "File chunk is too large" in str(exc_info.value)
assert "10000 bytes" in str(exc_info.value)
@patch("core.plugin.impl.tool.dify_config")
def test_file_size_limit_exceeded(self, mock_config):
"""Test that files exceeding total size limit raise an error."""
mock_config.TOOL_FILE_MAX_SIZE = 1024 # Set limit to 1KB
# Create chunks that together exceed the limit
chunk1 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=0, total_length=2000, blob=b"x" * 600, end=False
),
)
chunk2 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=1, total_length=2000, blob=b"x" * 600, end=False
),
)
def response_generator():
yield chunk1
yield chunk2
# Process first chunk successfully, second should fail
with pytest.raises(ValueError) as exc_info:
list(self.manager._process_blob_chunks(response_generator()))
assert "File is too large" in str(exc_info.value)
assert "1024 bytes" in str(exc_info.value)
def test_multiple_files_concurrent_processing(self):
"""Test processing chunks from multiple files concurrently."""
# Create chunks for two different files
file1_chunk1 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file1", sequence=0, total_length=10, blob=b"File1 data", end=False
),
)
file2_chunk1 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(
id="file2", sequence=0, total_length=10, blob=b"File2 data", end=False
),
)
file1_chunk2 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(id="file1", sequence=1, total_length=10, blob=b"", end=True),
)
file2_chunk2 = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(id="file2", sequence=1, total_length=10, blob=b"", end=True),
)
def response_generator():
yield file1_chunk1
yield file2_chunk1
yield file1_chunk2
yield file2_chunk2
# Process the response
result = list(self.manager._process_blob_chunks(response_generator()))
# Should get two complete blobs
assert len(result) == 2
assert all(r.type == ToolInvokeMessage.MessageType.BLOB for r in result)
def test_mixed_message_types(self):
"""Test processing a mix of blob chunks and other message types."""
text_msg = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.TEXT, message=ToolInvokeMessage.TextMessage(text="Status update")
)
chunk_msg = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.BLOB_CHUNK,
message=ToolInvokeMessage.BlobChunkMessage(id="file1", sequence=0, total_length=4, blob=b"Data", end=True),
)
# Use LOG message type with ERROR status instead of non-existent ERROR message type
error_msg = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.LOG,
message=ToolInvokeMessage.LogMessage(
id="error1",
label="Error Log",
status=ToolInvokeMessage.LogMessage.LogStatus.ERROR,
data={"error": "Test error"},
),
)
def response_generator():
yield text_msg
yield chunk_msg
yield error_msg
# Process the response
result = list(self.manager._process_blob_chunks(response_generator()))
assert len(result) == 3
assert result[0].type == ToolInvokeMessage.MessageType.TEXT
assert result[1].type == ToolInvokeMessage.MessageType.BLOB
assert result[2].type == ToolInvokeMessage.MessageType.LOG

10
api/uv.lock generated
View File

@@ -1236,7 +1236,7 @@ wheels = [
[[package]]
name = "dify-api"
version = "1.7.1"
version = "1.7.2"
source = { virtual = "." }
dependencies = [
{ name = "arize-phoenix-otel" },
@@ -1486,7 +1486,7 @@ requires-dist = [
{ name = "pycryptodome", specifier = "==3.19.1" },
{ name = "pydantic", specifier = "~=2.11.4" },
{ name = "pydantic-extra-types", specifier = "~=2.10.3" },
{ name = "pydantic-settings", specifier = "~=2.10.1" },
{ name = "pydantic-settings", specifier = "~=2.9.1" },
{ name = "pyjwt", specifier = "~=2.8.0" },
{ name = "pypdfium2", specifier = "==4.30.0" },
{ name = "python-docx", specifier = "~=1.1.0" },
@@ -4474,16 +4474,16 @@ wheels = [
[[package]]
name = "pydantic-settings"
version = "2.10.1"
version = "2.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
]
[[package]]

View File

@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.7.1
image: langgenius/dify-api:1.7.2
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.7.1
image: langgenius/dify-api:1.7.2
restart: always
environment:
# Use the shared environment variables.
@@ -58,7 +58,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.7.1
image: langgenius/dify-api:1.7.2
restart: always
environment:
# Use the shared environment variables.
@@ -76,7 +76,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.7.1
image: langgenius/dify-web:1.7.2
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

View File

@@ -567,7 +567,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.7.1
image: langgenius/dify-api:1.7.2
restart: always
environment:
# Use the shared environment variables.
@@ -596,7 +596,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.7.1
image: langgenius/dify-api:1.7.2
restart: always
environment:
# Use the shared environment variables.
@@ -623,7 +623,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.7.1
image: langgenius/dify-api:1.7.2
restart: always
environment:
# Use the shared environment variables.
@@ -641,7 +641,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.7.1
image: langgenius/dify-web:1.7.2
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}

View File

@@ -0,0 +1,197 @@
/**
* Test GotoAnything search error handling mechanisms
*
* Main validations:
* 1. @plugin search error handling when API fails
* 2. Regular search (without @prefix) error handling when API fails
* 3. Verify consistent error handling across different search types
* 4. Ensure errors don't propagate to UI layer causing "search failed"
*/
import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
import { postMarketplace } from '@/service/base'
import { fetchAppList } from '@/service/apps'
import { fetchDatasets } from '@/service/datasets'
// Mock API functions
jest.mock('@/service/base', () => ({
postMarketplace: jest.fn(),
}))
jest.mock('@/service/apps', () => ({
fetchAppList: jest.fn(),
}))
jest.mock('@/service/datasets', () => ({
fetchDatasets: jest.fn(),
}))
const mockPostMarketplace = postMarketplace as jest.MockedFunction<typeof postMarketplace>
const mockFetchAppList = fetchAppList as jest.MockedFunction<typeof fetchAppList>
const mockFetchDatasets = fetchDatasets as jest.MockedFunction<typeof fetchDatasets>
describe('GotoAnything Search Error Handling', () => {
beforeEach(() => {
jest.clearAllMocks()
// Suppress console.warn for clean test output
jest.spyOn(console, 'warn').mockImplementation(() => {
// Suppress console.warn for clean test output
})
})
afterEach(() => {
jest.restoreAllMocks()
})
describe('@plugin search error handling', () => {
it('should return empty array when API fails instead of throwing error', async () => {
// Mock marketplace API failure (403 permission denied)
mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden'))
const pluginAction = Actions.plugin
// Directly call plugin action's search method
const result = await pluginAction.search('@plugin', 'test', 'en')
// Should return empty array instead of throwing error
expect(result).toEqual([])
expect(mockPostMarketplace).toHaveBeenCalledWith('/plugins/search/advanced', {
body: {
page: 1,
page_size: 10,
query: 'test',
type: 'plugin',
},
})
})
it('should return empty array when user has no plugin data', async () => {
// Mock marketplace returning empty data
mockPostMarketplace.mockResolvedValue({
data: { plugins: [] },
})
const pluginAction = Actions.plugin
const result = await pluginAction.search('@plugin', '', 'en')
expect(result).toEqual([])
})
it('should return empty array when API returns unexpected data structure', async () => {
// Mock API returning unexpected data structure
mockPostMarketplace.mockResolvedValue({
data: null,
})
const pluginAction = Actions.plugin
const result = await pluginAction.search('@plugin', 'test', 'en')
expect(result).toEqual([])
})
})
describe('Other search types error handling', () => {
it('@app search should return empty array when API fails', async () => {
// Mock app API failure
mockFetchAppList.mockRejectedValue(new Error('API Error'))
const appAction = Actions.app
const result = await appAction.search('@app', 'test', 'en')
expect(result).toEqual([])
})
it('@knowledge search should return empty array when API fails', async () => {
// Mock knowledge API failure
mockFetchDatasets.mockRejectedValue(new Error('API Error'))
const knowledgeAction = Actions.knowledge
const result = await knowledgeAction.search('@knowledge', 'test', 'en')
expect(result).toEqual([])
})
})
describe('Unified search entry error handling', () => {
it('regular search (without @prefix) should return successful results even when partial APIs fail', async () => {
// Set app and knowledge success, plugin failure
mockFetchAppList.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed'))
const result = await searchAnything('en', 'test')
// Should return successful results even if plugin search fails
expect(result).toEqual([])
expect(console.warn).toHaveBeenCalledWith('Plugin search failed:', expect.any(Error))
})
it('@plugin dedicated search should return empty array when API fails', async () => {
// Mock plugin API failure
mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable'))
const pluginAction = Actions.plugin
const result = await searchAnything('en', '@plugin test', pluginAction)
// Should return empty array instead of throwing error
expect(result).toEqual([])
})
it('@app dedicated search should return empty array when API fails', async () => {
// Mock app API failure
mockFetchAppList.mockRejectedValue(new Error('App service unavailable'))
const appAction = Actions.app
const result = await searchAnything('en', '@app test', appAction)
expect(result).toEqual([])
})
})
describe('Error handling consistency validation', () => {
it('all search types should return empty array when encountering errors', async () => {
// Mock all APIs to fail
mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed'))
mockFetchAppList.mockRejectedValue(new Error('App API failed'))
mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed'))
const actions = [
{ name: '@plugin', action: Actions.plugin },
{ name: '@app', action: Actions.app },
{ name: '@knowledge', action: Actions.knowledge },
]
for (const { name, action } of actions) {
const result = await action.search(name, 'test', 'en')
expect(result).toEqual([])
}
})
})
describe('Edge case testing', () => {
it('empty search term should be handled properly', async () => {
mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } })
const result = await searchAnything('en', '@plugin ', Actions.plugin)
expect(result).toEqual([])
})
it('network timeout should be handled correctly', async () => {
const timeoutError = new Error('Network timeout')
timeoutError.name = 'TimeoutError'
mockPostMarketplace.mockRejectedValue(timeoutError)
const result = await searchAnything('en', '@plugin test', Actions.plugin)
expect(result).toEqual([])
})
it('JSON parsing errors should be handled correctly', async () => {
const parseError = new SyntaxError('Unexpected token in JSON')
mockPostMarketplace.mockRejectedValue(parseError)
const result = await searchAnything('en', '@plugin test', Actions.plugin)
expect(result).toEqual([])
})
})
})

View File

@@ -8,6 +8,7 @@ import Header from '@/app/components/header'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
import { ModalContextProvider } from '@/context/modal-context'
import GotoAnything from '@/app/components/goto-anything'
const Layout = ({ children }: { children: ReactNode }) => {
return (
@@ -22,6 +23,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<Header />
</HeaderWrapper>
{children}
<GotoAnything />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>

View File

@@ -32,7 +32,7 @@ export type InputProps = {
unit?: string
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & VariantProps<typeof inputVariants>
const Input = ({
const Input = React.forwardRef<HTMLInputElement, InputProps>(({
size,
disabled,
destructive,
@@ -47,12 +47,13 @@ const Input = ({
onChange = noop,
unit,
...props
}: InputProps) => {
}, ref) => {
const { t } = useTranslation()
return (
<div className={cn('relative w-full', wrapperClassName)}>
{showLeftIcon && <RiSearchLine className={cn('absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-components-input-text-placeholder')} />}
<input
ref={ref}
style={styleCss}
className={cn(
'w-full appearance-none border border-transparent bg-components-input-bg-normal py-[7px] 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',
@@ -92,6 +93,8 @@ const Input = ({
}
</div>
)
}
})
Input.displayName = 'Input'
export default Input

View File

@@ -0,0 +1,61 @@
'use client'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { GlobeAltIcon } from '@heroicons/react/24/outline'
type ISelectProps = {
items: Array<{ value: string; name: string }>
value?: string
className?: string
onChange?: (value: string) => void
}
export default function LocaleSigninSelect({
items,
value,
onChange,
}: ISelectProps) {
const item = items.filter(item => item.value === value)[0]
return (
<div className="w-56 text-right">
<Menu as="div" className="relative inline-block text-left">
<div>
<MenuButton className="h-[44px]justify-center inline-flex w-full items-center rounded-lg border border-components-button-secondary-border px-[10px] py-[6px] text-[13px] font-medium text-text-primary hover:bg-state-base-hover">
<GlobeAltIcon className="mr-1 h-5 w-5" aria-hidden="true" />
{item?.name}
</MenuButton>
</div>
<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="absolute right-0 z-10 mt-2 w-[200px] origin-top-right divide-y divide-divider-regular rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg focus:outline-none">
<div className="px-1 py-1 ">
{items.map((item) => {
return <MenuItem key={item.value}>
<button
className={'group flex w-full items-center rounded-lg px-3 py-2 text-sm text-text-secondary data-[active]:bg-state-base-hover'}
onClick={(evt) => {
evt.preventDefault()
onChange && onChange(item.value)
}}
>
{item.name}
</button>
</MenuItem>
})}
</div>
</MenuItems>
</Transition>
</Menu>
</div>
)
}

View File

@@ -0,0 +1,58 @@
import type { ActionItem, AppSearchResult } from './types'
import type { App } from '@/types/app'
import { fetchAppList } from '@/service/apps'
import AppIcon from '../../base/app-icon'
import { AppTypeIcon } from '../../app/type-selector'
import { getRedirectionPath } from '@/utils/app-redirection'
const parser = (apps: App[]): AppSearchResult[] => {
return apps.map(app => ({
id: app.id,
title: app.name,
description: app.description,
type: 'app' as const,
path: getRedirectionPath(true, {
id: app.id,
mode: app.mode,
}),
icon: (
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={app.icon_type}
icon={app.icon}
background={app.icon_background}
imageUrl={app.icon_url}
/>
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-[4px] border border-divider-regular outline outline-components-panel-on-panel-item-bg'
className='h-3 w-3' type={app.mode} />
</div>
),
data: app,
}))
}
export const appAction: ActionItem = {
key: '@app',
shortcut: '@app',
title: 'Search Applications',
description: 'Search and navigate to your applications',
// action,
search: async (_, searchTerm = '', _locale) => {
try {
const response = await fetchAppList({
url: 'apps',
params: {
page: 1,
name: searchTerm,
},
})
const apps = response?.data || []
return parser(apps)
}
catch (error) {
console.warn('App search failed:', error)
return []
}
},
}

View File

@@ -0,0 +1,74 @@
import { appAction } from './app'
import { knowledgeAction } from './knowledge'
import { pluginAction } from './plugin'
import { workflowNodesAction } from './workflow-nodes'
import type { ActionItem, SearchResult } from './types'
export const Actions = {
app: appAction,
knowledge: knowledgeAction,
plugin: pluginAction,
node: workflowNodesAction,
}
export const searchAnything = async (
locale: string,
query: string,
actionItem?: ActionItem,
): Promise<SearchResult[]> => {
if (actionItem) {
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
try {
return await actionItem.search(query, searchTerm, locale)
}
catch (error) {
console.warn(`Search failed for ${actionItem.key}:`, error)
return []
}
}
if (query.startsWith('@'))
return []
// Use Promise.allSettled to handle partial failures gracefully
const searchPromises = Object.values(Actions).map(async (action) => {
try {
const results = await action.search(query, query, locale)
return { success: true, data: results, actionType: action.key }
}
catch (error) {
console.warn(`Search failed for ${action.key}:`, error)
return { success: false, data: [], actionType: action.key, error }
}
})
const settledResults = await Promise.allSettled(searchPromises)
const allResults: SearchResult[] = []
const failedActions: string[] = []
settledResults.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value.success) {
allResults.push(...result.value.data)
}
else {
const actionKey = Object.values(Actions)[index]?.key || 'unknown'
failedActions.push(actionKey)
}
})
if (failedActions.length > 0)
console.warn(`Some search actions failed: ${failedActions.join(', ')}`)
return allResults
}
export const matchAction = (query: string, actions: Record<string, ActionItem>) => {
return Object.values(actions).find((action) => {
const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
return reg.test(query)
})
}
export * from './types'
export { appAction, knowledgeAction, pluginAction, workflowNodesAction }

View File

@@ -0,0 +1,56 @@
import type { ActionItem, KnowledgeSearchResult } from './types'
import type { DataSet } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
import { Folder } from '../../base/icons/src/vender/solid/files'
import cn from '@/utils/classnames'
const EXTERNAL_PROVIDER = 'external' as const
const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER
const parser = (datasets: DataSet[]): KnowledgeSearchResult[] => {
return datasets.map((dataset) => {
const path = isExternalProvider(dataset.provider) ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`
return {
id: dataset.id,
title: dataset.name,
description: dataset.description,
type: 'knowledge' as const,
path,
icon: (
<div className={cn(
'flex shrink-0 items-center justify-center rounded-md border-[0.5px] border-[#E0EAFF] bg-[#F5F8FF] p-2.5',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}>
<Folder className='h-5 w-5 text-[#444CE7]' />
</div>
),
data: dataset,
}
})
}
export const knowledgeAction: ActionItem = {
key: '@knowledge',
shortcut: '@kb',
title: 'Search Knowledge Bases',
description: 'Search and navigate to your knowledge bases',
// action,
search: async (_, searchTerm = '', _locale) => {
try {
const response = await fetchDatasets({
url: '/datasets',
params: {
page: 1,
limit: 10,
keyword: searchTerm,
},
})
const datasets = response?.data || []
return parser(datasets)
}
catch (error) {
console.warn('Knowledge search failed:', error)
return []
}
},
}

View File

@@ -0,0 +1,53 @@
import type { ActionItem, PluginSearchResult } from './types'
import { renderI18nObject } from '@/i18n-config'
import Icon from '../../plugins/card/base/card-icon'
import { postMarketplace } from '@/service/base'
import type { Plugin, PluginsFromMarketplaceResponse } from '../../plugins/types'
import { getPluginIconInMarketplace } from '../../plugins/marketplace/utils'
const parser = (plugins: Plugin[], locale: string): PluginSearchResult[] => {
return plugins.map((plugin) => {
return {
id: plugin.name,
title: renderI18nObject(plugin.label, locale) || plugin.name,
description: renderI18nObject(plugin.brief, locale) || '',
type: 'plugin' as const,
icon: <Icon src={plugin.icon} />,
data: plugin,
}
})
}
export const pluginAction: ActionItem = {
key: '@plugin',
shortcut: '@plugin',
title: 'Search Plugins',
description: 'Search and navigate to your plugins',
search: async (_, searchTerm = '', locale) => {
try {
const response = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/search/advanced', {
body: {
page: 1,
page_size: 10,
query: searchTerm,
type: 'plugin',
},
})
if (!response?.data?.plugins) {
console.warn('Plugin search: Unexpected response structure', response)
return []
}
const list = response.data.plugins.map(plugin => ({
...plugin,
icon: getPluginIconInMarketplace(plugin),
}))
return parser(list, locale!)
}
catch (error) {
console.warn('Plugin search failed:', error)
return []
}
},
}

View File

@@ -0,0 +1,54 @@
import type { ReactNode } from 'react'
import type { TypeWithI18N } from '../../base/form/types'
import type { App } from '@/types/app'
import type { Plugin } from '../../plugins/types'
import type { DataSet } from '@/models/datasets'
import type { CommonNodeType } from '../../workflow/types'
export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node'
export type BaseSearchResult<T = any> = {
id: string
title: string
description?: string
type: SearchResultType
path?: string
icon?: ReactNode
data: T
}
export type AppSearchResult = {
type: 'app'
} & BaseSearchResult<App>
export type PluginSearchResult = {
type: 'plugin'
} & BaseSearchResult<Plugin>
export type KnowledgeSearchResult = {
type: 'knowledge'
} & BaseSearchResult<DataSet>
export type WorkflowNodeSearchResult = {
type: 'workflow-node'
metadata?: {
nodeId: string
nodeData: CommonNodeType
}
} & BaseSearchResult<CommonNodeType>
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult
export type ActionItem = {
key: '@app' | '@knowledge' | '@plugin' | '@node'
shortcut: string
title: string | TypeWithI18N
description: string
action?: (data: SearchResult) => void
searchFn?: (searchTerm: string) => SearchResult[]
search: (
query: string,
searchTerm: string,
locale?: string,
) => (Promise<SearchResult[]> | SearchResult[])
}

View File

@@ -0,0 +1,24 @@
import type { ActionItem } from './types'
// Create the workflow nodes action
export const workflowNodesAction: ActionItem = {
key: '@node',
shortcut: '@node',
title: 'Search Workflow Nodes',
description: 'Find and jump to nodes in the current workflow by name or type',
searchFn: undefined, // Will be set by useWorkflowSearch hook
search: async (_, searchTerm = '', locale) => {
try {
// Use the searchFn if available (set by useWorkflowSearch hook)
if (workflowNodesAction.searchFn)
return workflowNodesAction.searchFn(searchTerm)
// If not in workflow context, return empty array
return []
}
catch (error) {
console.warn('Workflow nodes search failed:', error)
return []
}
},
}

View File

@@ -0,0 +1,51 @@
import type { FC } from 'react'
import { Command } from 'cmdk'
import { useTranslation } from 'react-i18next'
import type { ActionItem } from './actions/types'
type Props = {
actions: Record<string, ActionItem>
onCommandSelect: (commandKey: string) => void
}
const CommandSelector: FC<Props> = ({ actions, onCommandSelect }) => {
const { t } = useTranslation()
return (
<div className="p-4">
<div className="mb-3 text-left text-sm font-medium text-text-secondary">
{t('app.gotoAnything.selectSearchType')}
</div>
<Command.Group className="space-y-1">
{Object.values(actions).map(action => (
<Command.Item
key={action.key}
value={action.shortcut}
className="flex cursor-pointer items-center rounded-md
p-2.5
transition-all
duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover"
onSelect={() => onCommandSelect(action.shortcut)}
>
<span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary">
{action.shortcut}
</span>
<span className="ml-3 text-sm text-text-secondary">
{(() => {
const keyMap: Record<string, string> = {
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
}
return t(keyMap[action.key])
})()}
</span>
</Command.Item>
))}
</Command.Group>
</div>
)
}
export default CommandSelector

View File

@@ -0,0 +1,50 @@
'use client'
import type { ReactNode } from 'react'
import React, { createContext, useContext, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
/**
* Interface for the GotoAnything context
*/
type GotoAnythingContextType = {
/**
* Whether the current page is a workflow page
*/
isWorkflowPage: boolean
}
// Create context with default values
const GotoAnythingContext = createContext<GotoAnythingContextType>({
isWorkflowPage: false,
})
/**
* Hook to use the GotoAnything context
*/
export const useGotoAnythingContext = () => useContext(GotoAnythingContext)
type GotoAnythingProviderProps = {
children: ReactNode
}
/**
* Provider component for GotoAnything context
*/
export const GotoAnythingProvider: React.FC<GotoAnythingProviderProps> = ({ children }) => {
const [isWorkflowPage, setIsWorkflowPage] = useState(false)
const pathname = usePathname()
// Update context based on current pathname
useEffect(() => {
// Check if current path contains workflow
const isWorkflow = pathname?.includes('/workflow') || false
setIsWorkflowPage(isWorkflow)
}, [pathname])
return (
<GotoAnythingContext.Provider value={{ isWorkflowPage }}>
{children}
</GotoAnythingContext.Provider>
)
}

View File

@@ -0,0 +1,403 @@
'use client'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import Modal from '@/app/components/base/modal'
import Input from '@/app/components/base/input'
import { useDebounce, useKeyPress } from 'ahooks'
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
import { RiSearchLine } from '@remixicon/react'
import { Actions as AllActions, type SearchResult, matchAction, searchAnything } from './actions'
import { GotoAnythingProvider, useGotoAnythingContext } from './context'
import { useQuery } from '@tanstack/react-query'
import { useGetLanguage } from '@/context/i18n'
import { useTranslation } from 'react-i18next'
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
import type { Plugin } from '../plugins/types'
import { Command } from 'cmdk'
import CommandSelector from './command-selector'
type Props = {
onHide?: () => void
}
const GotoAnything: FC<Props> = ({
onHide,
}) => {
const router = useRouter()
const defaultLocale = useGetLanguage()
const { isWorkflowPage } = useGotoAnythingContext()
const { t } = useTranslation()
const [show, setShow] = useState<boolean>(false)
const [searchQuery, setSearchQuery] = useState<string>('')
const [cmdVal, setCmdVal] = useState<string>('')
const inputRef = useRef<HTMLInputElement>(null)
// Filter actions based on context
const Actions = useMemo(() => {
// Create a filtered copy of actions based on current page context
if (isWorkflowPage) {
// Include all actions on workflow pages
return AllActions
}
else {
// Exclude node action on non-workflow pages
const { app, knowledge, plugin } = AllActions
return { app, knowledge, plugin }
}
}, [isWorkflowPage])
const [activePlugin, setActivePlugin] = useState<Plugin>()
// Handle keyboard shortcuts
const handleToggleModal = useCallback((e: KeyboardEvent) => {
// Allow closing when modal is open, even if focus is in the search input
if (!show && isEventTargetInputArea(e.target as HTMLElement))
return
e.preventDefault()
setShow((prev) => {
if (!prev) {
// Opening modal - reset search state
setSearchQuery('')
}
return !prev
})
}, [show])
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.k`, handleToggleModal, {
exactMatch: true,
useCapture: true,
})
useKeyPress(['esc'], (e) => {
if (show) {
e.preventDefault()
setShow(false)
setSearchQuery('')
}
})
const searchQueryDebouncedValue = useDebounce(searchQuery.trim(), {
wait: 300,
})
const isCommandsMode = searchQuery.trim() === '@'
const searchMode = useMemo(() => {
if (isCommandsMode) return 'commands'
const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions)
return action ? action.key : 'general'
}, [searchQueryDebouncedValue, Actions, isCommandsMode])
const { data: searchResults = [], isLoading, isError, error } = useQuery(
{
queryKey: [
'goto-anything',
'search-result',
searchQueryDebouncedValue,
searchMode,
isWorkflowPage,
defaultLocale,
Object.keys(Actions).sort().join(','),
],
queryFn: async () => {
const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions)
return await searchAnything(defaultLocale, query, action)
},
enabled: !!searchQueryDebouncedValue && !isCommandsMode,
staleTime: 30000,
gcTime: 300000,
},
)
const handleCommandSelect = useCallback((commandKey: string) => {
setSearchQuery(`${commandKey} `)
setCmdVal('')
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}, [])
// Handle navigation to selected result
const handleNavigate = useCallback((result: SearchResult) => {
setShow(false)
setSearchQuery('')
switch (result.type) {
case 'plugin':
setActivePlugin(result.data)
break
case 'workflow-node':
// Handle workflow node selection and navigation
if (result.metadata?.nodeId)
selectWorkflowNode(result.metadata.nodeId, true)
break
default:
if (result.path)
router.push(result.path)
}
}, [router])
// Group results by type
const groupedResults = useMemo(() => searchResults.reduce((acc, result) => {
if (!acc[result.type])
acc[result.type] = []
acc[result.type].push(result)
return acc
}, {} as { [key: string]: SearchResult[] }),
[searchResults])
const emptyResult = useMemo(() => {
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
return null
const isCommandSearch = searchMode !== 'general'
const commandType = isCommandSearch ? searchMode.replace('@', '') : ''
if (isError) {
return (
<div className="flex items-center justify-center py-8 text-center text-text-tertiary">
<div>
<div className='text-sm font-medium text-red-500'>{t('app.gotoAnything.searchTemporarilyUnavailable')}</div>
<div className='mt-1 text-xs text-text-quaternary'>
{t('app.gotoAnything.servicesUnavailableMessage')}
</div>
</div>
</div>
)
}
return (
<div className="flex items-center justify-center py-8 text-center text-text-tertiary">
<div>
<div className='text-sm font-medium'>
{isCommandSearch
? (() => {
const keyMap: Record<string, string> = {
app: 'app.gotoAnything.emptyState.noAppsFound',
plugin: 'app.gotoAnything.emptyState.noPluginsFound',
knowledge: 'app.gotoAnything.emptyState.noKnowledgeBasesFound',
node: 'app.gotoAnything.emptyState.noWorkflowNodesFound',
}
return t(keyMap[commandType] || 'app.gotoAnything.noResults')
})()
: t('app.gotoAnything.noResults')
}
</div>
<div className='mt-1 text-xs text-text-quaternary'>
{isCommandSearch
? t('app.gotoAnything.emptyState.tryDifferentTerm', { mode: searchMode })
: t('app.gotoAnything.emptyState.trySpecificSearch', { shortcuts: Object.values(Actions).map(action => action.shortcut).join(', ') })
}
</div>
</div>
</div>
)
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
const defaultUI = useMemo(() => {
if (searchQuery.trim())
return null
return (<div className="flex items-center justify-center py-12 text-center text-text-tertiary">
<div>
<div className='text-sm font-medium'>{t('app.gotoAnything.searchTitle')}</div>
<div className='mt-3 space-y-1 text-xs text-text-quaternary'>
<div>{t('app.gotoAnything.searchHint')}</div>
<div>{t('app.gotoAnything.commandHint')}</div>
</div>
</div>
</div>)
}, [searchQuery, Actions])
useEffect(() => {
if (show) {
requestAnimationFrame(() => {
inputRef.current?.focus()
})
}
return () => {
setCmdVal('')
}
}, [show])
return (
<>
<Modal
isShow={show}
onClose={() => {
setShow(false)
setSearchQuery('')
onHide?.()
}}
closable={false}
className='!w-[480px] !p-0'
>
<div className='flex flex-col rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'>
<Command
className='outline-none'
value={cmdVal}
onValueChange={setCmdVal}
>
<div className='flex items-center gap-3 border-b border-divider-subtle bg-components-panel-bg-blur px-4 py-3'>
<RiSearchLine className='h-4 w-4 text-text-quaternary' />
<div className='flex flex-1 items-center gap-2'>
<Input
ref={inputRef}
value={searchQuery}
placeholder={t('app.gotoAnything.searchPlaceholder')}
onChange={(e) => {
setCmdVal('')
setSearchQuery(e.target.value)
}}
className='flex-1 !border-0 !bg-transparent !shadow-none'
wrapperClassName='flex-1 !border-0 !bg-transparent'
autoFocus
/>
{searchMode !== 'general' && (
<div className='flex items-center gap-1 rounded bg-blue-50 px-2 py-[2px] text-xs font-medium text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'>
<span>{searchMode.replace('@', '').toUpperCase()}</span>
</div>
)}
</div>
<div className='text-xs text-text-quaternary'>
<span className='system-kbd rounded bg-gray-200 px-1 py-[2px] font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-100'>
{isMac() ? '⌘' : 'Ctrl'}
</span>
<span className='system-kbd ml-1 rounded bg-gray-200 px-1 py-[2px] font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-100'>
K
</span>
</div>
</div>
<Command.List className='max-h-[275px] min-h-[240px] overflow-y-auto'>
{isLoading && (
<div className="flex items-center justify-center py-8 text-center text-text-tertiary">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-gray-600"></div>
<span className="text-sm">{t('app.gotoAnything.searching')}</span>
</div>
</div>
)}
{isError && (
<div className="flex items-center justify-center py-8 text-center text-text-tertiary">
<div>
<div className="text-sm font-medium text-red-500">{t('app.gotoAnything.searchFailed')}</div>
<div className="mt-1 text-xs text-text-quaternary">
{error.message}
</div>
</div>
</div>
)}
{!isLoading && !isError && (
<>
{isCommandsMode ? (
<CommandSelector
actions={Actions}
onCommandSelect={handleCommandSelect}
/>
) : (
Object.entries(groupedResults).map(([type, results], groupIndex) => (
<Command.Group key={groupIndex} heading={(() => {
const typeMap: Record<string, string> = {
'app': 'app.gotoAnything.groups.apps',
'plugin': 'app.gotoAnything.groups.plugins',
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
}
return t(typeMap[type] || `${type}s`)
})()} className='p-2 capitalize text-text-secondary'>
{results.map(result => (
<Command.Item
key={`${result.type}-${result.id}`}
value={result.title}
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
onSelect={() => handleNavigate(result)}
>
{result.icon}
<div className='min-w-0 flex-1'>
<div className='truncate font-medium text-text-secondary'>
{result.title}
</div>
{result.description && (
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
{result.description}
</div>
)}
</div>
<div className='text-xs capitalize text-text-quaternary'>
{result.type}
</div>
</Command.Item>
))}
</Command.Group>
))
)}
{!isCommandsMode && emptyResult}
{!isCommandsMode && defaultUI}
</>
)}
</Command.List>
{(!!searchResults.length || isError) && (
<div className='border-t border-divider-subtle bg-components-panel-bg-blur px-4 py-2 text-xs text-text-tertiary'>
<div className='flex items-center justify-between'>
<span>
{isError ? (
<span className='text-red-500'>{t('app.gotoAnything.someServicesUnavailable')}</span>
) : (
<>
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
{searchMode !== 'general' && (
<span className='ml-2 opacity-60'>
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
</span>
)}
</>
)}
</span>
<span className='opacity-60'>
{searchMode !== 'general'
? t('app.gotoAnything.clearToSearchAll')
: t('app.gotoAnything.useAtForSpecific')
}
</span>
</div>
</div>
)}
</Command>
</div>
</Modal>
{
activePlugin && (
<InstallFromMarketplace
manifest={activePlugin}
uniqueIdentifier={activePlugin.latest_package_identifier}
onClose={() => setActivePlugin(undefined)}
onSuccess={() => setActivePlugin(undefined)}
/>
)
}
</>
)
}
/**
* GotoAnything component with context provider
*/
const GotoAnythingWithContext: FC<Props> = (props) => {
return (
<GotoAnythingProvider>
<GotoAnything {...props} />
</GotoAnythingProvider>
)
}
export default GotoAnythingWithContext

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import type { ModelParameterRule } from '../declarations'
import { useLanguage } from '../hooks'
import { isNullOrUndefined } from '../utils'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'
@@ -26,6 +27,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
onSwitch,
isInWorkflow,
}) => {
const language = useLanguage()
const [localValue, setLocalValue] = useState(value)
const numberInputRef = useRef<HTMLInputElement>(null)

View File

@@ -18,3 +18,4 @@ export * from './use-workflow-mode'
export * from './use-workflow-refresh-draft'
export * from './use-inspect-vars-crud'
export * from './use-set-workflow-vars-with-value'
export * from './use-workflow-search'

View File

@@ -0,0 +1,135 @@
'use client'
import { useCallback, useEffect, useMemo } from 'react'
import { useNodes } from 'reactflow'
import { useNodesInteractions } from './use-nodes-interactions'
import type { CommonNodeType } from '../types'
import { workflowNodesAction } from '@/app/components/goto-anything/actions/workflow-nodes'
import BlockIcon from '@/app/components/workflow/block-icon'
import { setupNodeSelectionListener } from '../utils/node-navigation'
/**
* Hook to register workflow nodes search functionality
*/
export const useWorkflowSearch = () => {
const nodes = useNodes()
const { handleNodeSelect } = useNodesInteractions()
// Filter and process nodes for search
const searchableNodes = useMemo(() => {
const filteredNodes = nodes.filter((node) => {
if (!node.id || !node.data || node.type === 'sticky') return false
const nodeData = node.data as CommonNodeType
const nodeType = nodeData?.type
const internalStartNodes = ['iteration-start', 'loop-start']
return !internalStartNodes.includes(nodeType)
})
const result = filteredNodes
.map((node) => {
const nodeData = node.data as CommonNodeType
return {
id: node.id,
title: nodeData?.title || nodeData?.type || 'Untitled',
type: nodeData?.type || '',
desc: nodeData?.desc || '',
blockType: nodeData?.type,
nodeData,
}
})
return result
}, [nodes])
// Create search function for workflow nodes
const searchWorkflowNodes = useCallback((query: string) => {
if (!searchableNodes.length) return []
const searchTerm = query.toLowerCase().trim()
const results = searchableNodes
.map((node) => {
const titleMatch = node.title.toLowerCase()
const typeMatch = node.type.toLowerCase()
const descMatch = node.desc?.toLowerCase() || ''
let score = 0
// If no search term, show all nodes with base score
if (!searchTerm) {
score = 1
}
else {
// Score based on search relevance
if (titleMatch.startsWith(searchTerm)) score += 100
else if (titleMatch.includes(searchTerm)) score += 50
else if (typeMatch === searchTerm) score += 80
else if (typeMatch.includes(searchTerm)) score += 30
else if (descMatch.includes(searchTerm)) score += 20
}
return score > 0
? {
id: node.id,
title: node.title,
description: node.desc || node.type,
type: 'workflow-node' as const,
path: `#${node.id}`,
icon: (
<BlockIcon
type={node.blockType}
className="shrink-0"
size="sm"
/>
),
metadata: {
nodeId: node.id,
nodeData: node.nodeData,
},
// Add required data property for SearchResult type
data: node.nodeData,
}
: null
})
.filter((node): node is NonNullable<typeof node> => node !== null)
.sort((a, b) => {
// If no search term, sort alphabetically
if (!searchTerm)
return a.title.localeCompare(b.title)
// Sort by relevance when searching
const aTitle = a.title.toLowerCase()
const bTitle = b.title.toLowerCase()
if (aTitle.startsWith(searchTerm) && !bTitle.startsWith(searchTerm)) return -1
if (!aTitle.startsWith(searchTerm) && bTitle.startsWith(searchTerm)) return 1
return 0
})
return results
}, [searchableNodes])
// Directly set the search function on the action object
useEffect(() => {
if (searchableNodes.length > 0) {
// Set the search function directly on the action
workflowNodesAction.searchFn = searchWorkflowNodes
}
return () => {
// Clean up when component unmounts
workflowNodesAction.searchFn = undefined
}
}, [searchableNodes, searchWorkflowNodes])
// Set up node selection event listener using the utility function
useEffect(() => {
return setupNodeSelectionListener(handleNodeSelect)
}, [handleNodeSelect])
return null
}

View File

@@ -58,6 +58,7 @@ import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
import CustomSimpleNode from './simple-node'
import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
import Operator from './operator'
import { useWorkflowSearch } from './hooks/use-workflow-search'
import Control from './operator/control'
import CustomEdge from './custom-edge'
import CustomConnectionLine from './custom-connection-line'
@@ -68,6 +69,7 @@ import NodeContextmenu from './node-contextmenu'
import SelectionContextmenu from './selection-contextmenu'
import SyncingDataModal from './syncing-data-modal'
import LimitTips from './limit-tips'
import { setupScrollToNodeListener } from './utils/node-navigation'
import {
useStore,
useWorkflowStore,
@@ -280,6 +282,14 @@ export const Workflow: FC<WorkflowProps> = memo(({
})
useShortcuts()
// Initialize workflow node search functionality
useWorkflowSearch()
// Set up scroll to node event listener using the utility function
useEffect(() => {
return setupScrollToNodeListener(nodes, reactflow)
}, [nodes, reactflow])
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
useEffect(() => {
fetchInspectVars()

View File

@@ -31,7 +31,8 @@ const Placeholder = () => {
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
<div
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
onClick={((e) => {
onMouseDown={((e) => {
e.preventDefault()
e.stopPropagation()
handleInsert('/')
})}

View File

@@ -52,7 +52,9 @@ const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
}
>
<div className='flex justify-between px-1 pb-2'>
<UndoRedo handleUndo={handleUndo} handleRedo={handleRedo} />
<div className='flex items-center gap-2'>
<UndoRedo handleUndo={handleUndo} handleRedo={handleRedo} />
</div>
<VariableTrigger />
<div className='relative'>
<MiniMap

View File

@@ -1,5 +1,5 @@
import { memo, useCallback } from 'react'
import type { WorkflowDataUpdater } from '../types'
import type { WorkflowRunDetailResponse } from '@/models/log'
import Run from '../run'
import { useStore } from '../store'
import { useWorkflowUpdate } from '../hooks'
@@ -9,12 +9,12 @@ const Record = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
const handleResultCallback = useCallback((res: any) => {
const graph: WorkflowDataUpdater = res.graph
const handleResultCallback = useCallback((res: WorkflowRunDetailResponse) => {
const graph = res.graph
handleUpdateWorkflowCanvas({
nodes: graph.nodes,
edges: graph.edges,
viewport: graph.viewport,
viewport: graph.viewport || { x: 0, y: 0, zoom: 1 },
})
}, [handleUpdateWorkflowCanvas])

View File

@@ -0,0 +1,124 @@
/**
* Node navigation utilities for workflow
* This module provides functions for node selection, focusing and scrolling in workflow
*/
/**
* Interface for node selection event detail
*/
export type NodeSelectionDetail = {
nodeId: string;
focus?: boolean;
}
/**
* Select a node in the workflow
* @param nodeId - The ID of the node to select
* @param focus - Whether to focus/scroll to the node
*/
export function selectWorkflowNode(nodeId: string, focus = false): void {
// Create and dispatch a custom event for node selection
const event = new CustomEvent('workflow:select-node', {
detail: {
nodeId,
focus,
},
})
document.dispatchEvent(event)
}
/**
* Scroll to a specific node in the workflow
* @param nodeId - The ID of the node to scroll to
*/
export function scrollToWorkflowNode(nodeId: string): void {
// Create and dispatch a custom event for scrolling to node
const event = new CustomEvent('workflow:scroll-to-node', {
detail: { nodeId },
})
document.dispatchEvent(event)
}
/**
* Setup node selection event listener
* @param handleNodeSelect - Function to handle node selection
* @returns Cleanup function
*/
export function setupNodeSelectionListener(
handleNodeSelect: (nodeId: string) => void,
): () => void {
// Event handler for node selection
const handleNodeSelection = (event: CustomEvent<NodeSelectionDetail>) => {
const { nodeId, focus } = event.detail
if (nodeId) {
// Select the node
handleNodeSelect(nodeId)
// If focus is requested, scroll to the node
if (focus) {
// Use a small timeout to ensure node selection happens first
setTimeout(() => {
scrollToWorkflowNode(nodeId)
}, 100)
}
}
}
// Add event listener
document.addEventListener(
'workflow:select-node',
handleNodeSelection as EventListener,
)
// Return cleanup function
return () => {
document.removeEventListener(
'workflow:select-node',
handleNodeSelection as EventListener,
)
}
}
/**
* Setup scroll to node event listener with ReactFlow
* @param nodes - The workflow nodes
* @param reactflow - The ReactFlow instance
* @returns Cleanup function
*/
export function setupScrollToNodeListener(
nodes: any[],
reactflow: any,
): () => void {
// Event handler for scrolling to node
const handleScrollToNode = (event: CustomEvent<NodeSelectionDetail>) => {
const { nodeId } = event.detail
if (nodeId) {
// Find the target node
const node = nodes.find(n => n.id === nodeId)
if (node) {
// Use ReactFlow's fitView API to scroll to the node
reactflow.fitView({
nodes: [node],
padding: 0.2,
duration: 800,
minZoom: 0.5,
maxZoom: 1,
})
}
}
}
// Add event listener
document.addEventListener(
'workflow:scroll-to-node',
handleScrollToNode as EventListener,
)
// Return cleanup function
return () => {
document.removeEventListener(
'workflow:scroll-to-node',
handleScrollToNode as EventListener,
)
}
}

View File

@@ -1,7 +1,7 @@
'use client'
import React from 'react'
import { useContext } from 'use-context-selector'
import Select from '@/app/components/base/select/locale'
import LocaleSigninSelect from '@/app/components/base/select/locale-signin'
import Divider from '@/app/components/base/divider'
import { languages } from '@/i18n-config/language'
import type { Locale } from '@/i18n-config'
@@ -33,7 +33,7 @@ const Header = () => {
/>
: <DifyLogo size='large' />}
<div className='flex items-center gap-1'>
<Select
<LocaleSigninSelect
value={locale}
items={languages.filter(item => item.supported)}
onChange={(value) => {

View File

@@ -0,0 +1,17 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_131_1011)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0003 0.5C9.15149 0.501478 6.39613 1.51046 4.22687 3.34652C2.05761 5.18259 0.615903 7.72601 0.159545 10.522C-0.296814 13.318 0.261927 16.1842 1.73587 18.6082C3.20981 21.0321 5.50284 22.8558 8.20493 23.753C8.80105 23.8636 9.0256 23.4941 9.0256 23.18C9.0256 22.8658 9.01367 21.955 9.0097 20.9592C5.6714 21.6804 4.96599 19.5505 4.96599 19.5505C4.42152 18.1674 3.63464 17.8039 3.63464 17.8039C2.54571 17.065 3.71611 17.0788 3.71611 17.0788C4.92227 17.1637 5.55616 18.3097 5.55616 18.3097C6.62521 20.1333 8.36389 19.6058 9.04745 19.2976C9.15475 18.5251 9.46673 17.9995 9.8105 17.7012C7.14383 17.4008 4.34204 16.3774 4.34204 11.8054C4.32551 10.6197 4.76802 9.47305 5.57801 8.60268C5.45481 8.30236 5.04348 7.08923 5.69524 5.44143C5.69524 5.44143 6.7027 5.12135 8.9958 6.66444C10.9627 6.12962 13.0379 6.12962 15.0047 6.66444C17.2958 5.12135 18.3013 5.44143 18.3013 5.44143C18.9551 7.08528 18.5437 8.29841 18.4205 8.60268C19.2331 9.47319 19.6765 10.6218 19.6585 11.8094C19.6585 16.3912 16.8507 17.4008 14.1801 17.6952C14.6093 18.0667 14.9928 18.7918 14.9928 19.9061C14.9928 21.5026 14.9789 22.7868 14.9789 23.18C14.9789 23.4981 15.1955 23.8695 15.8035 23.753C18.5059 22.8557 20.7992 21.0317 22.2731 18.6073C23.747 16.183 24.3055 13.3163 23.8486 10.5201C23.3917 7.7238 21.9493 5.18035 19.7793 3.34461C17.6093 1.50886 14.8533 0.500541 12.0042 0.5H12.0003Z" fill="#ffffff"/>
<path d="M4.54444 17.6321C4.5186 17.6914 4.42322 17.7092 4.34573 17.6677C4.26823 17.6262 4.21061 17.5491 4.23843 17.4879C4.26625 17.4266 4.35964 17.4108 4.43714 17.4523C4.51463 17.4938 4.57424 17.5729 4.54444 17.6321Z" fill="#ffffff"/>
<path d="M5.03123 18.1714C4.99008 18.192 4.943 18.1978 4.89805 18.1877C4.8531 18.1776 4.81308 18.1523 4.78483 18.1161C4.70734 18.0331 4.69143 17.9185 4.75104 17.8671C4.81066 17.8157 4.91797 17.8395 4.99546 17.9224C5.07296 18.0054 5.09084 18.12 5.03123 18.1714Z" fill="#ffffff"/>
<path d="M5.50425 18.857C5.43072 18.9084 5.30553 18.857 5.23598 18.7543C5.21675 18.7359 5.20146 18.7138 5.19101 18.6893C5.18056 18.6649 5.17517 18.6386 5.17517 18.612C5.17517 18.5855 5.18056 18.5592 5.19101 18.5347C5.20146 18.5103 5.21675 18.4882 5.23598 18.4698C5.3095 18.4204 5.4347 18.4698 5.50425 18.5705C5.57379 18.6713 5.57578 18.8057 5.50425 18.857V18.857Z" fill="#ffffff"/>
<path d="M6.14612 19.5207C6.08054 19.5939 5.94741 19.5741 5.83812 19.4753C5.72883 19.3765 5.70299 19.2422 5.76857 19.171C5.83414 19.0999 5.96727 19.1197 6.08054 19.2165C6.1938 19.3133 6.21566 19.4496 6.14612 19.5207V19.5207Z" fill="#ffffff"/>
<path d="M7.04617 19.9081C7.01637 20.001 6.88124 20.0425 6.74612 20.003C6.611 19.9635 6.52158 19.8528 6.54741 19.758C6.57325 19.6631 6.71036 19.6197 6.84747 19.6631C6.98457 19.7066 7.07201 19.8113 7.04617 19.9081Z" fill="#ffffff"/>
<path d="M8.02783 19.9752C8.02783 20.072 7.91656 20.155 7.77349 20.1569C7.63042 20.1589 7.51318 20.0799 7.51318 19.9831C7.51318 19.8863 7.62445 19.8033 7.76752 19.8013C7.91059 19.7993 8.02783 19.8764 8.02783 19.9752Z" fill="#ffffff"/>
<path d="M8.9419 19.8232C8.95978 19.92 8.86042 20.0207 8.71735 20.0445C8.57428 20.0682 8.4491 20.0109 8.43121 19.916C8.41333 19.8212 8.51666 19.7185 8.65576 19.6928C8.79485 19.6671 8.92401 19.7264 8.9419 19.8232Z" fill="#ffffff"/>
</g>
<defs>
<clipPath id="clip0_131_1011">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -144,11 +144,10 @@ const NormalForm = () => {
</div>
{showORLine && <div className="relative mt-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div className="relative flex justify-center">
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
<div className="flex items-center">
<div className="h-px flex-1 bg-gradient-to-r from-background-gradient-mask-transparent to-divider-regular"></div>
<span className="system-xs-medium-uppercase px-3 text-text-tertiary">{t('login.or')}</span>
<div className="h-px flex-1 bg-gradient-to-l from-background-gradient-mask-transparent to-divider-regular"></div>
</div>
</div>}
{

View File

@@ -2,6 +2,10 @@
background: center/contain url('./assets/github.svg') no-repeat;
}
html[data-theme="dark"] .githubIcon {
background: center/contain url('./assets/github-dark.svg') no-repeat;
}
.googleIcon {
background: center/contain url('./assets/google.svg') no-repeat;
}

View File

@@ -1,7 +1,7 @@
'use client'
import { useSearchParams } from 'next/navigation'
import OneMoreStep from './oneMoreStep'
import NormalForm from './normalForm'
import OneMoreStep from './one-more-step'
import NormalForm from './normal-form'
const SignIn = () => {
const searchParams = useSearchParams()

View File

@@ -254,6 +254,44 @@ const translation = {
maxActiveRequests: 'Maximale gleichzeitige Anfragen',
maxActiveRequestsPlaceholder: 'Geben Sie 0 für unbegrenzt ein',
maxActiveRequestsTip: 'Maximale Anzahl gleichzeitiger aktiver Anfragen pro App (0 für unbegrenzt)',
gotoAnything: {
actions: {
searchPlugins: 'Such-Plugins',
searchKnowledgeBases: 'Wissensdatenbanken durchsuchen',
searchWorkflowNodes: 'Workflow-Knoten durchsuchen',
searchKnowledgeBasesDesc: 'Suchen und navigieren Sie zu Ihren Wissensdatenbanken',
searchApplications: 'Anwendungen suchen',
searchWorkflowNodesHelp: 'Diese Funktion funktioniert nur, wenn ein Workflow angezeigt wird. Navigieren Sie zuerst zu einem Workflow.',
searchApplicationsDesc: 'Suchen und navigieren Sie zu Ihren Anwendungen',
searchPluginsDesc: 'Suchen und navigieren Sie zu Ihren Plugins',
searchWorkflowNodesDesc: 'Suchen und Springen zu Knoten im aktuellen Workflow nach Name oder Typ',
},
emptyState: {
noPluginsFound: 'Keine Plugins gefunden',
noWorkflowNodesFound: 'Keine Workflow-Knoten gefunden',
noKnowledgeBasesFound: 'Keine Wissensdatenbanken gefunden',
noAppsFound: 'Keine Apps gefunden',
},
groups: {
knowledgeBases: 'Wissensdatenbanken',
plugins: 'Plugins',
apps: 'Apps',
workflowNodes: 'Workflow-Knoten',
},
clearToSearchAll: 'Löschen Sie @, um alle zu durchsuchen',
searchTemporarilyUnavailable: 'Suche vorübergehend nicht verfügbar',
searchFailed: 'Suche fehlgeschlagen',
someServicesUnavailable: 'Einige Suchdienste sind nicht verfügbar',
servicesUnavailableMessage: 'Bei einigen Suchdiensten können Probleme auftreten. Versuchen Sie es gleich noch einmal.',
noResults: 'Keine Ergebnisse gefunden',
searchPlaceholder: 'Suchen Sie nach Befehlen, oder geben Sie @ ein...',
useAtForSpecific: 'Verwenden von @ für bestimmte Typen',
searchTitle: 'Suchen Sie nach irgendetwas',
searching: 'Suche...',
selectSearchType: 'Wählen Sie aus, wonach gesucht werden soll',
commandHint: 'Geben Sie @ ein, um nach Kategorie zu suchen',
searchHint: 'Beginnen Sie mit der Eingabe, um alles sofort zu durchsuchen',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
noDependents: 'Keine Angehörigen',
},
relationsTab: 'Beziehungen',
copyLastRun: 'Letzte Ausführung kopieren',
copyLastRunError: 'Fehler beim Kopieren der letzten Lauf-Eingaben',
noMatchingInputsFound: 'Keine übereinstimmenden Eingaben aus dem letzten Lauf gefunden.',
noLastRunFound: 'Kein vorheriger Lauf gefunden',
},
}

View File

@@ -252,6 +252,49 @@ const translation = {
maxActiveRequests: 'Max concurrent requests',
maxActiveRequestsPlaceholder: 'Enter 0 for unlimited',
maxActiveRequestsTip: 'Maximum number of concurrent active requests per app (0 for unlimited)',
gotoAnything: {
searchPlaceholder: 'Search or type @ for commands...',
searchTitle: 'Search for anything',
searching: 'Searching...',
noResults: 'No results found',
searchFailed: 'Search failed',
searchTemporarilyUnavailable: 'Search temporarily unavailable',
servicesUnavailableMessage: 'Some search services may be experiencing issues. Try again in a moment.',
someServicesUnavailable: 'Some search services unavailable',
resultCount: '{{count}} result',
resultCount_other: '{{count}} results',
inScope: 'in {{scope}}s',
clearToSearchAll: 'Clear @ to search all',
useAtForSpecific: 'Use @ for specific types',
selectSearchType: 'Choose what to search for',
searchHint: 'Start typing to search everything instantly',
commandHint: 'Type @ to browse by category',
actions: {
searchApplications: 'Search Applications',
searchApplicationsDesc: 'Search and navigate to your applications',
searchPlugins: 'Search Plugins',
searchPluginsDesc: 'Search and navigate to your plugins',
searchKnowledgeBases: 'Search Knowledge Bases',
searchKnowledgeBasesDesc: 'Search and navigate to your knowledge bases',
searchWorkflowNodes: 'Search Workflow Nodes',
searchWorkflowNodesDesc: 'Find and jump to nodes in the current workflow by name or type',
searchWorkflowNodesHelp: 'This feature only works when viewing a workflow. Navigate to a workflow first.',
},
emptyState: {
noAppsFound: 'No apps found',
noPluginsFound: 'No plugins found',
noKnowledgeBasesFound: 'No knowledge bases found',
noWorkflowNodesFound: 'No workflow nodes found',
tryDifferentTerm: 'Try a different search term or remove the {{mode}} filter',
trySpecificSearch: 'Try {{shortcuts}} for specific searches',
},
groups: {
apps: 'Apps',
plugins: 'Plugins',
knowledgeBases: 'Knowledge Bases',
workflowNodes: 'Workflow Nodes',
},
},
}
export default translation

View File

@@ -958,6 +958,11 @@ const translation = {
settingsTab: 'Settings',
lastRunTab: 'Last Run',
relationsTab: 'Relations',
copyLastRun: 'Copy Last Run',
noLastRunFound: 'No previous run found',
noMatchingInputsFound: 'No matching inputs found from last run',
lastRunInputsCopied: '{{count}} input(s) copied from last run',
copyLastRunError: 'Failed to copy last run inputs',
noData: {
description: 'The results of the last run will be displayed here',
runThisNode: 'Run this node',

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Introduce 0 para ilimitado',
maxActiveRequests: 'Máximas solicitudes concurrentes',
maxActiveRequestsTip: 'Número máximo de solicitudes activas concurrentes por aplicación (0 para ilimitado)',
gotoAnything: {
actions: {
searchApplications: 'Aplicaciones de búsqueda',
searchKnowledgeBasesDesc: 'Busque y navegue por sus bases de conocimiento',
searchWorkflowNodes: 'Buscar nodos de flujo de trabajo',
searchPlugins: 'Complementos de búsqueda',
searchWorkflowNodesDesc: 'Buscar y saltar a nodos en el flujo de trabajo actual por nombre o tipo',
searchKnowledgeBases: 'Buscar en las bases de conocimiento',
searchApplicationsDesc: 'Buscar y navegar a sus aplicaciones',
searchPluginsDesc: 'Busca y navega a tus plugins',
searchWorkflowNodesHelp: 'Esta función solo funciona cuando se visualiza un flujo de trabajo. Primero vaya a un flujo de trabajo.',
},
emptyState: {
noAppsFound: 'No se encontraron aplicaciones',
noPluginsFound: 'No se encontraron complementos',
noWorkflowNodesFound: 'No se encontraron nodos de flujo de trabajo',
noKnowledgeBasesFound: 'No se han encontrado bases de conocimiento',
},
groups: {
apps: 'Aplicaciones',
workflowNodes: 'Nodos de flujo de trabajo',
knowledgeBases: 'Bases de conocimiento',
plugins: 'Complementos',
},
clearToSearchAll: 'Borrar @ para buscar todo',
noResults: 'No se han encontrado resultados',
searching: 'Minucioso...',
searchTemporarilyUnavailable: 'La búsqueda no está disponible temporalmente',
searchFailed: 'Error de búsqueda',
useAtForSpecific: 'Use @ para tipos específicos',
searchPlaceholder: 'Busque o escriba @ para los comandos...',
searchTitle: 'Busca cualquier cosa',
someServicesUnavailable: 'Algunos servicios de búsqueda no están disponibles',
servicesUnavailableMessage: 'Algunos servicios de búsqueda pueden estar experimentando problemas. Inténtalo de nuevo en un momento.',
searchHint: 'Empieza a escribir para buscar todo al instante',
commandHint: 'Escriba @ para buscar por categoría',
selectSearchType: 'Elige qué buscar',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependencies: 'Dependencias',
},
relationsTab: 'Relaciones',
noLastRunFound: 'No se encontró ninguna ejecución anterior',
copyLastRunError: 'No se pudo copiar las entradas de la última ejecución',
copyLastRun: 'Copiar última ejecución',
noMatchingInputsFound: 'No se encontraron entradas coincidentes de la última ejecución.',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequests: 'بیشترین درخواست‌های همزمان',
maxActiveRequestsPlaceholder: 'برای نامحدود، 0 را وارد کنید',
maxActiveRequestsTip: 'حداکثر تعداد درخواست‌های فعال همزمان در هر برنامه (0 برای نامحدود)',
gotoAnything: {
actions: {
searchPlugins: 'افزونه های جستجو',
searchWorkflowNodes: 'گره های گردش کار جستجو',
searchApplications: 'جستجوی برنامه ها',
searchKnowledgeBases: 'جستجو در پایگاه های دانش',
searchWorkflowNodesHelp: 'این ویژگی فقط هنگام مشاهده گردش کار کار می کند. ابتدا به گردش کار بروید.',
searchApplicationsDesc: 'جستجو و پیمایش به برنامه های خود',
searchKnowledgeBasesDesc: 'پایگاه های دانش خود را جستجو کرده و به آن ناوبری کنید',
searchPluginsDesc: 'افزونه های خود را جستجو کرده و به آنها پیمایش کنید',
searchWorkflowNodesDesc: 'گره ها را در گردش کار فعلی بر اساس نام یا نوع پیدا کنید و به آنها بروید',
},
emptyState: {
noKnowledgeBasesFound: 'هیچ پایگاه دانش یافت نشد',
noAppsFound: 'هیچ برنامه ای یافت نشد',
noPluginsFound: 'هیچ افزونه ای یافت نشد',
noWorkflowNodesFound: 'هیچ گره گردش کاری یافت نشد',
},
groups: {
plugins: 'پلاگین',
apps: 'واژهنامه',
knowledgeBases: 'پایگاه های دانش',
workflowNodes: 'گره های گردش کار',
},
searching: 'جستجو...',
searchFailed: 'جستجو انجام نشد',
useAtForSpecific: 'از @ برای انواع خاص استفاده کنید',
clearToSearchAll: 'پاک کردن @ برای جستجوی همه',
noResults: 'هیچ نتیجه ای یافت نشد',
searchTitle: 'هر چیزی را جستجو کنید',
searchPlaceholder: 'جستجو یا تایپ @ برای دستورات...',
searchTemporarilyUnavailable: 'جستجو به طور موقت در دسترس نیست',
servicesUnavailableMessage: 'برخی از سرویس های جستجو ممکن است با مشکل مواجه شوند. یک لحظه دیگر دوباره امتحان کنید.',
someServicesUnavailable: 'برخی از سرویس های جستجو دردسترس نیستند',
selectSearchType: 'انتخاب کنید چه چیزی را جستجو کنید',
commandHint: '@ را برای مرور بر اساس دسته بندی تایپ کنید',
searchHint: 'شروع به تایپ کنید تا فورا همه چیز را جستجو کنید',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependentsDescription: 'گره هایی که به این گره متکی هستند',
},
relationsTab: 'روابط',
copyLastRun: 'کپی آخرین اجرا',
noLastRunFound: 'هیچ اجرای قبلی یافت نشد',
noMatchingInputsFound: 'هیچ ورودی مطابقی از آخرین اجرا یافت نشد',
copyLastRunError: 'نتوانستم ورودی‌های آخرین اجرای را کپی کنم',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Entrez 0 pour illimité',
maxActiveRequests: 'Nombre maximal de requêtes simultanées',
maxActiveRequestsTip: 'Nombre maximum de requêtes actives concurrentes par application (0 pour illimité)',
gotoAnything: {
actions: {
searchPluginsDesc: 'Recherchez et naviguez vers vos plugins',
searchKnowledgeBasesDesc: 'Recherchez et accédez à vos bases de connaissances',
searchWorkflowNodesDesc: 'Recherchez et accédez aux nœuds du flux de travail actuel par nom ou type',
searchApplicationsDesc: 'Recherchez et accédez à vos applications',
searchPlugins: 'Plugins de recherche',
searchWorkflowNodes: 'Rechercher des nœuds de workflow',
searchKnowledgeBases: 'Rechercher dans les bases de connaissances',
searchApplications: 'Rechercher des applications',
searchWorkflowNodesHelp: 'Cette fonctionnalité ne fonctionne que lors de laffichage dun flux de travail. Accédez dabord à un flux de travail.',
},
emptyState: {
noKnowledgeBasesFound: 'Aucune base de connaissances trouvée',
noAppsFound: 'Aucune application trouvée',
noPluginsFound: 'Aucun plugin trouvé',
noWorkflowNodesFound: 'Aucun nœud de workflow trouvé',
},
groups: {
apps: 'Apps',
workflowNodes: 'Nœuds de flux de travail',
knowledgeBases: 'Bases de connaissances',
plugins: 'Plug-ins',
},
someServicesUnavailable: 'Certains services de recherche indisponibles',
servicesUnavailableMessage: 'Certains services de recherche peuvent rencontrer des problèmes. Réessayez dans un instant.',
useAtForSpecific: 'Utilisez @ pour des types spécifiques',
searchTemporarilyUnavailable: 'Recherche temporairement indisponible',
searchTitle: 'Recherchez nimporte quoi',
clearToSearchAll: 'Effacer @ pour rechercher tout',
searching: 'Recherche...',
searchPlaceholder: 'Recherchez ou tapez @ pour les commandes...',
searchFailed: 'Echec de la recherche',
noResults: 'Aucun résultat trouvé',
commandHint: 'Tapez @ pour parcourir par catégorie',
selectSearchType: 'Choisissez les éléments de recherche',
searchHint: 'Commencez à taper pour tout rechercher instantanément',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependenciesDescription: 'Nœuds sur lesquels repose ce nœud',
},
relationsTab: 'Relations',
copyLastRun: 'Copier la dernière exécution',
noLastRunFound: 'Aucune exécution précédente trouvée',
noMatchingInputsFound: 'Aucune entrée correspondante trouvée dans la dernière exécution.',
copyLastRunError: 'Échec de la copie des entrées de la dernière exécution',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequests: 'अधिकतम समवर्ती अनुरोध',
maxActiveRequestsPlaceholder: 'असीमित के लिए 0 दर्ज करें',
maxActiveRequestsTip: 'प्रति ऐप सक्रिय अनुरोधों की अधिकतम संख्या (असीमित के लिए 0)',
gotoAnything: {
actions: {
searchPlugins: 'खोज प्लगइन्स',
searchWorkflowNodes: 'खोज कार्यप्रवाह नोड्स',
searchKnowledgeBases: 'ज्ञान आधार खोजें',
searchApplications: 'अनुसंधान एप्लिकेशन',
searchPluginsDesc: 'अपने प्लगइन्स को खोजें और नेविगेट करें',
searchWorkflowNodesDesc: 'वर्तमान कार्यप्रवाह में नाम या प्रकार द्वारा नोड्स को खोजें और उन पर कूदें',
searchKnowledgeBasesDesc: 'अपने ज्ञान आधारों की खोज करें और उन्हें नेविगेट करें',
searchApplicationsDesc: 'अपने अनुप्रयोगों की खोज करें और उन्हें नेविगेट करें',
searchWorkflowNodesHelp: 'यह सुविधा केवल तब काम करती है जब आप एक कार्यप्रवाह देख रहे हों। पहले एक कार्यप्रवाह पर जाएं।',
},
emptyState: {
noPluginsFound: 'कोई प्लगइन नहीं मिले',
noAppsFound: 'कोई ऐप्स नहीं मिले',
noKnowledgeBasesFound: 'कोई ज्ञान आधार नहीं मिले',
noWorkflowNodesFound: 'कोई कार्यप्रवाह नोड नहीं मिला',
},
groups: {
apps: 'ऐप्स',
knowledgeBases: 'ज्ञान आधार',
plugins: 'प्लगइन्स',
workflowNodes: 'कार्यप्रवाह नोड्स',
},
noResults: 'कोई परिणाम नहीं मिले',
clearToSearchAll: 'साफ़ @ सभी खोजने के लिए',
searchTitle: 'किसी भी चीज़ की खोज करें',
useAtForSpecific: '@ का उपयोग विशिष्ट प्रकारों के लिए करें',
someServicesUnavailable: 'कुछ खोज सेवाएँ उपलब्ध नहीं हैं',
searching: 'खोजना...',
searchFailed: 'खोज विफल रहा',
searchPlaceholder: 'कमांड के लिए खोजें या टाइप करें @...',
searchTemporarilyUnavailable: 'खोज अस्थायी रूप से उपलब्ध नहीं है',
servicesUnavailableMessage: 'कुछ खोज सेवाएँ समस्याओं का सामना कर सकती हैं। थोड़ी देर बाद फिर से प्रयास करें।',
commandHint: '@ का उपयोग कर श्रेणी के अनुसार ब्राउज़ करें',
selectSearchType: 'खोजने के लिए क्या चुनें',
searchHint: 'सब कुछ तुरंत खोजने के लिए टाइप करना शुरू करें',
},
}
export default translation

View File

@@ -1011,6 +1011,10 @@ const translation = {
noDependencies: 'कोई निर्भरताएँ नहीं',
},
relationsTab: 'रिश्ते',
copyLastRun: 'अंतिम रन कॉपी करें',
noLastRunFound: 'कोई पिछला रन नहीं मिला',
noMatchingInputsFound: 'अंतिम रन से कोई मेल खाने वाले इनपुट नहीं मिले',
copyLastRunError: 'अंतिम रन इनपुट को कॉपी करने में विफल',
},
}

View File

@@ -258,6 +258,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Inserisci 0 per illimitato',
maxActiveRequests: 'Massimo numero di richieste concorrenti',
maxActiveRequestsTip: 'Numero massimo di richieste attive concorrenti per app (0 per illimitato)',
gotoAnything: {
actions: {
searchWorkflowNodesHelp: 'Questa funzione funziona solo durante la visualizzazione di un flusso di lavoro. Passa prima a un flusso di lavoro.',
searchApplicationsDesc: 'Cerca e naviga alle tue applicazioni',
searchWorkflowNodes: 'Ricerca nei nodi del flusso di lavoro',
searchApplications: 'Cerca applicazioni',
searchPluginsDesc: 'Cerca e naviga verso i tuoi plugin',
searchKnowledgeBasesDesc: 'Cerca e naviga nelle tue knowledge base',
searchPlugins: 'Plugin di ricerca',
searchWorkflowNodesDesc: 'Trovare e passare ai nodi nel flusso di lavoro corrente in base al nome o al tipo',
searchKnowledgeBases: 'Cerca nelle Basi di Conoscenza',
},
emptyState: {
noKnowledgeBasesFound: 'Nessuna base di conoscenza trovata',
noAppsFound: 'Nessuna app trovata',
noWorkflowNodesFound: 'Nessun nodo del flusso di lavoro trovato',
noPluginsFound: 'Nessun plugin trovato',
},
groups: {
knowledgeBases: 'Basi di conoscenza',
workflowNodes: 'Nodi del flusso di lavoro',
plugins: 'Plugin',
apps: 'Applicazioni',
},
searchTitle: 'Cerca qualsiasi cosa',
searchPlaceholder: 'Cerca o digita @ per i comandi...',
searching: 'Indagatore...',
searchTemporarilyUnavailable: 'Ricerca temporaneamente non disponibile',
searchFailed: 'Ricerca non riuscita',
servicesUnavailableMessage: 'Alcuni servizi di ricerca potrebbero riscontrare problemi. Riprova tra un attimo.',
someServicesUnavailable: 'Alcuni servizi di ricerca non sono disponibili',
noResults: 'Nessun risultato trovato',
useAtForSpecific: 'Utilizzare @ per tipi specifici',
clearToSearchAll: 'Cancella @ per cercare tutto',
selectSearchType: 'Scegli cosa cercare',
commandHint: 'Digita @ per sfogliare per categoria',
searchHint: 'Inizia a digitare per cercare tutto all\'istante',
},
}
export default translation

View File

@@ -1017,6 +1017,10 @@ const translation = {
dependenciesDescription: 'Nodi su cui si basa questo nodo',
},
relationsTab: 'Relazioni',
copyLastRun: 'Copia ultimo eseguito',
noMatchingInputsFound: 'Nessun input corrispondente trovato dall\'ultimo funzionamento.',
copyLastRunError: 'Impossibile copiare gli input dell\'ultima esecuzione',
noLastRunFound: 'Nessuna esecuzione precedente trovata',
},
}

View File

@@ -251,6 +251,49 @@ const translation = {
maxActiveRequestsPlaceholder: '無制限のために0を入力してください',
maxActiveRequests: '最大同時リクエスト数',
maxActiveRequestsTip: 'アプリごとの同時アクティブリクエストの最大数無制限の場合は0',
gotoAnything: {
searchPlaceholder: '検索するか、@ を入力してコマンドを使用...',
searchTitle: '何でも検索',
searching: '検索中...',
noResults: '結果が見つかりません',
searchFailed: '検索に失敗しました',
searchTemporarilyUnavailable: '検索が一時的に利用できません',
servicesUnavailableMessage: '一部の検索サービスで問題が発生している可能性があります。しばらくしてからもう一度お試しください。',
someServicesUnavailable: '一部の検索サービスが利用できません',
resultCount: '{{count}} 件の結果',
resultCount_other: '{{count}} 件の結果',
inScope: '{{scope}}s 内',
clearToSearchAll: '@ をクリアしてすべてを検索',
useAtForSpecific: '特定のタイプには @ を使用',
selectSearchType: '検索対象を選択',
searchHint: '入力を開始してすべてを瞬時に検索',
commandHint: '@ を入力してカテゴリ別に参照',
actions: {
searchApplications: 'アプリケーションを検索',
searchApplicationsDesc: 'アプリケーションを検索してナビゲート',
searchPlugins: 'プラグインを検索',
searchPluginsDesc: 'プラグインを検索してナビゲート',
searchKnowledgeBases: 'ナレッジベースを検索',
searchKnowledgeBasesDesc: 'ナレッジベースを検索してナビゲート',
searchWorkflowNodes: 'ワークフローノードを検索',
searchWorkflowNodesDesc: '現在のワークフロー内のノードを名前またはタイプで検索してジャンプ',
searchWorkflowNodesHelp: 'この機能はワークフロー表示時のみ利用できます。まずワークフローに移動してください。',
},
emptyState: {
noAppsFound: 'アプリが見つかりません',
noPluginsFound: 'プラグインが見つかりません',
noKnowledgeBasesFound: 'ナレッジベースが見つかりません',
noWorkflowNodesFound: 'ワークフローノードが見つかりません',
tryDifferentTerm: '別の検索語句を試すか、{{mode}} フィルターを削除してください',
trySpecificSearch: '特定検索には {{shortcuts}} を試してください',
},
groups: {
apps: 'アプリケーション',
plugins: 'プラグイン',
knowledgeBases: 'ナレッジベース',
workflowNodes: 'ワークフローノード',
},
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
noDependencies: '依存元なし',
noDependents: '依存先なし',
},
copyLastRun: '最後の実行をコピー',
noLastRunFound: '以前の実行が見つかりませんでした。',
copyLastRunError: '最後の実行の入力をコピーできませんでした',
noMatchingInputsFound: '前回の実行から一致する入力が見つかりませんでした。',
},
}

View File

@@ -272,6 +272,44 @@ const translation = {
maxActiveRequests: '동시 최대 요청 수',
maxActiveRequestsPlaceholder: '무제한 사용을 원하시면 0을 입력하세요.',
maxActiveRequestsTip: '앱당 최대 동시 활성 요청 수(무제한은 0)',
gotoAnything: {
actions: {
searchWorkflowNodes: '워크플로 노드 검색',
searchApplicationsDesc: '애플리케이션 검색 및 탐색',
searchPlugins: '플러그인 검색',
searchApplications: '응용 프로그램 검색',
searchPluginsDesc: '플러그인을 검색하고 탐색합니다.',
searchWorkflowNodesDesc: '이름 또는 유형별로 현재 워크플로의 노드를 찾아 이동',
searchKnowledgeBasesDesc: '기술 자료를 검색하고 탐색합니다.',
searchWorkflowNodesHelp: '이 기능은 워크플로를 볼 때만 작동합니다. 먼저 워크플로로 이동합니다.',
searchKnowledgeBases: '기술 자료 검색',
},
emptyState: {
noAppsFound: '앱을 찾을 수 없습니다.',
noPluginsFound: '플러그인을 찾을 수 없습니다.',
noKnowledgeBasesFound: '기술 자료를 찾을 수 없습니다.',
noWorkflowNodesFound: '워크플로 노드를 찾을 수 없습니다.',
},
groups: {
apps: '앱',
plugins: '플러그인',
knowledgeBases: '기술 자료',
workflowNodes: '워크플로 노드',
},
searching: '검색...',
searchTitle: '무엇이든 검색',
useAtForSpecific: '특정 형식에 @ 사용',
searchTemporarilyUnavailable: '일시적으로 검색할 수 없음',
noResults: '결과를 찾을 수 없습니다.',
someServicesUnavailable: '일부 검색 서비스를 사용할 수 없습니다.',
servicesUnavailableMessage: '일부 검색 서비스에서 문제가 발생할 수 있습니다. 잠시 후에 다시 시도하십시오.',
searchFailed: '검색 실패',
searchPlaceholder: '명령을 검색하거나 @를 입력합니다...',
clearToSearchAll: '@를 지우면 모두 검색됩니다.',
selectSearchType: '검색할 항목 선택',
commandHint: '@를 입력하여 카테고리별로 찾아봅니다.',
searchHint: '즉시 모든 것을 검색하려면 입력을 시작하세요.',
},
}
export default translation

View File

@@ -1042,6 +1042,10 @@ const translation = {
dependenciesDescription: '이 노드가 의존하는 노드',
},
relationsTab: '관계',
copyLastRun: '마지막 실행 복사',
noLastRunFound: '이전 실행이 없습니다.',
noMatchingInputsFound: '지난 실행에서 일치하는 입력을 찾을 수 없습니다.',
copyLastRunError: '마지막 실행 입력을 복사하는 데 실패했습니다.',
},
}

View File

@@ -253,6 +253,44 @@ const translation = {
maxActiveRequests: 'Maksymalne równoczesne żądania',
maxActiveRequestsPlaceholder: 'Wprowadź 0, aby uzyskać nielimitowane',
maxActiveRequestsTip: 'Maksymalna liczba jednoczesnych aktywnych żądań na aplikację (0 dla nieograniczonej)',
gotoAnything: {
actions: {
searchPlugins: 'Szukaj wtyczek',
searchWorkflowNodesHelp: 'Ta funkcja działa tylko podczas wyświetlania przepływu pracy. Najpierw przejdź do przepływu pracy.',
searchApplicationsDesc: 'Wyszukiwanie aplikacji i przechodzenie do nich',
searchPluginsDesc: 'Wyszukiwanie i przechodzenie do wtyczek',
searchApplications: 'Szukaj aplikacji',
searchKnowledgeBasesDesc: 'Wyszukiwanie i przechodzenie do baz wiedzy',
searchWorkflowNodesDesc: 'Znajdowanie węzłów w bieżącym przepływie pracy i przechodzenie do nich według nazwy lub typu',
searchKnowledgeBases: 'Szukaj w bazach wiedzy',
searchWorkflowNodes: 'Wyszukiwanie węzłów przepływu pracy',
},
emptyState: {
noAppsFound: 'Nie znaleziono aplikacji',
noKnowledgeBasesFound: 'Nie znaleziono baz wiedzy',
noWorkflowNodesFound: 'Nie znaleziono węzłów przepływu pracy',
noPluginsFound: 'Nie znaleziono wtyczek',
},
groups: {
apps: 'Aplikacje',
workflowNodes: 'Węzły przepływu pracy',
knowledgeBases: 'Bazy wiedzy',
plugins: 'Wtyczki',
},
useAtForSpecific: 'Użyj @ dla określonych typów',
searchPlaceholder: 'Wyszukaj lub wpisz @ dla poleceń...',
searching: 'Wyszukiwanie...',
noResults: 'Nie znaleziono wyników',
searchTitle: 'Szukaj czegokolwiek',
someServicesUnavailable: 'Niektóre usługi wyszukiwania są niedostępne',
clearToSearchAll: 'Wyczyść @, aby przeszukać wszystko',
searchTemporarilyUnavailable: 'Wyszukiwanie chwilowo niedostępne',
servicesUnavailableMessage: 'W przypadku niektórych usług wyszukiwania mogą występować problemy. Spróbuj ponownie za chwilę.',
searchFailed: 'Wyszukiwanie nie powiodło się',
searchHint: 'Zacznij pisać, aby natychmiast wszystko przeszukać',
commandHint: 'Wpisz @, aby przeglądać według kategorii',
selectSearchType: 'Wybierz, czego chcesz szukać',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
noDependencies: 'Brak zależności',
},
relationsTab: 'Stosunków',
copyLastRun: 'Kopiuj ostatnie uruchomienie',
noLastRunFound: 'Nie znaleziono poprzedniego biegu.',
noMatchingInputsFound: 'Nie znaleziono pasujących danych wejściowych z ostatniego uruchomienia',
copyLastRunError: 'Nie udało się skopiować danych wejściowych z ostatniego uruchomienia',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Digite 0 para ilimitado',
maxActiveRequests: 'Máximo de solicitações simultâneas',
maxActiveRequestsTip: 'Número máximo de solicitações ativas simultâneas por aplicativo (0 para ilimitado)',
gotoAnything: {
actions: {
searchPlugins: 'Pesquisar Plugins',
searchApplicationsDesc: 'Pesquise e navegue até seus aplicativos',
searchPluginsDesc: 'Pesquise e navegue até seus plug-ins',
searchKnowledgeBases: 'Pesquisar bases de conhecimento',
searchApplications: 'Aplicativos de pesquisa',
searchWorkflowNodesDesc: 'Localizar e ir para nós no fluxo de trabalho atual por nome ou tipo',
searchWorkflowNodesHelp: 'Esse recurso só funciona ao visualizar um fluxo de trabalho. Navegue até um fluxo de trabalho primeiro.',
searchKnowledgeBasesDesc: 'Pesquise e navegue até suas bases de conhecimento',
searchWorkflowNodes: 'Nós de fluxo de trabalho de pesquisa',
},
emptyState: {
noAppsFound: 'Nenhum aplicativo encontrado',
noPluginsFound: 'Nenhum plugin encontrado',
noWorkflowNodesFound: 'Nenhum nó de fluxo de trabalho encontrado',
noKnowledgeBasesFound: 'Nenhuma base de conhecimento encontrada',
},
groups: {
apps: 'Apps',
knowledgeBases: 'Bases de conhecimento',
plugins: 'Plugins',
workflowNodes: 'Nós de fluxo de trabalho',
},
searching: 'Procurando...',
searchTitle: 'Pesquisar qualquer coisa',
someServicesUnavailable: 'Alguns serviços de pesquisa indisponíveis',
searchTemporarilyUnavailable: 'Pesquisa temporariamente indisponível',
servicesUnavailableMessage: 'Alguns serviços de pesquisa podem estar enfrentando problemas. Tente novamente em um momento.',
searchPlaceholder: 'Pesquise ou digite @ para comandos...',
noResults: 'Nenhum resultado encontrado',
useAtForSpecific: 'Use @ para tipos específicos',
clearToSearchAll: 'Desmarque @ para pesquisar tudo',
searchFailed: 'Falha na pesquisa',
searchHint: 'Comece a digitar para pesquisar tudo instantaneamente',
commandHint: 'Digite @ para navegar por categoria',
selectSearchType: 'Escolha o que pesquisar',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
noDependencies: 'Sem dependências',
},
relationsTab: 'Relações',
noMatchingInputsFound: 'Nenhuma entrada correspondente encontrada na última execução.',
copyLastRunError: 'Falha ao copiar as entradas da última execução',
noLastRunFound: 'Nenhuma execução anterior encontrada.',
copyLastRun: 'Copiar Última Execução',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Introduceți 0 pentru nelimitat',
maxActiveRequests: 'Maxime cereri simultane',
maxActiveRequestsTip: 'Numărul maxim de cereri active concurente pe aplicație (0 pentru nelimitat)',
gotoAnything: {
actions: {
searchKnowledgeBasesDesc: 'Căutați și navigați la bazele de cunoștințe',
searchWorkflowNodes: 'Căutare în noduri de flux de lucru',
searchKnowledgeBases: 'Căutare în baze de cunoștințe',
searchApplicationsDesc: 'Căutați și navigați la aplicațiile dvs.',
searchApplications: 'Căutare aplicații',
searchPluginsDesc: 'Căutați și navigați la plugin-urile dvs.',
searchWorkflowNodesDesc: 'Găsiți și treceți la nodurile din fluxul de lucru curent după nume sau tip',
searchWorkflowNodesHelp: 'Această caracteristică funcționează numai atunci când vizualizați un flux de lucru. Navigați mai întâi la un flux de lucru.',
searchPlugins: 'Căutare plugin-uri',
},
emptyState: {
noAppsFound: 'Nu s-au găsit aplicații',
noPluginsFound: 'Nu au fost găsite plugin-uri',
noWorkflowNodesFound: 'Nu au fost găsite noduri de flux de lucru',
noKnowledgeBasesFound: 'Nu au fost găsite baze de cunoștințe',
},
groups: {
knowledgeBases: 'Baze de cunoștințe',
workflowNodes: 'Noduri de flux de lucru',
plugins: 'Pluginuri',
apps: 'Aplicații',
},
useAtForSpecific: 'Utilizați @ pentru anumite tipuri',
searchTemporarilyUnavailable: 'Căutare temporar indisponibilă',
searchPlaceholder: 'Căutați sau tastați @ pentru comenzi...',
searchTitle: 'Căutați orice',
searching: 'Căutarea...',
noResults: 'Nu s-au găsit rezultate',
searchFailed: 'Căutarea a eșuat',
servicesUnavailableMessage: 'Este posibil ca unele servicii de căutare să întâmpine probleme. Încercați din nou într-o clipă.',
someServicesUnavailable: 'Unele servicii de căutare nu sunt disponibile',
clearToSearchAll: 'Ștergeți @ pentru a căuta toate',
selectSearchType: 'Alegeți ce să căutați',
commandHint: 'Tastați @ pentru a naviga după categorie',
searchHint: 'Începeți să tastați pentru a căuta totul instantaneu',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependenciesDescription: 'Noduri pe care se bazează acest nod',
},
relationsTab: 'Relații',
noMatchingInputsFound: 'Nu s-au găsit intrări corespunzătoare din ultima rulare',
copyLastRun: 'Copiază ultima execuție',
noLastRunFound: 'Niciun rulament anterior găsit',
copyLastRunError: 'Nu s-au putut copia ultimele intrări de rulare',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequests: 'Максимальное количество параллельных запросов',
maxActiveRequestsPlaceholder: 'Введите 0 для неограниченного количества',
maxActiveRequestsTip: 'Максимальное количество одновременно активных запросов на одно приложение (0 для неограниченного количества)',
gotoAnything: {
actions: {
searchPlugins: 'Поисковые плагины',
searchKnowledgeBases: 'Поиск в базах знаний',
searchApplications: 'Поиск приложений',
searchKnowledgeBasesDesc: 'Поиск и переход к базам знаний',
searchPluginsDesc: 'Поиск и переход к вашим плагинам',
searchWorkflowNodes: 'Поиск узлов рабочего процесса',
searchApplicationsDesc: 'Поиск и переход к приложениям',
searchWorkflowNodesHelp: 'Эта функция работает только при просмотре рабочего процесса. Сначала перейдите к рабочему процессу.',
searchWorkflowNodesDesc: 'Поиск узлов в текущем рабочем процессе и переход к ним по имени или типу',
},
emptyState: {
noPluginsFound: 'Плагины не найдены',
noKnowledgeBasesFound: 'Базы знаний не найдены',
noAppsFound: 'Приложения не найдены',
noWorkflowNodesFound: 'Узлы расчетной схемы не найдены',
},
groups: {
knowledgeBases: 'Базы знаний',
plugins: 'Плагины',
apps: 'Приложения',
workflowNodes: 'Узлы рабочих процессов',
},
searching: 'Поиск...',
noResults: 'Ничего не найдено',
searchFailed: 'Ошибка поиска',
searchTitle: 'Ищите что угодно',
useAtForSpecific: 'Используйте @ для определенных типов',
clearToSearchAll: 'Очистите @ для поиска по всем',
searchTemporarilyUnavailable: 'Поиск временно недоступен',
searchPlaceholder: 'Найдите или введите @ для команд...',
someServicesUnavailable: 'Некоторые поисковые сервисы недоступны',
servicesUnavailableMessage: 'В некоторых поисковых службах могут возникать проблемы. Повторите попытку через мгновение.',
searchHint: 'Начните печатать, чтобы мгновенно искать все',
commandHint: 'Введите @ для просмотра по категориям',
selectSearchType: 'Выберите, что искать',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependenciesDescription: 'Узлы, на которые опирается этот узел',
},
relationsTab: 'Отношения',
noLastRunFound: 'Предыдущий запуск не найден',
copyLastRun: 'Копировать последний запуск',
copyLastRunError: 'Не удалось скопировать последние входные данные выполнения',
noMatchingInputsFound: 'Не найдено соответствующих входных данных из последнего запуска.',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Vnesite 0 za neomejeno',
maxActiveRequests: 'Maksimalno število hkratnih zahtevkov',
maxActiveRequestsTip: 'Največje število hkrati aktivnih zahtevkov na aplikacijo (0 za neomejeno)',
gotoAnything: {
actions: {
searchWorkflowNodes: 'Iskanje vozlišč poteka dela',
searchKnowledgeBasesDesc: 'Iskanje in krmarjenje do zbirk znanja',
searchWorkflowNodesHelp: 'Ta funkcija deluje le pri ogledu poteka dela. Najprej se pomaknite do poteka dela.',
searchApplicationsDesc: 'Iskanje in krmarjenje do aplikacij',
searchPlugins: 'Iskalni vtičniki',
searchApplications: 'Iskanje aplikacij',
searchWorkflowNodesDesc: 'Iskanje vozlišč in skok nanje v trenutnem poteku dela po imenu ali vrsti',
searchKnowledgeBases: 'Iskanje po zbirkah znanja',
searchPluginsDesc: 'Iskanje in krmarjenje do vtičnikov',
},
emptyState: {
noPluginsFound: 'Vtičnikov ni mogoče najti',
noWorkflowNodesFound: 'Vozlišča poteka dela niso bila najdena',
noKnowledgeBasesFound: 'Zbirk znanja ni mogoče najti',
noAppsFound: 'Ni bilo najdenih aplikacij',
},
groups: {
workflowNodes: 'Vozlišča poteka dela',
apps: 'Apps',
knowledgeBases: 'Baze znanja',
plugins: 'Vtičniki',
},
searching: 'Iskanje...',
searchTitle: 'Poiščite karkoli',
searchTemporarilyUnavailable: 'Iskanje začasno ni na voljo',
someServicesUnavailable: 'Nekatere iskalne storitve niso na voljo',
noResults: 'Ni najdenih rezultatov',
clearToSearchAll: 'Počisti @ za iskanje vseh',
searchPlaceholder: 'Poiščite ali vnesite @ za ukaze ...',
searchFailed: 'Iskanje ni uspelo',
useAtForSpecific: 'Uporaba znaka @ za določene vrste',
servicesUnavailableMessage: 'Pri nekaterih iskalnih storitvah se morda pojavljajo težave. Poskusite znova čez trenutek.',
commandHint: 'Vnesite @ za brskanje po kategoriji',
selectSearchType: 'Izberite, kaj želite iskati',
searchHint: 'Začnite tipkati, da takoj preiščete vse',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
noDependencies: 'Brez odvisnosti',
},
relationsTab: 'Odnose',
copyLastRun: 'Kopiraj zadnji zagon',
copyLastRunError: 'Kopiranje vhodov zadnjega zagona ni uspelo',
noLastRunFound: 'Nobenega prejšnjega zagona ni bilo najdenega.',
noMatchingInputsFound: 'Ni podatkov, ki bi ustrezali prejšnjemu zagonu',
},
}

View File

@@ -248,6 +248,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'ใส่ 0 สำหรับไม่จำกัด',
maxActiveRequests: 'จำนวนคำขอพร้อมกันสูงสุด',
maxActiveRequestsTip: 'จำนวนการร้องขอที่ใช้งานพร้อมกันสูงสุดต่อแอป (0 หมายถึงไม่จำกัด)',
gotoAnything: {
actions: {
searchKnowledgeBases: 'ค้นหาฐานความรู้',
searchPlugins: 'ค้นหาปลั๊กอิน',
searchWorkflowNodes: 'ค้นหาโหนดเวิร์กโฟลว์',
searchApplications: 'ค้นหาแอปพลิเคชัน',
searchKnowledgeBasesDesc: 'ค้นหาและนําทางไปยังฐานความรู้ของคุณ',
searchPluginsDesc: 'ค้นหาและนําทางไปยังปลั๊กอินของคุณ',
searchApplicationsDesc: 'ค้นหาและนําทางไปยังแอปพลิเคชันของคุณ',
searchWorkflowNodesHelp: 'คุณลักษณะนี้ใช้ได้เฉพาะเมื่อดูเวิร์กโฟลว์เท่านั้น นําทางไปยังเวิร์กโฟลว์ก่อน',
searchWorkflowNodesDesc: 'ค้นหาและข้ามไปยังโหนดในเวิร์กโฟลว์ปัจจุบันตามชื่อหรือประเภท',
},
emptyState: {
noPluginsFound: 'ไม่พบปลั๊กอิน',
noAppsFound: 'ไม่พบแอป',
noWorkflowNodesFound: 'ไม่พบโหนดเวิร์กโฟลว์',
noKnowledgeBasesFound: 'ไม่พบฐานความรู้',
},
groups: {
apps: 'ปพลิ เค ชัน',
knowledgeBases: 'ฐานความรู้',
plugins: 'ปลั๊กอิน',
workflowNodes: 'โหนดเวิร์กโฟลว์',
},
searchTitle: 'ค้นหาอะไรก็ได้',
searchFailed: 'การค้นหาล้มเหลว',
useAtForSpecific: 'ใช้ @ สําหรับบางประเภท',
noResults: 'ไม่พบผลลัพธ์',
searchTemporarilyUnavailable: 'การค้นหาไม่พร้อมใช้งานชั่วคราว',
someServicesUnavailable: 'บริการค้นหาบางบริการไม่พร้อมใช้งาน',
clearToSearchAll: 'ล้าง @ เพื่อค้นหาทั้งหมด',
searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สําหรับคําสั่ง...',
servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่',
searching: 'กำลังค้นหา...',
searchHint: 'เริ่มพิมพ์เพื่อค้นหาทุกอย่างได้ทันที',
selectSearchType: 'เลือกสิ่งที่จะค้นหา',
commandHint: 'พิมพ์ @ เพื่อเรียกดูตามหมวดหมู่',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependentsDescription: 'โหนดที่อาศัยโหนดนี้',
},
relationsTab: 'สัมพันธ์',
copyLastRun: 'คัดลอกการทำงานล่าสุด',
noLastRunFound: 'ไม่พบการทำงานก่อนหน้า',
copyLastRunError: 'ไม่สามารถคัดลอกข้อมูลการทำงานครั้งสุดท้ายได้',
noMatchingInputsFound: 'ไม่พบข้อมูลที่ตรงกันจากการรันครั้งล่าสุด',
},
}

View File

@@ -248,6 +248,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Sınırsız için 0 girin',
maxActiveRequests: 'Maksimum eş zamanlı istekler',
maxActiveRequestsTip: 'Her uygulama için maksimum eşzamanlı aktif istek sayısı (sınırsız için 0)',
gotoAnything: {
actions: {
searchKnowledgeBasesDesc: 'Bilgi bankalarınızda arama yapın ve bu forumlara gidin',
searchWorkflowNodesDesc: 'Geçerli iş akışındaki düğümleri ada veya türe göre bulun ve atlayın',
searchApplications: 'Arama Uygulamaları',
searchKnowledgeBases: 'Bilgi Bankalarında Ara',
searchWorkflowNodes: 'Arama İş Akışı Düğümleri',
searchPluginsDesc: 'Eklentilerinizi arayın ve eklentilerinize gidin',
searchPlugins: 'Arama Eklentileri',
searchWorkflowNodesHelp: 'Bu özellik yalnızca bir iş akışını görüntülerken çalışır. Önce bir iş akışına gidin.',
searchApplicationsDesc: 'Uygulamalarınızı arayın ve uygulamalarınıza gidin',
},
emptyState: {
noAppsFound: 'Uygulama bulunamadı',
noWorkflowNodesFound: 'İş akışı düğümü bulunamadı',
noKnowledgeBasesFound: 'Bilgi bankası bulunamadı',
noPluginsFound: 'Eklenti bulunamadı',
},
groups: {
apps: 'Apps',
plugins: 'Eklentiler',
knowledgeBases: 'Bilgi Tabanları',
workflowNodes: 'İş Akışı Düğümleri',
},
searchFailed: 'Arama başarısız oldu',
clearToSearchAll: 'Tümünü aramak için @ işaretini kaldırın',
someServicesUnavailable: 'Bazı arama hizmetleri kullanılamıyor',
searchPlaceholder: 'Komutlar için @ arayın veya yazın...',
useAtForSpecific: 'Belirli türler için @ kullanın',
searchTemporarilyUnavailable: 'Arama geçici olarak kullanılamıyor',
searchTitle: 'Her şeyi arayın',
noResults: 'Sonuç bulunamadı',
servicesUnavailableMessage: 'Bazı arama hizmetlerinde sorunlar yaşanıyor olabilir. Kısa bir süre sonra tekrar deneyin.',
searching: 'Araştırıcı...',
selectSearchType: 'Ne arayacağınızı seçin',
searchHint: 'Her şeyi anında aramak için yazmaya başlayın',
commandHint: 'Kategoriye göre göz atmak için @ yazın',
},
}
export default translation

View File

@@ -992,6 +992,10 @@ const translation = {
noDependencies: 'Bağımlılık yok',
},
relationsTab: 'Ilişkiler',
copyLastRun: 'Son Çalışmayı Kopyala',
noLastRunFound: 'Önceki çalışmaya rastlanmadı.',
noMatchingInputsFound: 'Son çalışmadan eşleşen giriş bulunamadı.',
copyLastRunError: 'Son çalışma girdilerini kopyalamak başarısız oldu.',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Введіть 0 для необмеженого',
maxActiveRequests: 'Максимальна кількість одночасних запитів',
maxActiveRequestsTip: 'Максимальна кількість одночасних активних запитів на додаток (0 для необмеженої кількості)',
gotoAnything: {
actions: {
searchApplications: 'Пошук додатків',
searchKnowledgeBases: 'Пошук по базах знань',
searchWorkflowNodes: 'Вузли документообігу пошуку',
searchApplicationsDesc: 'Шукайте та переходьте до своїх програм',
searchPluginsDesc: 'Пошук і навігація до ваших плагінів',
searchWorkflowNodesHelp: 'Ця функція працює лише під час перегляду робочого процесу. Спочатку перейдіть до робочого процесу.',
searchPlugins: 'Пошукові плагіни',
searchKnowledgeBasesDesc: 'Шукайте та переходьте до своїх баз знань',
searchWorkflowNodesDesc: 'Знаходьте вузли в поточному робочому процесі та переходьте до них за іменем або типом',
},
emptyState: {
noPluginsFound: 'Плагінів не знайдено',
noKnowledgeBasesFound: 'Баз знань не знайдено',
noAppsFound: 'Не знайдено додатків',
noWorkflowNodesFound: 'Вузли бізнес-процесу не знайдено',
},
groups: {
knowledgeBases: 'Бази знань',
plugins: 'Плагіни',
apps: 'Програми',
workflowNodes: 'Вузли документообігу',
},
searching: 'Пошук...',
searchTitle: 'Шукайте що завгодно',
searchFailed: 'Пошук не вдався',
clearToSearchAll: 'Clear @ для пошуку всіх',
noResults: 'Результатів не знайдено',
searchPlaceholder: 'Виконайте пошук або введіть @ для команд...',
searchTemporarilyUnavailable: 'Пошук тимчасово недоступний',
useAtForSpecific: 'Використовуйте @ для конкретних типів',
someServicesUnavailable: 'Деякі пошукові сервіси недоступні',
servicesUnavailableMessage: 'У деяких пошукових службах можуть виникати проблеми. Повторіть спробу за мить.',
selectSearchType: 'Виберіть, що шукати',
commandHint: 'Введіть @ для навігації за категоріями',
searchHint: 'Почніть вводити текст, щоб миттєво шукати все',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependentsDescription: 'Вузли, які спираються на цей вузол',
},
relationsTab: 'Відносин',
copyLastRun: 'Копіювати останній запуск',
noLastRunFound: 'Жодного попереднього запуску не знайдено.',
copyLastRunError: 'Не вдалося скопіювати вхідні дані останнього виконання',
noMatchingInputsFound: 'Не знайдено відповідних вхідних даних з останнього запуску',
},
}

View File

@@ -252,6 +252,44 @@ const translation = {
maxActiveRequestsPlaceholder: 'Nhập 0 để không giới hạn',
maxActiveRequests: 'Số yêu cầu đồng thời tối đa',
maxActiveRequestsTip: 'Số yêu cầu hoạt động đồng thời tối đa cho mỗi ứng dụng (0 để không giới hạn)',
gotoAnything: {
actions: {
searchPlugins: 'Tìm kiếm Plugin',
searchPluginsDesc: 'Tìm kiếm và điều hướng đến plugin của bạn',
searchKnowledgeBases: 'Tìm kiếm cơ sở kiến thức',
searchApplicationsDesc: 'Tìm kiếm và điều hướng đến các ứng dụng của bạn',
searchWorkflowNodesHelp: 'Tính năng này chỉ hoạt động khi xem quy trình làm việc. Điều hướng đến quy trình làm việc trước.',
searchWorkflowNodes: 'Tìm kiếm các nút quy trình làm việc',
searchApplications: 'Tìm kiếm ứng dụng',
searchWorkflowNodesDesc: 'Tìm và chuyển đến các nút trong quy trình làm việc hiện tại theo tên hoặc loại',
searchKnowledgeBasesDesc: 'Tìm kiếm và điều hướng đến cơ sở kiến thức của bạn',
},
emptyState: {
noWorkflowNodesFound: 'Không tìm thấy nút quy trình làm việc',
noKnowledgeBasesFound: 'Không tìm thấy cơ sở kiến thức',
noPluginsFound: 'Không tìm thấy plugin',
noAppsFound: 'Không tìm thấy ứng dụng nào',
},
groups: {
plugins: 'Plugin',
workflowNodes: 'Nút quy trình làm việc',
knowledgeBases: 'Cơ sở kiến thức',
apps: 'Ứng dụng',
},
searchTemporarilyUnavailable: 'Tìm kiếm tạm thời không khả dụng',
clearToSearchAll: 'Xóa @ để tìm kiếm tất cả',
noResults: 'Không tìm thấy kết quả',
searching: 'Tìm kiếm...',
searchPlaceholder: 'Tìm kiếm hoặc nhập @ cho các lệnh...',
searchTitle: 'Tìm kiếm bất cứ thứ gì',
searchFailed: 'Tìm kiếm không thành công',
useAtForSpecific: 'Sử dụng @ cho các loại cụ thể',
someServicesUnavailable: 'Một số dịch vụ tìm kiếm không khả dụng',
servicesUnavailableMessage: 'Một số dịch vụ tìm kiếm có thể gặp sự cố. Thử lại trong giây lát.',
searchHint: 'Bắt đầu nhập để tìm kiếm mọi thứ ngay lập tức',
commandHint: 'Nhập @ để duyệt theo danh mục',
selectSearchType: 'Chọn nội dung để tìm kiếm',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
dependentsDescription: 'Các nút dựa vào nút này',
},
relationsTab: 'Mối quan hệ',
noLastRunFound: 'Không tìm thấy lần chạy trước',
noMatchingInputsFound: 'Không tìm thấy đầu vào nào khớp từ lần chạy trước',
copyLastRun: 'Sao chép lần chạy cuối',
copyLastRunError: 'Không thể sao chép đầu vào của lần chạy trước',
},
}

View File

@@ -251,6 +251,49 @@ const translation = {
maxActiveRequests: '最大活跃请求数',
maxActiveRequestsPlaceholder: '0 表示不限制',
maxActiveRequestsTip: '当前应用的最大活跃请求数0 表示不限制)',
gotoAnything: {
searchPlaceholder: '搜索或输入 @ 以使用命令...',
searchTitle: '搜索任何内容',
searching: '搜索中...',
noResults: '未找到结果',
searchFailed: '搜索失败',
searchTemporarilyUnavailable: '搜索暂时不可用',
servicesUnavailableMessage: '某些搜索服务可能遇到问题,请稍后再试。',
someServicesUnavailable: '某些搜索服务不可用',
resultCount: '{{count}} 个结果',
resultCount_other: '{{count}} 个结果',
inScope: '在 {{scope}}s 中',
clearToSearchAll: '清除 @ 以搜索全部',
useAtForSpecific: '使用 @ 进行特定类型搜索',
selectSearchType: '选择搜索内容',
searchHint: '开始输入即可立即搜索所有内容',
commandHint: '输入 @ 按类别浏览',
actions: {
searchApplications: '搜索应用程序',
searchApplicationsDesc: '搜索并导航到您的应用程序',
searchPlugins: '搜索插件',
searchPluginsDesc: '搜索并导航到您的插件',
searchKnowledgeBases: '搜索知识库',
searchKnowledgeBasesDesc: '搜索并导航到您的知识库',
searchWorkflowNodes: '搜索工作流节点',
searchWorkflowNodesDesc: '按名称或类型查找并跳转到当前工作流中的节点',
searchWorkflowNodesHelp: '此功能仅在查看工作流时有效。首先导航到工作流。',
},
emptyState: {
noAppsFound: '未找到应用',
noPluginsFound: '未找到插件',
noKnowledgeBasesFound: '未找到知识库',
noWorkflowNodesFound: '未找到工作流节点',
tryDifferentTerm: '尝试不同的搜索词或移除 {{mode}} 过滤器',
trySpecificSearch: '尝试使用 {{shortcuts}} 进行特定搜索',
},
groups: {
apps: '应用程序',
plugins: '插件',
knowledgeBases: '知识库',
workflowNodes: '工作流节点',
},
},
}
export default translation

View File

@@ -958,6 +958,11 @@ const translation = {
settingsTab: '设置',
lastRunTab: '上次运行',
relationsTab: '关系',
copyLastRun: '复制上次运行值',
noLastRunFound: '未找到上次运行记录',
noMatchingInputsFound: '上次运行中未找到匹配的输入',
lastRunInputsCopied: '已复制{{count}}个输入值',
copyLastRunError: '复制上次运行输入失败',
noData: {
description: '上次运行的结果将显示在这里',
runThisNode: '运行此节点',

View File

@@ -251,6 +251,44 @@ const translation = {
maxActiveRequestsPlaceholder: '輸入 0 以表示無限',
maxActiveRequests: '同時最大請求數',
maxActiveRequestsTip: '每個應用程式可同時活躍請求的最大數量0為無限制',
gotoAnything: {
actions: {
searchWorkflowNodes: '搜索工作流節點',
searchPluginsDesc: '搜索並導航到您的外掛程式',
searchApplications: '搜索應用程式',
searchKnowledgeBases: '搜索知識庫',
searchKnowledgeBasesDesc: '搜索並導航到您的知識庫',
searchWorkflowNodesHelp: '此功能僅在查看工作流時有效。首先導航到工作流。',
searchApplicationsDesc: '搜索並導航到您的應用程式',
searchPlugins: '搜索外掛程式',
searchWorkflowNodesDesc: '按名稱或類型查找並跳轉到當前工作流中的節點',
},
emptyState: {
noAppsFound: '未找到應用',
noWorkflowNodesFound: '未找到工作流節點',
noKnowledgeBasesFound: '未找到知識庫',
noPluginsFound: '未找到外掛程式',
},
groups: {
apps: '應用程式',
knowledgeBases: '知識庫',
plugins: '外掛程式',
workflowNodes: '工作流節點',
},
searchPlaceholder: '搜尋或鍵入 @ 以取得命令...',
searching: '搜索。。。',
searchTitle: '搜索任何內容',
noResults: '未找到結果',
clearToSearchAll: '清除 @ 以搜尋全部',
searchFailed: '搜索失敗',
servicesUnavailableMessage: '某些搜索服務可能遇到問題。稍後再試一次。',
someServicesUnavailable: '某些搜索服務不可用',
useAtForSpecific: '對特定類型使用 @',
searchTemporarilyUnavailable: '搜索暫時不可用',
selectSearchType: '選擇要搜索的內容',
commandHint: '鍵入 @ 按類別流覽',
searchHint: '開始輸入以立即搜索所有內容',
},
}
export default translation

View File

@@ -991,6 +991,10 @@ const translation = {
noDependencies: '無依賴',
noDependents: '無被依賴',
},
copyLastRun: '複製上一次運行',
copyLastRunError: '未能複製上一次運行的輸入',
noMatchingInputsFound: '在上次運行中未找到匹配的輸入',
noLastRunFound: '沒有找到之前的運行',
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "1.7.1",
"version": "1.7.2",
"private": true,
"engines": {
"node": ">=v22.11.0"
@@ -75,6 +75,7 @@
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"copy-to-clipboard": "^3.3.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",

446
web/pnpm-lock.yaml generated
View File

@@ -153,6 +153,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
copy-to-clipboard:
specifier: ^3.3.3
version: 3.3.3
@@ -2471,6 +2474,177 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.14':
resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==}
peerDependencies:
'@types/react': ~19.1.8
'@types/react-dom': ~19.1.6
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-dismissable-layer@1.1.10':
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
peerDependencies:
'@types/react': ~19.1.8
'@types/react-dom': ~19.1.6
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.1.2':
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.1.7':
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
peerDependencies:
'@types/react': ~19.1.8
'@types/react-dom': ~19.1.6
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-portal@1.1.9':
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
peerDependencies:
'@types/react': ~19.1.8
'@types/react-dom': ~19.1.6
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.4':
resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
peerDependencies:
'@types/react': ~19.1.8
'@types/react-dom': ~19.1.6
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': ~19.1.8
'@types/react-dom': ~19.1.6
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.1':
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@react-aria/focus@3.20.5':
resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==}
peerDependencies:
@@ -3651,6 +3825,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -4042,6 +4220,12 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cmdk@1.1.1:
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -4490,6 +4674,9 @@ packages:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
devalue@5.1.1:
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
@@ -5252,6 +5439,10 @@ packages:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-package-type@0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
@@ -7068,6 +7259,26 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.7.1:
resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
react-rnd@10.5.2:
resolution: {integrity: sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==}
peerDependencies:
@@ -7087,6 +7298,16 @@ packages:
react-dom: '>=16.9.0'
sortablejs: '1'
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
react-syntax-highlighter@15.6.1:
resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==}
peerDependencies:
@@ -7959,6 +8180,16 @@ packages:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'}
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
use-composed-ref@1.4.0:
resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
peerDependencies:
@@ -7992,6 +8223,16 @@ packages:
'@types/react':
optional: true
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ~19.1.8
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
use-strict@1.0.1:
resolution: {integrity: sha512-IeiWvvEXfW5ltKVMkxq6FvNf2LojMKvB2OCeja6+ct24S1XOmQw2dGr2JyndwACWAGJva9B7yPHwAmeA9QCqAQ==}
@@ -10470,6 +10711,149 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@radix-ui/primitive@1.1.2': {}
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-context@1.1.2(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
aria-hidden: 1.2.6
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-id@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.8)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.8)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.8
'@react-aria/focus@3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -11968,6 +12352,10 @@ snapshots:
argparse@2.0.1: {}
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
@@ -12388,6 +12776,18 @@ snapshots:
clsx@2.1.1: {}
cmdk@1.1.1(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
co@4.6.0: {}
code-inspector-core@0.18.3:
@@ -12862,6 +13262,8 @@ snapshots:
detect-newline@3.1.0: {}
detect-node-es@1.1.0: {}
devalue@5.1.1: {}
devlop@1.1.0:
@@ -13887,6 +14289,8 @@ snapshots:
get-east-asian-width@1.3.0: {}
get-nonce@1.0.1: {}
get-package-type@0.1.0: {}
get-stream@5.2.0:
@@ -16275,6 +16679,25 @@ snapshots:
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0):
dependencies:
react: 19.1.0
react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0)
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.1.8
react-remove-scroll@2.7.1(@types/react@19.1.8)(react@19.1.0):
dependencies:
react: 19.1.0
react-remove-scroll-bar: 2.3.8(@types/react@19.1.8)(react@19.1.0)
react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0)
tslib: 2.8.1
use-callback-ref: 1.3.3(@types/react@19.1.8)(react@19.1.0)
use-sidecar: 1.1.3(@types/react@19.1.8)(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
react-rnd@10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
re-resizable: 6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -16297,6 +16720,14 @@ snapshots:
sortablejs: 1.15.6
tiny-invariant: 1.2.0
react-style-singleton@2.2.3(@types/react@19.1.8)(react@19.1.0):
dependencies:
get-nonce: 1.0.1
react: 19.1.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.1.8
react-syntax-highlighter@15.6.1(react@19.1.0):
dependencies:
'@babel/runtime': 7.27.6
@@ -17322,6 +17753,13 @@ snapshots:
punycode: 1.4.1
qs: 6.14.0
use-callback-ref@1.3.3(@types/react@19.1.8)(react@19.1.0):
dependencies:
react: 19.1.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.1.8
use-composed-ref@1.4.0(@types/react@19.1.8)(react@19.1.0):
dependencies:
react: 19.1.0
@@ -17346,6 +17784,14 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0):
dependencies:
detect-node-es: 1.1.0
react: 19.1.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.1.8
use-strict@1.0.1: {}
use-sync-external-store@1.5.0(react@19.1.0):

View File

@@ -1,17 +1,25 @@
import type { AppMode } from '@/types/app'
export const getRedirectionPath = (
isCurrentWorkspaceEditor: boolean,
app: { id: string, mode: AppMode },
) => {
if (!isCurrentWorkspaceEditor) {
return `/app/${app.id}/overview`
}
else {
if (app.mode === 'workflow' || app.mode === 'advanced-chat')
return `/app/${app.id}/workflow`
else
return `/app/${app.id}/configuration`
}
}
export const getRedirection = (
isCurrentWorkspaceEditor: boolean,
app: { id: string, mode: AppMode },
redirectionFunc: (href: string) => void,
) => {
if (!isCurrentWorkspaceEditor) {
redirectionFunc(`/app/${app.id}/overview`)
}
else {
if (app.mode === 'workflow' || app.mode === 'advanced-chat')
redirectionFunc(`/app/${app.id}/workflow`)
else
redirectionFunc(`/app/${app.id}/configuration`)
}
const redirectionPath = getRedirectionPath(isCurrentWorkspaceEditor, app)
redirectionFunc(redirectionPath)
}