mirror of
https://github.com/langgenius/dify.git
synced 2026-02-08 09:14:28 +00:00
Compare commits
15 Commits
config-too
...
1.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0baccb9e82 | ||
|
|
2c81db5a1c | ||
|
|
43411d7a9e | ||
|
|
2dbf20a3e9 | ||
|
|
aaf9fc1562 | ||
|
|
d30f898274 | ||
|
|
4a72fa6268 | ||
|
|
0c5e66bccb | ||
|
|
ff791efe18 | ||
|
|
6083b1d618 | ||
|
|
69c3439c3a | ||
|
|
7ee170f0a7 | ||
|
|
36b221b170 | ||
|
|
d1fc98200c | ||
|
|
bb852ef6d2 |
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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
10
api/uv.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
197
web/__tests__/goto-anything/search-error-handling.test.ts
Normal file
197
web/__tests__/goto-anything/search-error-handling.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
61
web/app/components/base/select/locale-signin.tsx
Normal file
61
web/app/components/base/select/locale-signin.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
58
web/app/components/goto-anything/actions/app.tsx
Normal file
58
web/app/components/goto-anything/actions/app.tsx
Normal 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 []
|
||||
}
|
||||
},
|
||||
}
|
||||
74
web/app/components/goto-anything/actions/index.ts
Normal file
74
web/app/components/goto-anything/actions/index.ts
Normal 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 }
|
||||
56
web/app/components/goto-anything/actions/knowledge.tsx
Normal file
56
web/app/components/goto-anything/actions/knowledge.tsx
Normal 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 []
|
||||
}
|
||||
},
|
||||
}
|
||||
53
web/app/components/goto-anything/actions/plugin.tsx
Normal file
53
web/app/components/goto-anything/actions/plugin.tsx
Normal 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 []
|
||||
}
|
||||
},
|
||||
}
|
||||
54
web/app/components/goto-anything/actions/types.ts
Normal file
54
web/app/components/goto-anything/actions/types.ts
Normal 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[])
|
||||
}
|
||||
24
web/app/components/goto-anything/actions/workflow-nodes.tsx
Normal file
24
web/app/components/goto-anything/actions/workflow-nodes.tsx
Normal 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 []
|
||||
}
|
||||
},
|
||||
}
|
||||
51
web/app/components/goto-anything/command-selector.tsx
Normal file
51
web/app/components/goto-anything/command-selector.tsx
Normal 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
|
||||
50
web/app/components/goto-anything/context.tsx
Normal file
50
web/app/components/goto-anything/context.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
403
web/app/components/goto-anything/index.tsx
Normal file
403
web/app/components/goto-anything/index.tsx
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
135
web/app/components/workflow/hooks/use-workflow-search.tsx
Normal file
135
web/app/components/workflow/hooks/use-workflow-search.tsx
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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('/')
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
124
web/app/components/workflow/utils/node-navigation.ts
Normal file
124
web/app/components/workflow/utils/node-navigation.ts
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
17
web/app/signin/assets/github-dark.svg
Normal file
17
web/app/signin/assets/github-dark.svg
Normal 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 |
@@ -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>}
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -991,6 +991,10 @@ const translation = {
|
||||
dependentsDescription: 'گره هایی که به این گره متکی هستند',
|
||||
},
|
||||
relationsTab: 'روابط',
|
||||
copyLastRun: 'کپی آخرین اجرا',
|
||||
noLastRunFound: 'هیچ اجرای قبلی یافت نشد',
|
||||
noMatchingInputsFound: 'هیچ ورودی مطابقی از آخرین اجرا یافت نشد',
|
||||
copyLastRunError: 'نتوانستم ورودیهای آخرین اجرای را کپی کنم',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 l’affichage d’un flux de travail. Accédez d’abord à 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 n’importe 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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1011,6 +1011,10 @@ const translation = {
|
||||
noDependencies: 'कोई निर्भरताएँ नहीं',
|
||||
},
|
||||
relationsTab: 'रिश्ते',
|
||||
copyLastRun: 'अंतिम रन कॉपी करें',
|
||||
noLastRunFound: 'कोई पिछला रन नहीं मिला',
|
||||
noMatchingInputsFound: 'अंतिम रन से कोई मेल खाने वाले इनपुट नहीं मिले',
|
||||
copyLastRunError: 'अंतिम रन इनपुट को कॉपी करने में विफल',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -991,6 +991,10 @@ const translation = {
|
||||
noDependencies: '依存元なし',
|
||||
noDependents: '依存先なし',
|
||||
},
|
||||
copyLastRun: '最後の実行をコピー',
|
||||
noLastRunFound: '以前の実行が見つかりませんでした。',
|
||||
copyLastRunError: '最後の実行の入力をコピーできませんでした',
|
||||
noMatchingInputsFound: '前回の実行から一致する入力が見つかりませんでした。',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1042,6 +1042,10 @@ const translation = {
|
||||
dependenciesDescription: '이 노드가 의존하는 노드',
|
||||
},
|
||||
relationsTab: '관계',
|
||||
copyLastRun: '마지막 실행 복사',
|
||||
noLastRunFound: '이전 실행이 없습니다.',
|
||||
noMatchingInputsFound: '지난 실행에서 일치하는 입력을 찾을 수 없습니다.',
|
||||
copyLastRunError: '마지막 실행 입력을 복사하는 데 실패했습니다.',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -991,6 +991,10 @@ const translation = {
|
||||
dependenciesDescription: 'Узлы, на которые опирается этот узел',
|
||||
},
|
||||
relationsTab: 'Отношения',
|
||||
noLastRunFound: 'Предыдущий запуск не найден',
|
||||
copyLastRun: 'Копировать последний запуск',
|
||||
copyLastRunError: 'Не удалось скопировать последние входные данные выполнения',
|
||||
noMatchingInputsFound: 'Не найдено соответствующих входных данных из последнего запуска.',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -991,6 +991,10 @@ const translation = {
|
||||
dependentsDescription: 'โหนดที่อาศัยโหนดนี้',
|
||||
},
|
||||
relationsTab: 'สัมพันธ์',
|
||||
copyLastRun: 'คัดลอกการทำงานล่าสุด',
|
||||
noLastRunFound: 'ไม่พบการทำงานก่อนหน้า',
|
||||
copyLastRunError: 'ไม่สามารถคัดลอกข้อมูลการทำงานครั้งสุดท้ายได้',
|
||||
noMatchingInputsFound: 'ไม่พบข้อมูลที่ตรงกันจากการรันครั้งล่าสุด',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -991,6 +991,10 @@ const translation = {
|
||||
dependentsDescription: 'Вузли, які спираються на цей вузол',
|
||||
},
|
||||
relationsTab: 'Відносин',
|
||||
copyLastRun: 'Копіювати останній запуск',
|
||||
noLastRunFound: 'Жодного попереднього запуску не знайдено.',
|
||||
copyLastRunError: 'Не вдалося скопіювати вхідні дані останнього виконання',
|
||||
noMatchingInputsFound: 'Не знайдено відповідних вхідних даних з останнього запуску',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -958,6 +958,11 @@ const translation = {
|
||||
settingsTab: '设置',
|
||||
lastRunTab: '上次运行',
|
||||
relationsTab: '关系',
|
||||
copyLastRun: '复制上次运行值',
|
||||
noLastRunFound: '未找到上次运行记录',
|
||||
noMatchingInputsFound: '上次运行中未找到匹配的输入',
|
||||
lastRunInputsCopied: '已复制{{count}}个输入值',
|
||||
copyLastRunError: '复制上次运行输入失败',
|
||||
noData: {
|
||||
description: '上次运行的结果将显示在这里',
|
||||
runThisNode: '运行此节点',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -991,6 +991,10 @@ const translation = {
|
||||
noDependencies: '無依賴',
|
||||
noDependents: '無被依賴',
|
||||
},
|
||||
copyLastRun: '複製上一次運行',
|
||||
copyLastRunError: '未能複製上一次運行的輸入',
|
||||
noMatchingInputsFound: '在上次運行中未找到匹配的輸入',
|
||||
noLastRunFound: '沒有找到之前的運行',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
446
web/pnpm-lock.yaml
generated
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user