mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/trigger
This commit is contained in:
commit
85f1cf1d90
|
|
@ -373,11 +373,11 @@ class HttpConfig(BaseSettings):
|
|||
)
|
||||
|
||||
HTTP_REQUEST_MAX_READ_TIMEOUT: int = Field(
|
||||
ge=1, description="Maximum read timeout in seconds for HTTP requests", default=60
|
||||
ge=1, description="Maximum read timeout in seconds for HTTP requests", default=600
|
||||
)
|
||||
|
||||
HTTP_REQUEST_MAX_WRITE_TIMEOUT: int = Field(
|
||||
ge=1, description="Maximum write timeout in seconds for HTTP requests", default=20
|
||||
ge=1, description="Maximum write timeout in seconds for HTTP requests", default=600
|
||||
)
|
||||
|
||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: PositiveInt = Field(
|
||||
|
|
@ -782,7 +782,7 @@ class MailConfig(BaseSettings):
|
|||
|
||||
MAIL_TEMPLATING_TIMEOUT: int = Field(
|
||||
description="""
|
||||
Timeout for email templating in seconds. Used to prevent infinite loops in malicious templates.
|
||||
Timeout for email templating in seconds. Used to prevent infinite loops in malicious templates.
|
||||
Only available in sandbox mode.""",
|
||||
default=3,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from configs import dify_config
|
||||
from libs.collection_utils import convert_to_lower_and_upper_set
|
||||
|
||||
HIDDEN_VALUE = "[__HIDDEN__]"
|
||||
UNKNOWN_VALUE = "[__UNKNOWN__]"
|
||||
|
|
@ -6,24 +7,39 @@ UUID_NIL = "00000000-0000-0000-0000-000000000000"
|
|||
|
||||
DEFAULT_FILE_NUMBER_LIMITS = 3
|
||||
|
||||
IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "gif", "svg"]
|
||||
IMAGE_EXTENSIONS.extend([ext.upper() for ext in IMAGE_EXTENSIONS])
|
||||
IMAGE_EXTENSIONS = convert_to_lower_and_upper_set({"jpg", "jpeg", "png", "webp", "gif", "svg"})
|
||||
|
||||
VIDEO_EXTENSIONS = ["mp4", "mov", "mpeg", "webm"]
|
||||
VIDEO_EXTENSIONS.extend([ext.upper() for ext in VIDEO_EXTENSIONS])
|
||||
VIDEO_EXTENSIONS = convert_to_lower_and_upper_set({"mp4", "mov", "mpeg", "webm"})
|
||||
|
||||
AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "amr", "mpga"]
|
||||
AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
|
||||
AUDIO_EXTENSIONS = convert_to_lower_and_upper_set({"mp3", "m4a", "wav", "amr", "mpga"})
|
||||
|
||||
|
||||
_doc_extensions: list[str]
|
||||
_doc_extensions: set[str]
|
||||
if dify_config.ETL_TYPE == "Unstructured":
|
||||
_doc_extensions = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "vtt", "properties"]
|
||||
_doc_extensions.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
|
||||
_doc_extensions = {
|
||||
"txt",
|
||||
"markdown",
|
||||
"md",
|
||||
"mdx",
|
||||
"pdf",
|
||||
"html",
|
||||
"htm",
|
||||
"xlsx",
|
||||
"xls",
|
||||
"vtt",
|
||||
"properties",
|
||||
"doc",
|
||||
"docx",
|
||||
"csv",
|
||||
"eml",
|
||||
"msg",
|
||||
"pptx",
|
||||
"xml",
|
||||
"epub",
|
||||
}
|
||||
if dify_config.UNSTRUCTURED_API_URL:
|
||||
_doc_extensions.append("ppt")
|
||||
_doc_extensions.add("ppt")
|
||||
else:
|
||||
_doc_extensions = [
|
||||
_doc_extensions = {
|
||||
"txt",
|
||||
"markdown",
|
||||
"md",
|
||||
|
|
@ -37,5 +53,5 @@ else:
|
|||
"csv",
|
||||
"vtt",
|
||||
"properties",
|
||||
]
|
||||
DOCUMENT_EXTENSIONS = _doc_extensions + [ext.upper() for ext in _doc_extensions]
|
||||
}
|
||||
DOCUMENT_EXTENSIONS: set[str] = convert_to_lower_and_upper_set(_doc_extensions)
|
||||
|
|
|
|||
|
|
@ -61,9 +61,6 @@ class AppRunner:
|
|||
if model_context_tokens is None:
|
||||
return -1
|
||||
|
||||
if max_tokens is None:
|
||||
max_tokens = 0
|
||||
|
||||
prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
|
||||
|
||||
if prompt_tokens + max_tokens > model_context_tokens:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from collections.abc import Generator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TypeVar, Union, cast
|
||||
from typing import TypeVar, Union
|
||||
|
||||
from core.agent.entities import AgentInvokeMessage
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
|
|
@ -87,7 +87,8 @@ def merge_blob_chunks(
|
|||
),
|
||||
meta=resp.meta,
|
||||
)
|
||||
yield cast(MessageType, merged_message)
|
||||
assert isinstance(merged_message, (ToolInvokeMessage, AgentInvokeMessage))
|
||||
yield merged_message # type: ignore
|
||||
# Clean up the buffer
|
||||
del files[chunk_id]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -342,10 +342,13 @@ class IterationNode(Node):
|
|||
iterator_list_value: Sequence[object],
|
||||
iter_run_map: dict[str, float],
|
||||
) -> Generator[NodeEventBase, None, None]:
|
||||
# Flatten the list of lists if all outputs are lists
|
||||
flattened_outputs = self._flatten_outputs_if_needed(outputs)
|
||||
|
||||
yield IterationSucceededEvent(
|
||||
start_at=started_at,
|
||||
inputs=inputs,
|
||||
outputs={"output": outputs},
|
||||
outputs={"output": flattened_outputs},
|
||||
steps=len(iterator_list_value),
|
||||
metadata={
|
||||
WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens,
|
||||
|
|
@ -357,13 +360,39 @@ class IterationNode(Node):
|
|||
yield StreamCompletedEvent(
|
||||
node_run_result=NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||
outputs={"output": outputs},
|
||||
outputs={"output": flattened_outputs},
|
||||
metadata={
|
||||
WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def _flatten_outputs_if_needed(self, outputs: list[object]) -> list[object]:
|
||||
"""
|
||||
Flatten the outputs list if all elements are lists.
|
||||
This maintains backward compatibility with version 1.8.1 behavior.
|
||||
"""
|
||||
if not outputs:
|
||||
return outputs
|
||||
|
||||
# Check if all non-None outputs are lists
|
||||
non_none_outputs = [output for output in outputs if output is not None]
|
||||
if not non_none_outputs:
|
||||
return outputs
|
||||
|
||||
if all(isinstance(output, list) for output in non_none_outputs):
|
||||
# Flatten the list of lists
|
||||
flattened: list[Any] = []
|
||||
for output in outputs:
|
||||
if isinstance(output, list):
|
||||
flattened.extend(output)
|
||||
elif output is not None:
|
||||
# This shouldn't happen based on our check, but handle it gracefully
|
||||
flattened.append(output)
|
||||
return flattened
|
||||
|
||||
return outputs
|
||||
|
||||
def _handle_iteration_failure(
|
||||
self,
|
||||
started_at: datetime,
|
||||
|
|
@ -373,10 +402,13 @@ class IterationNode(Node):
|
|||
iter_run_map: dict[str, float],
|
||||
error: IterationNodeError,
|
||||
) -> Generator[NodeEventBase, None, None]:
|
||||
# Flatten the list of lists if all outputs are lists (even in failure case)
|
||||
flattened_outputs = self._flatten_outputs_if_needed(outputs)
|
||||
|
||||
yield IterationFailedEvent(
|
||||
start_at=started_at,
|
||||
inputs=inputs,
|
||||
outputs={"output": outputs},
|
||||
outputs={"output": flattened_outputs},
|
||||
steps=len(iterator_list_value),
|
||||
metadata={
|
||||
WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: self.graph_runtime_state.total_tokens,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import datetime
|
|||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ class KnowledgeIndexNode(Node):
|
|||
return self._node_data
|
||||
|
||||
def _run(self) -> NodeRunResult: # type: ignore
|
||||
node_data = cast(KnowledgeIndexNodeData, self._node_data)
|
||||
node_data = self._node_data
|
||||
variable_pool = self.graph_runtime_state.variable_pool
|
||||
dataset_id = variable_pool.get(["sys", SystemVariableKey.DATASET_ID])
|
||||
if not dataset_id:
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ def init_app(app: DifyApp):
|
|||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPSpanExporter
|
||||
from opentelemetry.instrumentation.celery import CeleryInstrumentor
|
||||
from opentelemetry.instrumentation.flask import FlaskInstrumentor
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
||||
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
||||
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
||||
|
|
@ -238,6 +239,7 @@ def init_app(app: DifyApp):
|
|||
init_sqlalchemy_instrumentor(app)
|
||||
RedisInstrumentor().instrument()
|
||||
RequestsInstrumentor().instrument()
|
||||
HTTPXClientInstrumentor().instrument()
|
||||
atexit.register(shutdown_tracer)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
def convert_to_lower_and_upper_set(inputs: list[str] | set[str]) -> set[str]:
|
||||
"""
|
||||
Convert a list or set of strings to a set containing both lower and upper case versions of each string.
|
||||
|
||||
Args:
|
||||
inputs (list[str] | set[str]): A list or set of strings to be converted.
|
||||
|
||||
Returns:
|
||||
set[str]: A set containing both lower and upper case versions of each string.
|
||||
"""
|
||||
if not inputs:
|
||||
return set()
|
||||
else:
|
||||
return {case for s in inputs if s for case in (s.lower(), s.upper())}
|
||||
|
|
@ -46,6 +46,7 @@ dependencies = [
|
|||
"opentelemetry-instrumentation==0.48b0",
|
||||
"opentelemetry-instrumentation-celery==0.48b0",
|
||||
"opentelemetry-instrumentation-flask==0.48b0",
|
||||
"opentelemetry-instrumentation-httpx==0.48b0",
|
||||
"opentelemetry-instrumentation-redis==0.48b0",
|
||||
"opentelemetry-instrumentation-requests==0.48b0",
|
||||
"opentelemetry-instrumentation-sqlalchemy==0.48b0",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"flask_login",
|
||||
"opentelemetry.instrumentation.celery",
|
||||
"opentelemetry.instrumentation.flask",
|
||||
"opentelemetry.instrumentation.httpx",
|
||||
"opentelemetry.instrumentation.requests",
|
||||
"opentelemetry.instrumentation.sqlalchemy",
|
||||
"opentelemetry.instrumentation.redis"
|
||||
|
|
@ -23,9 +24,7 @@
|
|||
"reportUnknownLambdaType": "hint",
|
||||
"reportMissingParameterType": "hint",
|
||||
"reportMissingTypeArgument": "hint",
|
||||
"reportUnnecessaryContains": "hint",
|
||||
"reportUnnecessaryComparison": "hint",
|
||||
"reportUnnecessaryCast": "hint",
|
||||
"reportUnnecessaryIsInstance": "hint",
|
||||
"reportUntypedFunctionDecorator": "hint",
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,7 @@ class RagPipelineTransformService:
|
|||
file_extensions = node.get("data", {}).get("fileExtensions", [])
|
||||
if not file_extensions:
|
||||
return node
|
||||
file_extensions = [file_extension.lower() for file_extension in file_extensions]
|
||||
node["data"]["fileExtensions"] = DOCUMENT_EXTENSIONS
|
||||
node["data"]["fileExtensions"] = [ext.lower() for ext in file_extensions if ext in DOCUMENT_EXTENSIONS]
|
||||
return node
|
||||
|
||||
def _deal_knowledge_index(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
|
@ -55,7 +55,7 @@ class MCPToolManageService:
|
|||
cache=NoOpProviderCredentialCache(),
|
||||
)
|
||||
|
||||
return cast(dict[str, str], encrypter_instance.encrypt(headers))
|
||||
return encrypter_instance.encrypt(headers)
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_provider_by_provider_id(provider_id: str, tenant_id: str) -> MCPToolProvider:
|
||||
|
|
|
|||
|
|
@ -5,15 +5,10 @@ These tasks provide asynchronous storage capabilities for workflow execution dat
|
|||
improving performance by offloading storage operations to background workers.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from celery import shared_task # type: ignore[import-untyped]
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
from services.workflow_draft_variable_service import DraftVarFileDeletion, WorkflowDraftVariableService
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
|
|||
# Set environment variables using monkeypatch
|
||||
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
|
||||
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
|
||||
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30")
|
||||
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30") # Custom value for testing
|
||||
monkeypatch.setenv("DB_USERNAME", "postgres")
|
||||
monkeypatch.setenv("DB_PASSWORD", "postgres")
|
||||
monkeypatch.setenv("DB_HOST", "localhost")
|
||||
monkeypatch.setenv("DB_PORT", "5432")
|
||||
monkeypatch.setenv("DB_DATABASE", "dify")
|
||||
monkeypatch.setenv("HTTP_REQUEST_MAX_READ_TIMEOUT", "600")
|
||||
monkeypatch.setenv("HTTP_REQUEST_MAX_READ_TIMEOUT", "300") # Custom value for testing
|
||||
|
||||
# load dotenv file with pydantic-settings
|
||||
config = DifyConfig()
|
||||
|
|
@ -35,16 +35,36 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
|
|||
assert config.SENTRY_TRACES_SAMPLE_RATE == 1.0
|
||||
assert config.TEMPLATE_TRANSFORM_MAX_LENGTH == 400_000
|
||||
|
||||
# annotated field with default value
|
||||
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 600
|
||||
# annotated field with custom configured value
|
||||
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 300
|
||||
|
||||
# annotated field with configured value
|
||||
# annotated field with custom configured value
|
||||
assert config.HTTP_REQUEST_MAX_WRITE_TIMEOUT == 30
|
||||
|
||||
# values from pyproject.toml
|
||||
assert Version(config.project.version) >= Version("1.0.0")
|
||||
|
||||
|
||||
def test_http_timeout_defaults(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Test that HTTP timeout defaults are correctly set"""
|
||||
# clear system environment variables
|
||||
os.environ.clear()
|
||||
|
||||
# Set minimal required env vars
|
||||
monkeypatch.setenv("DB_USERNAME", "postgres")
|
||||
monkeypatch.setenv("DB_PASSWORD", "postgres")
|
||||
monkeypatch.setenv("DB_HOST", "localhost")
|
||||
monkeypatch.setenv("DB_PORT", "5432")
|
||||
monkeypatch.setenv("DB_DATABASE", "dify")
|
||||
|
||||
config = DifyConfig()
|
||||
|
||||
# Verify default timeout values
|
||||
assert config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT == 10
|
||||
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 600
|
||||
assert config.HTTP_REQUEST_MAX_WRITE_TIMEOUT == 600
|
||||
|
||||
|
||||
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
|
||||
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.
|
||||
def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
|
||||
|
|
@ -55,7 +75,6 @@ def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
|
|||
# Set environment variables using monkeypatch
|
||||
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
|
||||
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
|
||||
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30")
|
||||
monkeypatch.setenv("DB_USERNAME", "postgres")
|
||||
monkeypatch.setenv("DB_PASSWORD", "postgres")
|
||||
monkeypatch.setenv("DB_HOST", "localhost")
|
||||
|
|
@ -105,7 +124,6 @@ def test_inner_api_config_exist(monkeypatch: pytest.MonkeyPatch):
|
|||
# Set environment variables using monkeypatch
|
||||
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
|
||||
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
|
||||
monkeypatch.setenv("HTTP_REQUEST_MAX_WRITE_TIMEOUT", "30")
|
||||
monkeypatch.setenv("DB_USERNAME", "postgres")
|
||||
monkeypatch.setenv("DB_PASSWORD", "postgres")
|
||||
monkeypatch.setenv("DB_HOST", "localhost")
|
||||
|
|
|
|||
17
api/uv.lock
17
api/uv.lock
|
|
@ -1337,6 +1337,7 @@ dependencies = [
|
|||
{ name = "opentelemetry-instrumentation" },
|
||||
{ name = "opentelemetry-instrumentation-celery" },
|
||||
{ name = "opentelemetry-instrumentation-flask" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-instrumentation-redis" },
|
||||
{ name = "opentelemetry-instrumentation-requests" },
|
||||
{ name = "opentelemetry-instrumentation-sqlalchemy" },
|
||||
|
|
@ -1528,6 +1529,7 @@ requires-dist = [
|
|||
{ name = "opentelemetry-instrumentation", specifier = "==0.48b0" },
|
||||
{ name = "opentelemetry-instrumentation-celery", specifier = "==0.48b0" },
|
||||
{ name = "opentelemetry-instrumentation-flask", specifier = "==0.48b0" },
|
||||
{ name = "opentelemetry-instrumentation-httpx", specifier = "==0.48b0" },
|
||||
{ name = "opentelemetry-instrumentation-redis", specifier = "==0.48b0" },
|
||||
{ name = "opentelemetry-instrumentation-requests", specifier = "==0.48b0" },
|
||||
{ name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.48b0" },
|
||||
|
|
@ -3893,6 +3895,21 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/78/3d/fcde4f8f0bf9fa1ee73a12304fa538076fb83fe0a2ae966ab0f0b7da5109/opentelemetry_instrumentation_flask-0.48b0-py3-none-any.whl", hash = "sha256:26b045420b9d76e85493b1c23fcf27517972423480dc6cf78fd6924248ba5808", size = 14588, upload-time = "2024-08-28T21:26:58.504Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-httpx"
|
||||
version = "0.48b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-instrumentation" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "opentelemetry-util-http" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/d9/c65d818607c16d1b7ea8d2de6111c6cecadf8d2fd38c1885a72733a7c6d3/opentelemetry_instrumentation_httpx-0.48b0.tar.gz", hash = "sha256:ee977479e10398931921fb995ac27ccdeea2e14e392cb27ef012fc549089b60a", size = 16931, upload-time = "2024-08-28T21:28:03.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/fe/f2daa9d6d988c093b8c7b1d35df675761a8ece0b600b035dc04982746c9d/opentelemetry_instrumentation_httpx-0.48b0-py3-none-any.whl", hash = "sha256:d94f9d612c82d09fe22944d1904a30a464c19bea2ba76be656c99a28ad8be8e5", size = 13900, upload-time = "2024-08-28T21:27:01.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-redis"
|
||||
version = "0.48b0"
|
||||
|
|
|
|||
|
|
@ -930,6 +930,16 @@ WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100
|
|||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY=True
|
||||
|
||||
# HTTP request node timeout configuration
|
||||
# Maximum timeout values (in seconds) that users can set in HTTP request nodes
|
||||
# - Connect timeout: Time to wait for establishing connection (default: 10s)
|
||||
# - Read timeout: Time to wait for receiving response data (default: 600s, 10 minutes)
|
||||
# - Write timeout: Time to wait for sending request data (default: 600s, 10 minutes)
|
||||
HTTP_REQUEST_MAX_CONNECT_TIMEOUT=10
|
||||
HTTP_REQUEST_MAX_READ_TIMEOUT=600
|
||||
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
|
||||
|
||||
# Base64 encoded CA certificate data for custom certificate verification (PEM format, optional)
|
||||
# HTTP_REQUEST_NODE_SSL_CERT_DATA=LS0tLS1CRUdJTi...
|
||||
# Base64 encoded client certificate data for mutual TLS authentication (PEM format, optional)
|
||||
|
|
|
|||
|
|
@ -419,6 +419,9 @@ x-shared-env: &shared-api-worker-env
|
|||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576}
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True}
|
||||
WEBHOOK_REQUEST_BODY_MAX_SIZE: ${WEBHOOK_REQUEST_BODY_MAX_SIZE:-10485760}
|
||||
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: ${HTTP_REQUEST_MAX_CONNECT_TIMEOUT:-10}
|
||||
HTTP_REQUEST_MAX_READ_TIMEOUT: ${HTTP_REQUEST_MAX_READ_TIMEOUT:-600}
|
||||
HTTP_REQUEST_MAX_WRITE_TIMEOUT: ${HTTP_REQUEST_MAX_WRITE_TIMEOUT:-600}
|
||||
RESPECT_XFORWARD_HEADERS_ENABLED: ${RESPECT_XFORWARD_HEADERS_ENABLED:-false}
|
||||
SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128}
|
||||
SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ jest.mock('cmdk', () => ({
|
|||
Item: ({ children, onSelect, value, className }: any) => (
|
||||
<div
|
||||
className={className}
|
||||
onClick={() => onSelect && onSelect()}
|
||||
onClick={() => onSelect?.()}
|
||||
data-value={value}
|
||||
data-testid={`command-item-${value}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useCallback, useRef, useState } from 'react'
|
|||
|
||||
import type { PopupProps } from './config-popup'
|
||||
import ConfigPopup from './config-popup'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
|
|
@ -45,7 +46,7 @@ const ConfigBtn: FC<Props> = ({
|
|||
offset={12}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<div className="select-none">
|
||||
<div className={cn('select-none', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ const CSVUploader: FC<Props> = ({
|
|||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -37,7 +38,8 @@ const CSVUploader: FC<Props> = ({
|
|||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -348,7 +348,8 @@ const AppPublisher = ({
|
|||
<SuggestedAction
|
||||
className='flex-1'
|
||||
onClick={() => {
|
||||
publishedAt && handleOpenInExplore()
|
||||
if (publishedAt)
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
|
||||
icon={<RiPlanetLine className='h-4 w-4' />}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
|||
return
|
||||
}
|
||||
else {
|
||||
titleError && setTitleError(false)
|
||||
if (titleError)
|
||||
setTitleError(false)
|
||||
}
|
||||
|
||||
if (releaseNotes.length > RELEASE_NOTES_MAX_LENGTH) {
|
||||
|
|
@ -52,7 +53,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
|||
return
|
||||
}
|
||||
else {
|
||||
releaseNotesError && setReleaseNotesError(false)
|
||||
if (releaseNotesError)
|
||||
setReleaseNotesError(false)
|
||||
}
|
||||
|
||||
onPublish({ title, releaseNotes, id: versionInfo?.id })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import type { SVGProps } from 'react'
|
||||
|
||||
const CitationIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 6h10M7 12h6M7 18h10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 6c0-1.105.895-2 2-2h10c1.105 0 2 .895 2 2v12c0 1.105-.895 2-2 2H9l-4 3v-3H7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default CitationIcon
|
||||
|
|
@ -79,7 +79,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
try {
|
||||
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
|
||||
}
|
||||
catch (_e) {
|
||||
catch {
|
||||
return ''
|
||||
}
|
||||
}, [tempPayload.json_schema])
|
||||
|
|
@ -123,7 +123,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
}
|
||||
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
|
||||
}
|
||||
catch (_e) {
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}, [handlePayloadChange])
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ const Configuration: FC = () => {
|
|||
Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` })
|
||||
setCompletionParams(filtered)
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('common.error') })
|
||||
setCompletionParams({})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun}
|
||||
onClick={() => onSend && onSend()}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]">
|
||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
{t('appDebug.inputs.run')}
|
||||
|
|
@ -203,7 +203,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun}
|
||||
onClick={() => onSend && onSend()}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]">
|
||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
{t('appDebug.inputs.run')}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ const Uploader: FC<Props> = ({
|
|||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -47,7 +48,8 @@ const Uploader: FC<Props> = ({
|
|||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ const Chart: React.FC<IChartProps> = ({
|
|||
const { t } = useTranslation()
|
||||
const statistics = chartData.data
|
||||
const statisticsLen = statistics.length
|
||||
const extraDataForMarkLine = new Array(statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen).fill('1')
|
||||
const markLineLength = statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen
|
||||
const extraDataForMarkLine = Array.from({ length: markLineLength }, () => '1')
|
||||
extraDataForMarkLine.push('')
|
||||
extraDataForMarkLine.unshift('')
|
||||
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export default class AudioPlayer {
|
|||
}
|
||||
catch {
|
||||
this.isLoadData = false
|
||||
this.callback && this.callback('error')
|
||||
this.callback?.('error')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,15 +137,14 @@ export default class AudioPlayer {
|
|||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume().then((_) => {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
})
|
||||
}
|
||||
else if (this.audio.ended) {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
if (this.callback)
|
||||
this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
else {
|
||||
this.isLoadData = true
|
||||
|
|
@ -189,24 +188,24 @@ export default class AudioPlayer {
|
|||
if (this.audio.paused) {
|
||||
this.audioContext.resume().then((_) => {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
})
|
||||
}
|
||||
else if (this.audio.ended) {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
else if (this.audio.played) { /* empty */ }
|
||||
|
||||
else {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public pauseAudio() {
|
||||
this.callback && this.callback('paused')
|
||||
this.callback?.('paused')
|
||||
this.audio.pause()
|
||||
this.audioContext.suspend()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
const localState = localStorage.getItem('webappSidebarCollapse')
|
||||
return localState === 'collapsed'
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
// localStorage may be disabled in private browsing mode or by security settings
|
||||
// fallback to default value
|
||||
return false
|
||||
|
|
@ -142,7 +142,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
try {
|
||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
// localStorage may be disabled, continue without persisting state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,10 +101,14 @@ const Answer: FC<AnswerProps> = ({
|
|||
}, [])
|
||||
|
||||
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
|
||||
if (direction === 'prev')
|
||||
item.prevSibling && switchSibling?.(item.prevSibling)
|
||||
else
|
||||
item.nextSibling && switchSibling?.(item.nextSibling)
|
||||
if (direction === 'prev') {
|
||||
if (item.prevSibling)
|
||||
switchSibling?.(item.prevSibling)
|
||||
}
|
||||
else {
|
||||
if (item.nextSibling)
|
||||
switchSibling?.(item.nextSibling)
|
||||
}
|
||||
}, [switchSibling, item.prevSibling, item.nextSibling])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -73,10 +73,14 @@ const Question: FC<QuestionProps> = ({
|
|||
}, [content])
|
||||
|
||||
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
|
||||
if (direction === 'prev')
|
||||
item.prevSibling && switchSibling?.(item.prevSibling)
|
||||
else
|
||||
item.nextSibling && switchSibling?.(item.nextSibling)
|
||||
if (direction === 'prev') {
|
||||
if (item.prevSibling)
|
||||
switchSibling?.(item.prevSibling)
|
||||
}
|
||||
else {
|
||||
if (item.nextSibling)
|
||||
switchSibling?.(item.nextSibling)
|
||||
}
|
||||
}, [switchSibling, item.prevSibling, item.nextSibling])
|
||||
|
||||
const getContentWidth = () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import TimePicker from './index'
|
||||
import dayjs from '../utils/dayjs'
|
||||
import { isDayjsObject } from '../utils/dayjs'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
if (key === 'time.defaultPlaceholder') return 'Pick a time...'
|
||||
if (key === 'time.operation.now') return 'Now'
|
||||
if (key === 'time.operation.ok') return 'OK'
|
||||
if (key === 'common.operation.clear') return 'Clear'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: (e: React.MouseEvent) => void }) => (
|
||||
<div onClick={onClick}>{children}</div>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="timepicker-content">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('./options', () => () => <div data-testid="time-options" />)
|
||||
jest.mock('./header', () => () => <div data-testid="time-header" />)
|
||||
|
||||
describe('TimePicker', () => {
|
||||
const baseProps = {
|
||||
onChange: jest.fn(),
|
||||
onClear: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('renders formatted value for string input (Issue #26692 regression)', () => {
|
||||
render(
|
||||
<TimePicker
|
||||
{...baseProps}
|
||||
value="18:45"
|
||||
timezone="UTC"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByDisplayValue('06:45 PM')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('confirms cleared value when confirming without selection', () => {
|
||||
render(
|
||||
<TimePicker
|
||||
{...baseProps}
|
||||
value={dayjs('2024-01-01T03:30:00Z')}
|
||||
timezone="UTC"
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.click(input)
|
||||
|
||||
const clearButton = screen.getByRole('button', { name: /clear/i })
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'OK' })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
expect(baseProps.onChange).toHaveBeenCalledTimes(1)
|
||||
expect(baseProps.onChange).toHaveBeenCalledWith(undefined)
|
||||
expect(baseProps.onClear).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('selecting current time emits timezone-aware value', () => {
|
||||
const onChange = jest.fn()
|
||||
render(
|
||||
<TimePicker
|
||||
{...baseProps}
|
||||
onChange={onChange}
|
||||
timezone="America/New_York"
|
||||
/>,
|
||||
)
|
||||
|
||||
const nowButton = screen.getByRole('button', { name: 'Now' })
|
||||
fireEvent.click(nowButton)
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
const emitted = onChange.mock.calls[0][0]
|
||||
expect(isDayjsObject(emitted)).toBe(true)
|
||||
expect(emitted?.utcOffset()).toBe(dayjs().tz('America/New_York').utcOffset())
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { Period, TimePickerProps } from '../types'
|
||||
import dayjs, { cloneTime, getDateWithTimezone, getHourIn12Hour } from '../utils/dayjs'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { Period } from '../types'
|
||||
import type { TimePickerProps } from '../types'
|
||||
import dayjs, {
|
||||
getDateWithTimezone,
|
||||
getHourIn12Hour,
|
||||
isDayjsObject,
|
||||
toDayjs,
|
||||
} from '../utils/dayjs'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
|
|
@ -13,6 +20,11 @@ import { useTranslation } from 'react-i18next'
|
|||
import { RiCloseCircleFill, RiTimeLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const to24Hour = (hour12: string, period: Period) => {
|
||||
const normalized = Number.parseInt(hour12, 10) % 12
|
||||
return period === Period.PM ? normalized + 12 : normalized
|
||||
}
|
||||
|
||||
const TimePicker = ({
|
||||
value,
|
||||
timezone,
|
||||
|
|
@ -29,7 +41,11 @@ const TimePicker = ({
|
|||
const [isOpen, setIsOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isInitial = useRef(true)
|
||||
const [selectedTime, setSelectedTime] = useState(() => value ? getDateWithTimezone({ timezone, date: value }) : undefined)
|
||||
|
||||
// Initialize selectedTime
|
||||
const [selectedTime, setSelectedTime] = useState(() => {
|
||||
return toDayjs(value, { timezone })
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
|
@ -40,20 +56,47 @@ const TimePicker = ({
|
|||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// Track previous values to avoid unnecessary updates
|
||||
const prevValueRef = useRef(value)
|
||||
const prevTimezoneRef = useRef(timezone)
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitial.current) {
|
||||
isInitial.current = false
|
||||
// Save initial values on first render
|
||||
prevValueRef.current = value
|
||||
prevTimezoneRef.current = timezone
|
||||
return
|
||||
}
|
||||
if (value) {
|
||||
const newValue = getDateWithTimezone({ date: value, timezone })
|
||||
setSelectedTime(newValue)
|
||||
onChange(newValue)
|
||||
|
||||
// Only update when timezone changes but value doesn't
|
||||
const valueChanged = prevValueRef.current !== value
|
||||
const timezoneChanged = prevTimezoneRef.current !== timezone
|
||||
|
||||
// Update reference values
|
||||
prevValueRef.current = value
|
||||
prevTimezoneRef.current = timezone
|
||||
|
||||
// Skip if neither timezone changed nor value changed
|
||||
if (!timezoneChanged && !valueChanged) return
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
const dayjsValue = toDayjs(value, { timezone })
|
||||
if (!dayjsValue) return
|
||||
|
||||
setSelectedTime(dayjsValue)
|
||||
|
||||
if (timezoneChanged && !valueChanged)
|
||||
onChange(dayjsValue)
|
||||
return
|
||||
}
|
||||
else {
|
||||
setSelectedTime(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined)
|
||||
}
|
||||
}, [timezone])
|
||||
|
||||
setSelectedTime((prev) => {
|
||||
if (!isDayjsObject(prev))
|
||||
return undefined
|
||||
return timezone ? getDateWithTimezone({ date: prev, timezone }) : prev
|
||||
})
|
||||
}, [timezone, value, onChange])
|
||||
|
||||
const handleClickTrigger = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -62,8 +105,16 @@ const TimePicker = ({
|
|||
return
|
||||
}
|
||||
setIsOpen(true)
|
||||
if (value)
|
||||
setSelectedTime(value)
|
||||
|
||||
if (value) {
|
||||
const dayjsValue = toDayjs(value, { timezone })
|
||||
const needsUpdate = dayjsValue && (
|
||||
!selectedTime
|
||||
|| !isDayjsObject(selectedTime)
|
||||
|| !dayjsValue.isSame(selectedTime, 'minute')
|
||||
)
|
||||
if (needsUpdate) setSelectedTime(dayjsValue)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = (e: React.MouseEvent) => {
|
||||
|
|
@ -74,42 +125,68 @@ const TimePicker = ({
|
|||
}
|
||||
|
||||
const handleTimeSelect = (hour: string, minute: string, period: Period) => {
|
||||
const newTime = cloneTime(dayjs(), dayjs(`1/1/2000 ${hour}:${minute} ${period}`))
|
||||
const periodAdjustedHour = to24Hour(hour, period)
|
||||
const nextMinute = Number.parseInt(minute, 10)
|
||||
setSelectedTime((prev) => {
|
||||
return prev ? cloneTime(prev, newTime) : newTime
|
||||
const reference = isDayjsObject(prev)
|
||||
? prev
|
||||
: (timezone ? getDateWithTimezone({ timezone }) : dayjs()).startOf('minute')
|
||||
return reference
|
||||
.set('hour', periodAdjustedHour)
|
||||
.set('minute', nextMinute)
|
||||
.set('second', 0)
|
||||
.set('millisecond', 0)
|
||||
})
|
||||
}
|
||||
|
||||
const getSafeTimeObject = useCallback(() => {
|
||||
if (isDayjsObject(selectedTime))
|
||||
return selectedTime
|
||||
return (timezone ? getDateWithTimezone({ timezone }) : dayjs()).startOf('day')
|
||||
}, [selectedTime, timezone])
|
||||
|
||||
const handleSelectHour = useCallback((hour: string) => {
|
||||
const time = selectedTime || dayjs().startOf('day')
|
||||
const time = getSafeTimeObject()
|
||||
handleTimeSelect(hour, time.minute().toString().padStart(2, '0'), time.format('A') as Period)
|
||||
}, [selectedTime])
|
||||
}, [getSafeTimeObject])
|
||||
|
||||
const handleSelectMinute = useCallback((minute: string) => {
|
||||
const time = selectedTime || dayjs().startOf('day')
|
||||
const time = getSafeTimeObject()
|
||||
handleTimeSelect(getHourIn12Hour(time).toString().padStart(2, '0'), minute, time.format('A') as Period)
|
||||
}, [selectedTime])
|
||||
}, [getSafeTimeObject])
|
||||
|
||||
const handleSelectPeriod = useCallback((period: Period) => {
|
||||
const time = selectedTime || dayjs().startOf('day')
|
||||
const time = getSafeTimeObject()
|
||||
handleTimeSelect(getHourIn12Hour(time).toString().padStart(2, '0'), time.minute().toString().padStart(2, '0'), period)
|
||||
}, [selectedTime])
|
||||
}, [getSafeTimeObject])
|
||||
|
||||
const handleSelectCurrentTime = useCallback(() => {
|
||||
const newDate = getDateWithTimezone({ timezone })
|
||||
setSelectedTime(newDate)
|
||||
onChange(newDate)
|
||||
setIsOpen(false)
|
||||
}, [onChange, timezone])
|
||||
}, [timezone, onChange])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onChange(selectedTime)
|
||||
const valueToEmit = isDayjsObject(selectedTime) ? selectedTime : undefined
|
||||
onChange(valueToEmit)
|
||||
setIsOpen(false)
|
||||
}, [onChange, selectedTime])
|
||||
}, [selectedTime, onChange])
|
||||
|
||||
const timeFormat = 'hh:mm A'
|
||||
const displayValue = value?.format(timeFormat) || ''
|
||||
const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))
|
||||
|
||||
const formatTimeValue = useCallback((timeValue: string | Dayjs | undefined): string => {
|
||||
if (!timeValue) return ''
|
||||
|
||||
const dayjsValue = toDayjs(timeValue, { timezone })
|
||||
return dayjsValue?.format(timeFormat) || ''
|
||||
}, [timezone])
|
||||
|
||||
const displayValue = formatTimeValue(value)
|
||||
|
||||
const placeholderDate = isOpen && isDayjsObject(selectedTime)
|
||||
? selectedTime.format(timeFormat)
|
||||
: (placeholder || t('time.defaultPlaceholder'))
|
||||
|
||||
const inputElem = (
|
||||
<input
|
||||
|
|
@ -142,15 +219,13 @@ const TimePicker = ({
|
|||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (isOpen && selectedTime)) && !notClearable && 'group-hover:hidden',
|
||||
)} />
|
||||
{!notClearable && (
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden h-4 w-4 shrink-0 text-text-quaternary',
|
||||
(displayValue || (isOpen && selectedTime)) && 'hover:text-text-secondary group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
)}
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden h-4 w-4 shrink-0 text-text-quaternary',
|
||||
(displayValue || (isOpen && selectedTime)) && 'hover:text-text-secondary group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export type TriggerParams = {
|
|||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
export type TimePickerProps = {
|
||||
value: Dayjs | undefined
|
||||
value: Dayjs | string | undefined
|
||||
timezone?: string
|
||||
placeholder?: string
|
||||
onChange: (date: Dayjs | undefined) => void
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import dayjs from './dayjs'
|
||||
import {
|
||||
getDateWithTimezone,
|
||||
isDayjsObject,
|
||||
toDayjs,
|
||||
} from './dayjs'
|
||||
|
||||
describe('dayjs utilities', () => {
|
||||
const timezone = 'UTC'
|
||||
|
||||
test('toDayjs parses time-only strings with timezone support', () => {
|
||||
const result = toDayjs('18:45', { timezone })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('HH:mm')).toBe('18:45')
|
||||
expect(result?.utcOffset()).toBe(getDateWithTimezone({ timezone }).utcOffset())
|
||||
})
|
||||
|
||||
test('toDayjs parses 12-hour time strings', () => {
|
||||
const tz = 'America/New_York'
|
||||
const result = toDayjs('07:15 PM', { timezone: tz })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('HH:mm')).toBe('19:15')
|
||||
expect(result?.utcOffset()).toBe(getDateWithTimezone({ timezone: tz }).utcOffset())
|
||||
})
|
||||
|
||||
test('isDayjsObject detects dayjs instances', () => {
|
||||
const date = dayjs()
|
||||
expect(isDayjsObject(date)).toBe(true)
|
||||
expect(isDayjsObject(getDateWithTimezone({ timezone }))).toBe(true)
|
||||
expect(isDayjsObject('2024-01-01')).toBe(false)
|
||||
expect(isDayjsObject({})).toBe(false)
|
||||
})
|
||||
|
||||
test('toDayjs parses datetime strings in target timezone', () => {
|
||||
const value = '2024-05-01 12:00:00'
|
||||
const tz = 'America/New_York'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(12)
|
||||
expect(result?.format('YYYY-MM-DD HH:mm')).toBe('2024-05-01 12:00')
|
||||
})
|
||||
|
||||
test('toDayjs parses ISO datetime strings in target timezone', () => {
|
||||
const value = '2024-05-01T14:30:00'
|
||||
const tz = 'Europe/London'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(14)
|
||||
expect(result?.minute()).toBe(30)
|
||||
})
|
||||
|
||||
test('toDayjs handles dates without time component', () => {
|
||||
const value = '2024-05-01'
|
||||
const tz = 'America/Los_Angeles'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
expect(result?.hour()).toBe(0)
|
||||
expect(result?.minute()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -10,6 +10,25 @@ dayjs.extend(timezone)
|
|||
export default dayjs
|
||||
|
||||
const monthMaps: Record<string, Day[]> = {}
|
||||
const DEFAULT_OFFSET_STR = 'UTC+0'
|
||||
const TIME_ONLY_REGEX = /^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/
|
||||
const TIME_ONLY_12H_REGEX = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s?(AM|PM)$/i
|
||||
|
||||
const COMMON_PARSE_FORMATS = [
|
||||
'YYYY-MM-DD',
|
||||
'YYYY/MM/DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'MM-DD-YYYY',
|
||||
'MM/DD/YYYY',
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
||||
'YYYY-MM-DDTHH:mm:ssZ',
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
'YYYY-MM-DDTHH:mm',
|
||||
'YYYY-MM-DDTHH:mmZ',
|
||||
'YYYY-MM-DDTHH:mm:ss',
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS',
|
||||
]
|
||||
|
||||
export const cloneTime = (targetDate: Dayjs, sourceDate: Dayjs) => {
|
||||
return targetDate.clone()
|
||||
|
|
@ -76,21 +95,116 @@ export const getHourIn12Hour = (date: Dayjs) => {
|
|||
return hour === 0 ? 12 : hour >= 12 ? hour - 12 : hour
|
||||
}
|
||||
|
||||
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
|
||||
return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone)
|
||||
export const getDateWithTimezone = ({ date, timezone }: { date?: Dayjs, timezone?: string }) => {
|
||||
if (!timezone)
|
||||
return (date ?? dayjs()).clone()
|
||||
return date ? dayjs.tz(date, timezone) : dayjs().tz(timezone)
|
||||
}
|
||||
|
||||
// Asia/Shanghai -> UTC+8
|
||||
const DEFAULT_OFFSET_STR = 'UTC+0'
|
||||
export const convertTimezoneToOffsetStr = (timezone?: string) => {
|
||||
if (!timezone)
|
||||
return DEFAULT_OFFSET_STR
|
||||
const tzItem = tz.find(item => item.value === timezone)
|
||||
if(!tzItem)
|
||||
if (!tzItem)
|
||||
return DEFAULT_OFFSET_STR
|
||||
return `UTC${tzItem.name.charAt(0)}${tzItem.name.charAt(2)}`
|
||||
}
|
||||
|
||||
export const isDayjsObject = (value: unknown): value is Dayjs => dayjs.isDayjs(value)
|
||||
|
||||
export type ToDayjsOptions = {
|
||||
timezone?: string
|
||||
format?: string
|
||||
formats?: string[]
|
||||
}
|
||||
|
||||
const warnParseFailure = (value: string) => {
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
console.warn('[TimePicker] Failed to parse time value', value)
|
||||
}
|
||||
|
||||
const normalizeMillisecond = (value: string | undefined) => {
|
||||
if (!value) return 0
|
||||
if (value.length === 3) return Number(value)
|
||||
if (value.length > 3) return Number(value.slice(0, 3))
|
||||
return Number(value.padEnd(3, '0'))
|
||||
}
|
||||
|
||||
const applyTimezone = (date: Dayjs, timezone?: string) => {
|
||||
return timezone ? getDateWithTimezone({ date, timezone }) : date
|
||||
}
|
||||
|
||||
export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptions = {}): Dayjs | undefined => {
|
||||
if (!value)
|
||||
return undefined
|
||||
|
||||
const { timezone: tzName, format, formats } = options
|
||||
|
||||
if (isDayjsObject(value))
|
||||
return applyTimezone(value, tzName)
|
||||
|
||||
if (typeof value !== 'string')
|
||||
return undefined
|
||||
|
||||
const trimmed = value.trim()
|
||||
|
||||
if (format) {
|
||||
const parsedWithFormat = tzName
|
||||
? dayjs.tz(trimmed, format, tzName, true)
|
||||
: dayjs(trimmed, format, true)
|
||||
if (parsedWithFormat.isValid())
|
||||
return parsedWithFormat
|
||||
}
|
||||
|
||||
const timeMatch = TIME_ONLY_REGEX.exec(trimmed)
|
||||
if (timeMatch) {
|
||||
const base = applyTimezone(dayjs(), tzName).startOf('day')
|
||||
const rawHour = Number(timeMatch[1])
|
||||
const minute = Number(timeMatch[2])
|
||||
const second = timeMatch[3] ? Number(timeMatch[3]) : 0
|
||||
const millisecond = normalizeMillisecond(timeMatch[4])
|
||||
|
||||
return base
|
||||
.set('hour', rawHour)
|
||||
.set('minute', minute)
|
||||
.set('second', second)
|
||||
.set('millisecond', millisecond)
|
||||
}
|
||||
|
||||
const timeMatch12h = TIME_ONLY_12H_REGEX.exec(trimmed)
|
||||
if (timeMatch12h) {
|
||||
const base = applyTimezone(dayjs(), tzName).startOf('day')
|
||||
let hour = Number(timeMatch12h[1]) % 12
|
||||
const isPM = timeMatch12h[4]?.toUpperCase() === 'PM'
|
||||
if (isPM)
|
||||
hour += 12
|
||||
const minute = Number(timeMatch12h[2])
|
||||
const second = timeMatch12h[3] ? Number(timeMatch12h[3]) : 0
|
||||
|
||||
return base
|
||||
.set('hour', hour)
|
||||
.set('minute', minute)
|
||||
.set('second', second)
|
||||
.set('millisecond', 0)
|
||||
}
|
||||
|
||||
const candidateFormats = formats ?? COMMON_PARSE_FORMATS
|
||||
for (const fmt of candidateFormats) {
|
||||
const parsed = tzName
|
||||
? dayjs.tz(trimmed, fmt, tzName, true)
|
||||
: dayjs(trimmed, fmt, true)
|
||||
if (parsed.isValid())
|
||||
return parsed
|
||||
}
|
||||
|
||||
const fallbackParsed = tzName ? dayjs.tz(trimmed, tzName) : dayjs(trimmed)
|
||||
if (fallbackParsed.isValid())
|
||||
return fallbackParsed
|
||||
|
||||
warnParseFailure(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Parse date with multiple format support
|
||||
export const parseDateWithFormat = (dateString: string, format?: string): Dayjs | null => {
|
||||
if (!dateString) return null
|
||||
|
|
@ -103,15 +217,7 @@ export const parseDateWithFormat = (dateString: string, format?: string): Dayjs
|
|||
|
||||
// Try common date formats
|
||||
const formats = [
|
||||
'YYYY-MM-DD', // Standard format
|
||||
'YYYY/MM/DD', // Slash format
|
||||
'DD-MM-YYYY', // European format
|
||||
'DD/MM/YYYY', // European slash format
|
||||
'MM-DD-YYYY', // US format
|
||||
'MM/DD/YYYY', // US slash format
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ', // ISO format
|
||||
'YYYY-MM-DDTHH:mm:ssZ', // ISO format (no milliseconds)
|
||||
'YYYY-MM-DD HH:mm:ss', // Standard datetime format
|
||||
...COMMON_PARSE_FORMATS,
|
||||
]
|
||||
|
||||
for (const fmt of formats) {
|
||||
|
|
@ -124,7 +230,7 @@ export const parseDateWithFormat = (dateString: string, format?: string): Dayjs
|
|||
}
|
||||
|
||||
// Format date output with localization support
|
||||
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, locale: string = 'en-US'): string => {
|
||||
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, _locale: string = 'en-US'): string => {
|
||||
if (!date || !date.isValid()) return ''
|
||||
|
||||
if (includeTime) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ export default function Drawer({
|
|||
<Dialog
|
||||
unmount={unmount}
|
||||
open={isOpen}
|
||||
onClose={() => !clickOutsideNotOpen && onClose()}
|
||||
onClose={() => {
|
||||
if (!clickOutsideNotOpen)
|
||||
onClose()
|
||||
}}
|
||||
className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)}
|
||||
>
|
||||
<div className={cn('flex h-screen w-screen justify-end', positionCenter && '!justify-center')}>
|
||||
|
|
@ -55,7 +58,8 @@ export default function Drawer({
|
|||
<DialogBackdrop
|
||||
className={cn('fixed inset-0 z-[40]', mask && 'bg-black/30', dialogBackdropClassName)}
|
||||
onClick={() => {
|
||||
!clickOutsideNotOpen && onClose()
|
||||
if (!clickOutsideNotOpen)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
<div className={cn('relative z-[50] flex w-full max-w-sm flex-col justify-between overflow-hidden bg-components-panel-bg p-6 text-left align-middle shadow-xl', panelClassName)}>
|
||||
|
|
@ -80,11 +84,11 @@ export default function Drawer({
|
|||
<Button
|
||||
className='mr-2'
|
||||
onClick={() => {
|
||||
onCancel && onCancel()
|
||||
onCancel?.()
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onOk && onOk()
|
||||
onOk?.()
|
||||
}}>{t('common.operation.save')}</Button>
|
||||
</div>)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
|
|||
<Divider className='mb-0 mt-3' />
|
||||
<div className='flex w-full items-center justify-center gap-2 p-3'>
|
||||
<Button className='w-full' onClick={() => {
|
||||
onClose && onClose()
|
||||
onClose?.()
|
||||
}}>
|
||||
{t('app.iconPicker.cancel')}
|
||||
</Button>
|
||||
|
|
@ -54,7 +54,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
|
|||
variant="primary"
|
||||
className='w-full'
|
||||
onClick={() => {
|
||||
onSelect && onSelect(selectedEmoji, selectedBackground!)
|
||||
onSelect?.(selectedEmoji, selectedBackground!)
|
||||
}}>
|
||||
{t('app.iconPicker.ok')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ const SelectField = ({
|
|||
<PureSelect
|
||||
value={field.state.value}
|
||||
options={options}
|
||||
onChange={value => field.handleChange(value)}
|
||||
onChange={(value) => {
|
||||
field.handleChange(value)
|
||||
onChange?.(value)
|
||||
}}
|
||||
{...selectProps}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const ImageList: FC<ImageListProps> = ({
|
|||
{item.progress === -1 && (
|
||||
<RefreshCcw01
|
||||
className="h-5 w-5 text-white"
|
||||
onClick={() => onReUpload && onReUpload(item._id)}
|
||||
onClick={() => onReUpload?.(item._id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -122,7 +122,7 @@ const ImageList: FC<ImageListProps> = ({
|
|||
'rounded-2xl shadow-lg hover:bg-state-base-hover',
|
||||
item.progress === -1 ? 'flex' : 'hidden group-hover:flex',
|
||||
)}
|
||||
onClick={() => onRemove && onRemove(item._id)}
|
||||
onClick={() => onRemove?.(item._id)}
|
||||
>
|
||||
<RiCloseLine className="h-3 w-3 text-text-tertiary" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const isBase64 = (str: string): boolean => {
|
|||
try {
|
||||
return btoa(atob(str)) === str
|
||||
}
|
||||
catch (err) {
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||
|
||||
// Store event handlers in useMemo to avoid recreating them
|
||||
const echartsEvents = useMemo(() => ({
|
||||
finished: (params: EChartsEventParams) => {
|
||||
finished: (_params: EChartsEventParams) => {
|
||||
// Limit finished event frequency to avoid infinite loops
|
||||
finishedEventCountRef.current++
|
||||
if (finishedEventCountRef.current > 3) {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export function svgToBase64(svgGraph: string): Promise<string> {
|
|||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
catch {
|
||||
return Promise.resolve('')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ const usePagination = ({
|
|||
edgePageCount,
|
||||
middlePagesSiblingCount,
|
||||
}: IPaginationProps): IUsePagination => {
|
||||
const pages = new Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1)
|
||||
const pages = React.useMemo(() => Array.from({ length: totalPages }, (_, i) => i + 1), [totalPages])
|
||||
|
||||
const hasPreviousPage = currentPage > 1
|
||||
const hasNextPage = currentPage < totalPages
|
||||
|
|
|
|||
|
|
@ -37,13 +37,16 @@ export default function CustomPopover({
|
|||
const timeOutRef = useRef<number | null>(null)
|
||||
|
||||
const onMouseEnter = (isOpen: boolean) => {
|
||||
timeOutRef.current && window.clearTimeout(timeOutRef.current)
|
||||
!isOpen && buttonRef.current?.click()
|
||||
if (timeOutRef.current != null)
|
||||
window.clearTimeout(timeOutRef.current)
|
||||
if (!isOpen)
|
||||
buttonRef.current?.click()
|
||||
}
|
||||
|
||||
const onMouseLeave = (isOpen: boolean) => {
|
||||
timeOutRef.current = window.setTimeout(() => {
|
||||
isOpen && buttonRef.current?.click()
|
||||
if (isOpen)
|
||||
buttonRef.current?.click()
|
||||
}, timeoutDuration)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function LocaleSigninSelect({
|
|||
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)
|
||||
onChange?.(item.value)
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function Select({
|
|||
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)
|
||||
onChange?.(item.value)
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
|
|
|
|||
|
|
@ -97,10 +97,13 @@ const Panel = (props: PanelProps) => {
|
|||
const removeTagIDs = value.filter(v => !selectedTagIDs.includes(v))
|
||||
const selectedTags = tagList.filter(tag => selectedTagIDs.includes(tag.id))
|
||||
onCacheUpdate(selectedTags)
|
||||
Promise.all([
|
||||
...(addTagIDs.length ? [bind(addTagIDs)] : []),
|
||||
...[removeTagIDs.length ? removeTagIDs.map(tagID => unbind(tagID)) : []],
|
||||
]).finally(() => {
|
||||
const operations: Promise<unknown>[] = []
|
||||
if (addTagIDs.length)
|
||||
operations.push(bind(addTagIDs))
|
||||
if (removeTagIDs.length)
|
||||
operations.push(...removeTagIDs.map(tagID => unbind(tagID)))
|
||||
|
||||
Promise.all(operations).finally(() => {
|
||||
if (onChange)
|
||||
onChange()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ const VoiceInput = ({
|
|||
setStartRecord(false)
|
||||
setStartConvert(true)
|
||||
recorder.current.stop()
|
||||
drawRecordId.current && cancelAnimationFrame(drawRecordId.current)
|
||||
if (drawRecordId.current)
|
||||
cancelAnimationFrame(drawRecordId.current)
|
||||
drawRecordId.current = null
|
||||
const canvas = canvasRef.current!
|
||||
const ctx = ctxRef.current!
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ const Uploader: FC<Props> = ({
|
|||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -43,7 +44,8 @@ const Uploader: FC<Props> = ({
|
|||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -185,7 +185,8 @@ const FileUploader = ({
|
|||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -194,7 +195,8 @@ const FileUploader = ({
|
|||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
type FileWithPath = {
|
||||
relativePath?: string
|
||||
|
|
|
|||
|
|
@ -568,9 +568,9 @@ const StepTwo = ({
|
|||
params,
|
||||
{
|
||||
onSuccess(data) {
|
||||
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
|
||||
updateResultCache && updateResultCache(data)
|
||||
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
|
||||
updateIndexingTypeCache?.(indexType as string)
|
||||
updateResultCache?.(data)
|
||||
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -578,17 +578,18 @@ const StepTwo = ({
|
|||
else {
|
||||
await createDocumentMutation.mutateAsync(params, {
|
||||
onSuccess(data) {
|
||||
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
|
||||
updateResultCache && updateResultCache(data)
|
||||
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
|
||||
updateIndexingTypeCache?.(indexType as string)
|
||||
updateResultCache?.(data)
|
||||
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
|
||||
},
|
||||
})
|
||||
}
|
||||
if (mutateDatasetRes)
|
||||
mutateDatasetRes()
|
||||
invalidDatasetList()
|
||||
onStepChange && onStepChange(+1)
|
||||
isSetting && onSave && onSave()
|
||||
onStepChange?.(+1)
|
||||
if (isSetting)
|
||||
onSave?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -1026,7 +1027,7 @@ const StepTwo = ({
|
|||
{!isSetting
|
||||
? (
|
||||
<div className='mt-8 flex items-center py-2'>
|
||||
<Button onClick={() => onStepChange && onStepChange(-1)}>
|
||||
<Button onClick={() => onStepChange?.(-1)}>
|
||||
<RiArrowLeftLine className='mr-1 h-4 w-4' />
|
||||
{t('datasetCreation.stepTwo.previousStep')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -200,7 +200,8 @@ const LocalFile = ({
|
|||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -209,7 +210,8 @@ const LocalFile = ({
|
|||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
const handleDrop = useCallback((e: DragEvent) => {
|
||||
|
|
|
|||
|
|
@ -45,10 +45,13 @@ const CrawledResult = ({
|
|||
|
||||
const handleItemCheckChange = useCallback((item: CrawlResultItem) => {
|
||||
return (checked: boolean) => {
|
||||
if (checked)
|
||||
isMultipleChoice ? onSelectedChange([...checkedList, item]) : onSelectedChange([item])
|
||||
else
|
||||
onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url))
|
||||
if (checked) {
|
||||
if (isMultipleChoice)
|
||||
onSelectedChange([...checkedList, item])
|
||||
else
|
||||
onSelectedChange([item])
|
||||
}
|
||||
else { onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url)) }
|
||||
}
|
||||
}, [checkedList, onSelectedChange, isMultipleChoice])
|
||||
|
||||
|
|
|
|||
|
|
@ -326,7 +326,10 @@ const CreateFormPipeline = () => {
|
|||
}, [])
|
||||
|
||||
const handleSubmit = useCallback((data: Record<string, any>) => {
|
||||
isPreview.current ? handlePreviewChunks(data) : handleProcess(data)
|
||||
if (isPreview.current)
|
||||
handlePreviewChunks(data)
|
||||
else
|
||||
handleProcess(data)
|
||||
}, [handlePreviewChunks, handleProcess])
|
||||
|
||||
const handlePreviewFileChange = useCallback((file: DocumentItem) => {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ const CSVUploader: FC<Props> = ({
|
|||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
@ -108,7 +109,8 @@ const CSVUploader: FC<Props> = ({
|
|||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -284,7 +284,8 @@ const Completed: FC<ICompletedProps> = ({
|
|||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
resetList()
|
||||
!segId && setSelectedSegmentIds([])
|
||||
if (!segId)
|
||||
setSelectedSegmentIds([])
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
|
|
@ -438,7 +439,8 @@ const Completed: FC<ICompletedProps> = ({
|
|||
}
|
||||
else {
|
||||
resetList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
if (currentPage !== totalPages)
|
||||
setCurrentPage(totalPages)
|
||||
}
|
||||
}, [segmentListData, limit, currentPage, resetList])
|
||||
|
||||
|
|
@ -491,7 +493,8 @@ const Completed: FC<ICompletedProps> = ({
|
|||
}
|
||||
else {
|
||||
resetChildList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
if (currentPage !== totalPages)
|
||||
setCurrentPage(totalPages)
|
||||
}
|
||||
}, [childChunkListData, limit, currentPage, resetChildList])
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
|
|||
? displayedValue
|
||||
: inputType === 'select'
|
||||
? <SimpleSelect
|
||||
onSelect={({ value }) => onUpdate && onUpdate(value as string)}
|
||||
onSelect={({ value }) => onUpdate?.(value as string)}
|
||||
items={selectOptions}
|
||||
defaultValue={value}
|
||||
className={s.select}
|
||||
|
|
@ -75,7 +75,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
|
|||
/>
|
||||
: inputType === 'textarea'
|
||||
? <AutoHeightTextarea
|
||||
onChange={e => onUpdate && onUpdate(e.target.value)}
|
||||
onChange={e => onUpdate?.(e.target.value)}
|
||||
value={value}
|
||||
className={s.textArea}
|
||||
placeholder={`${t('datasetDocuments.metadata.placeholder.add')}${label}`}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,10 @@ const PipelineSettings = ({
|
|||
}, [])
|
||||
|
||||
const handleSubmit = useCallback((data: Record<string, any>) => {
|
||||
isPreview.current ? handlePreviewChunks(data) : handleProcess(data)
|
||||
if (isPreview.current)
|
||||
handlePreviewChunks(data)
|
||||
else
|
||||
handleProcess(data)
|
||||
}, [handlePreviewChunks, handleProcess])
|
||||
|
||||
if (isFetchingLastRunData) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ const TextAreaWithButton = ({
|
|||
onUpdateList?.()
|
||||
}
|
||||
setLoading(false)
|
||||
_onSubmit && _onSubmit()
|
||||
if (_onSubmit)
|
||||
_onSubmit()
|
||||
}
|
||||
|
||||
const externalRetrievalTestingOnSubmit = async () => {
|
||||
|
|
|
|||
|
|
@ -157,12 +157,12 @@ const DatasetCard = ({
|
|||
data-disable-nprogress={true}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
isExternalProvider
|
||||
? push(`/datasets/${dataset.id}/hitTesting`)
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
: isPipelineUnpublished
|
||||
? push(`/datasets/${dataset.id}/pipeline`)
|
||||
: push(`/datasets/${dataset.id}/documents`)
|
||||
if (isExternalProvider)
|
||||
push(`/datasets/${dataset.id}/hitTesting`)
|
||||
else if (isPipelineUnpublished)
|
||||
push(`/datasets/${dataset.id}/pipeline`)
|
||||
else
|
||||
push(`/datasets/${dataset.id}/documents`)
|
||||
}}
|
||||
>
|
||||
{!dataset.embedding_available && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
const DatasetsLoading = () => null
|
||||
|
||||
export default DatasetsLoading
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
const DatasetPreview = () => null
|
||||
|
||||
export default DatasetPreview
|
||||
|
|
@ -39,7 +39,7 @@ const Collapse = ({
|
|||
<div className='mx-1 mb-1 rounded-lg border-t border-divider-subtle bg-components-panel-on-panel-item-bg py-1'>
|
||||
{
|
||||
items.map(item => (
|
||||
<div key={item.key} onClick={() => onSelect && onSelect(item)}>
|
||||
<div key={item.key} onClick={() => onSelect?.(item)}>
|
||||
{renderItem(item)}
|
||||
</div>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ function Form<
|
|||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<span className={cn(fieldLabelClassName, 'system-sm-regular flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
|
||||
<span className={cn(fieldLabelClassName, 'system-sm-semibold flex items-center py-2 text-text-secondary')}>{label[language] || label.en_US}</span>
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const ModelLoadBalancingConfigs = ({
|
|||
provider,
|
||||
model,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
currentCustomConfigurationModelFixedFields: _currentCustomConfigurationModelFixedFields,
|
||||
withSwitch = false,
|
||||
className,
|
||||
modelCredential,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const AppPicker: FC<Props> = ({
|
||||
scope,
|
||||
scope: _scope,
|
||||
disabled,
|
||||
trigger,
|
||||
placement = 'right-start',
|
||||
|
|
@ -90,7 +90,7 @@ const AppPicker: FC<Props> = ({
|
|||
}
|
||||
|
||||
// Set up MutationObserver to watch DOM changes
|
||||
mutationObserver = new MutationObserver((mutations) => {
|
||||
mutationObserver = new MutationObserver((_mutations) => {
|
||||
if (observerTarget.current) {
|
||||
setupIntersectionObserver()
|
||||
mutationObserver?.disconnect()
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
|||
})
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('common.error') })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const useFieldList = ({
|
|||
|
||||
const handleListSortChange = useCallback((list: SortableItem[]) => {
|
||||
const newInputFields = list.map((item) => {
|
||||
const { id, chosen, selected, ...filed } = item
|
||||
const { id: _id, chosen: _chosen, selected: _selected, ...filed } = item
|
||||
return filed
|
||||
})
|
||||
handleInputFieldsChange(newInputFields)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ const Header = () => {
|
|||
isPreparingDataSource,
|
||||
setIsPreparingDataSource,
|
||||
} = workflowStore.getState()
|
||||
isPreparingDataSource && setIsPreparingDataSource?.(false)
|
||||
if (isPreparingDataSource)
|
||||
setIsPreparingDataSource?.(false)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
}, [workflowStore])
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export const useNodesSyncDraft = () => {
|
|||
const res = await syncWorkflowDraft(postParams)
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
callback?.onSuccess && callback.onSuccess()
|
||||
callback?.onSuccess?.()
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
|
|
@ -113,10 +113,10 @@ export const useNodesSyncDraft = () => {
|
|||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError && callback.onError()
|
||||
callback?.onError?.()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled && callback.onSettled()
|
||||
callback?.onSettled?.()
|
||||
}
|
||||
}
|
||||
}, [getPostParams, getNodesReadOnly, workflowStore, handleRefreshWorkflowDraft])
|
||||
|
|
|
|||
|
|
@ -363,7 +363,8 @@ const TextGeneration: FC<IMainProps> = ({
|
|||
(async () => {
|
||||
if (!appData || !appParams)
|
||||
return
|
||||
!isWorkflow && fetchSavedMessage()
|
||||
if (!isWorkflow)
|
||||
fetchSavedMessage()
|
||||
const { app_id: appId, site: siteInfo, custom_config } = appData
|
||||
setAppId(appId)
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
|
|
|
|||
|
|
@ -78,15 +78,15 @@ const Result: FC<IResultProps> = ({
|
|||
setRespondingFalse()
|
||||
}, [controlStopResponding])
|
||||
|
||||
const [completionRes, doSetCompletionRes] = useState<any>('')
|
||||
const completionResRef = useRef<any>()
|
||||
const setCompletionRes = (res: any) => {
|
||||
const [completionRes, doSetCompletionRes] = useState<string>('')
|
||||
const completionResRef = useRef<string>('')
|
||||
const setCompletionRes = (res: string) => {
|
||||
completionResRef.current = res
|
||||
doSetCompletionRes(res)
|
||||
}
|
||||
const getCompletionRes = () => completionResRef.current
|
||||
const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>()
|
||||
const workflowProcessDataRef = useRef<WorkflowProcess>()
|
||||
const workflowProcessDataRef = useRef<WorkflowProcess | undefined>(undefined)
|
||||
const setWorkflowProcessData = (data: WorkflowProcess) => {
|
||||
workflowProcessDataRef.current = data
|
||||
doSetWorkflowProcessData(data)
|
||||
|
|
|
|||
|
|
@ -62,8 +62,10 @@ const SwrInitializer = ({
|
|||
return
|
||||
}
|
||||
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
|
||||
consoleToken && localStorage.setItem('console_token', consoleToken)
|
||||
refreshToken && localStorage.setItem('refresh_token', refreshToken)
|
||||
if (consoleToken)
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
if (refreshToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
if (redirectUrl)
|
||||
location.replace(redirectUrl)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
|
|||
return {
|
||||
...parameter,
|
||||
variable: parameter.name,
|
||||
type: toType(parameter.type),
|
||||
label: parameter.label,
|
||||
tooltip: parameter.help,
|
||||
show_on: [],
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export const useNodesSyncDraft = () => {
|
|||
const res = await syncWorkflowDraft(postParams)
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
callback?.onSuccess && callback.onSuccess()
|
||||
callback?.onSuccess?.()
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
|
|
@ -131,10 +131,10 @@ export const useNodesSyncDraft = () => {
|
|||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError && callback.onError()
|
||||
callback?.onError?.()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled && callback.onSettled()
|
||||
callback?.onSettled?.()
|
||||
}
|
||||
}
|
||||
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft])
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const Tool: FC<Props> = ({
|
|||
if (isHovering && !isAllSelected) {
|
||||
return (
|
||||
<span className='system-xs-regular text-components-button-secondary-accent-text'
|
||||
onClick={(e) => {
|
||||
onClick={() => {
|
||||
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
|
||||
const params: Record<string, string> = {}
|
||||
if (tool.parameters) {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ export const useShortcuts = (): void => {
|
|||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryBack()
|
||||
if (workflowHistoryShortcutsEnabled)
|
||||
handleHistoryBack()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
|
|
@ -117,7 +118,8 @@ export const useShortcuts = (): void => {
|
|||
(e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryForward()
|
||||
if (workflowHistoryShortcutsEnabled)
|
||||
handleHistoryForward()
|
||||
}
|
||||
},
|
||||
{ exactMatch: true, useCapture: true },
|
||||
|
|
|
|||
|
|
@ -41,16 +41,16 @@ export const useWorkflowHistory = () => {
|
|||
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [undoCallbacks, setUndoCallbacks] = useState<any[]>([])
|
||||
const [redoCallbacks, setRedoCallbacks] = useState<any[]>([])
|
||||
const [undoCallbacks, setUndoCallbacks] = useState<(() => void)[]>([])
|
||||
const [redoCallbacks, setRedoCallbacks] = useState<(() => void)[]>([])
|
||||
|
||||
const onUndo = useCallback((callback: unknown) => {
|
||||
setUndoCallbacks((prev: any) => [...prev, callback])
|
||||
const onUndo = useCallback((callback: () => void) => {
|
||||
setUndoCallbacks(prev => [...prev, callback])
|
||||
return () => setUndoCallbacks(prev => prev.filter(cb => cb !== callback))
|
||||
}, [])
|
||||
|
||||
const onRedo = useCallback((callback: unknown) => {
|
||||
setRedoCallbacks((prev: any) => [...prev, callback])
|
||||
const onRedo = useCallback((callback: () => void) => {
|
||||
setRedoCallbacks(prev => [...prev, callback])
|
||||
return () => setRedoCallbacks(prev => prev.filter(cb => cb !== callback))
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ export const useWorkflow = () => {
|
|||
return startNodes
|
||||
}, [nodesMap, getRootNodesById])
|
||||
|
||||
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
||||
const isValidConnection = useCallback(({ source, sourceHandle: _sourceHandle, target }: Connection) => {
|
||||
const {
|
||||
edges,
|
||||
getNodes,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export {}
|
||||
|
|
@ -129,7 +129,7 @@ const VarReferencePicker: FC<Props> = ({
|
|||
|
||||
const reactflow = useReactFlow()
|
||||
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
const startNode = availableNodes.find((node: Node) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
|
|
@ -409,7 +409,10 @@ const VarReferencePicker: FC<Props> = ({
|
|||
<WrapElem onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
if (!isConstant)
|
||||
setOpen(!open)
|
||||
else
|
||||
setControlFocus(Date.now())
|
||||
}} className='group/picker-trigger-wrap relative !flex'>
|
||||
<>
|
||||
{isAddBtnTrigger
|
||||
|
|
@ -459,7 +462,10 @@ const VarReferencePicker: FC<Props> = ({
|
|||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
if (!isConstant)
|
||||
setOpen(!open)
|
||||
else
|
||||
setControlFocus(Date.now())
|
||||
}}
|
||||
className='h-full grow'
|
||||
>
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ const Item: FC<ItemProps> = ({
|
|||
const isHovering = isItemHovering || isChildrenHovering
|
||||
const open = (isObj || isStructureOutput) && isHovering
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isHovering)
|
||||
onHovering?.(isHovering)
|
||||
}, [isHovering])
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ type Props = {
|
|||
} & Partial<ResultPanelProps>
|
||||
|
||||
const LastRun: FC<Props> = ({
|
||||
appId,
|
||||
appId: _appId,
|
||||
nodeId,
|
||||
canSingleRun,
|
||||
isRunAfterSingleRun,
|
||||
updateNodeRunningStatus,
|
||||
nodeInfo,
|
||||
nodeInfo: _nodeInfo,
|
||||
runningStatus: oneStepRunRunningStatus,
|
||||
onSingleRunClicked,
|
||||
singleRunResult,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'
|
|||
import type { Timeout as TimeoutPayloadType } from '../../types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
|
|
@ -61,6 +63,11 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
|
|||
const { t } = useTranslation()
|
||||
const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {}
|
||||
|
||||
// Get default config from store for max timeout values
|
||||
const nodesDefaultConfigs = useStore(s => s.nodesDefaultConfigs)
|
||||
const defaultConfig = nodesDefaultConfigs?.[BlockEnum.HttpRequest]
|
||||
const defaultTimeout = defaultConfig?.timeout || {}
|
||||
|
||||
return (
|
||||
<FieldCollapse title={t(`${i18nPrefix}.timeout.title`)}>
|
||||
<div className='mt-2 space-y-1'>
|
||||
|
|
@ -73,7 +80,7 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
|
|||
value={connect}
|
||||
onChange={v => onChange?.({ ...payload, connect: v })}
|
||||
min={1}
|
||||
max={max_connect_timeout || 300}
|
||||
max={max_connect_timeout || defaultTimeout.max_connect_timeout || 10}
|
||||
/>
|
||||
<InputField
|
||||
title={t('workflow.nodes.http.timeout.readLabel')!}
|
||||
|
|
@ -83,7 +90,7 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
|
|||
value={read}
|
||||
onChange={v => onChange?.({ ...payload, read: v })}
|
||||
min={1}
|
||||
max={max_read_timeout || 600}
|
||||
max={max_read_timeout || defaultTimeout.max_read_timeout || 600}
|
||||
/>
|
||||
<InputField
|
||||
title={t('workflow.nodes.http.timeout.writeLabel')!}
|
||||
|
|
@ -93,7 +100,7 @@ const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
|
|||
value={write}
|
||||
onChange={v => onChange?.({ ...payload, write: v })}
|
||||
min={1}
|
||||
max={max_write_timeout || 600}
|
||||
max={max_write_timeout || defaultTimeout.max_write_timeout || 600}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ const OptionCard = memo(({
|
|||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
!readonly && enableSelect && id && onClick?.(id)
|
||||
if (!readonly && enableSelect && id)
|
||||
onClick?.(id)
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||
setJson(JSON.stringify(schema, null, 2))
|
||||
}, [currentTab])
|
||||
|
||||
const handleSubmit = useCallback((schema: any) => {
|
||||
const handleSubmit = useCallback((schema: Record<string, unknown>) => {
|
||||
const jsonSchema = jsonToSchema(schema) as SchemaRoot
|
||||
if (currentTab === SchemaView.VisualEditor)
|
||||
setJsonSchema(jsonSchema)
|
||||
|
|
@ -139,8 +139,10 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||
const handleResetDefaults = useCallback(() => {
|
||||
if (currentTab === SchemaView.VisualEditor) {
|
||||
setHoveringProperty(null)
|
||||
advancedEditing && setAdvancedEditing(false)
|
||||
isAddingNewField && setIsAddingNewField(false)
|
||||
if (advancedEditing)
|
||||
setAdvancedEditing(false)
|
||||
if (isAddingNewField)
|
||||
setIsAddingNewField(false)
|
||||
}
|
||||
setJsonSchema(DEFAULT_SCHEMA)
|
||||
setJson(JSON.stringify(DEFAULT_SCHEMA, null, 2))
|
||||
|
|
|
|||
|
|
@ -87,8 +87,10 @@ const EditCard: FC<EditCardProps> = ({
|
|||
})
|
||||
|
||||
useSubscribe('fieldChangeSuccess', () => {
|
||||
isAddingNewField && setIsAddingNewField(false)
|
||||
advancedEditing && setAdvancedEditing(false)
|
||||
if (isAddingNewField)
|
||||
setIsAddingNewField(false)
|
||||
if (advancedEditing)
|
||||
setAdvancedEditing(false)
|
||||
})
|
||||
|
||||
const emitPropertyNameChange = useCallback(() => {
|
||||
|
|
@ -150,14 +152,16 @@ const EditCard: FC<EditCardProps> = ({
|
|||
}, [isAdvancedEditing, emitPropertyOptionsChange, currentFields])
|
||||
|
||||
const handleAdvancedOptionsChange = useCallback((options: AdvancedOptionsType) => {
|
||||
let enumValue: any = options.enum
|
||||
if (enumValue === '') {
|
||||
let enumValue: SchemaEnumType | undefined
|
||||
if (options.enum === '') {
|
||||
enumValue = undefined
|
||||
}
|
||||
else {
|
||||
enumValue = options.enum.replace(/\s/g, '').split(',')
|
||||
const stringArray = options.enum.replace(/\s/g, '').split(',')
|
||||
if (currentFields.type === Type.number)
|
||||
enumValue = (enumValue as SchemaEnumType).map(value => Number(value)).filter(num => !Number.isNaN(num))
|
||||
enumValue = stringArray.map(value => Number(value)).filter(num => !Number.isNaN(num))
|
||||
else
|
||||
enumValue = stringArray
|
||||
}
|
||||
setCurrentFields(prev => ({ ...prev, enum: enumValue }))
|
||||
if (isAdvancedEditing) return
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
|
|||
onChange(backupSchema)
|
||||
setBackupSchema(null)
|
||||
}
|
||||
isAddingNewField && setIsAddingNewField(false)
|
||||
advancedEditing && setAdvancedEditing(false)
|
||||
if (isAddingNewField)
|
||||
setIsAddingNewField(false)
|
||||
if (advancedEditing)
|
||||
setAdvancedEditing(false)
|
||||
setHoveringProperty(null)
|
||||
})
|
||||
|
||||
|
|
@ -221,7 +223,8 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
|
|||
})
|
||||
|
||||
useSubscribe('addField', (params) => {
|
||||
advancedEditing && setAdvancedEditing(false)
|
||||
if (advancedEditing)
|
||||
setAdvancedEditing(false)
|
||||
setBackupSchema(jsonSchema)
|
||||
const { path } = params as AddEventParams
|
||||
setIsAddingNewField(true)
|
||||
|
|
|
|||
|
|
@ -293,6 +293,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||
type='string'
|
||||
description={t(`${i18nPrefix}.outputVars.output`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='reasoning_content'
|
||||
type='string'
|
||||
description={t(`${i18nPrefix}.outputVars.reasoning_content`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='usage'
|
||||
type='object'
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ type ConditionValueProps = {
|
|||
}
|
||||
const ConditionValue = ({
|
||||
variableSelector,
|
||||
labelName,
|
||||
labelName: _labelName,
|
||||
operator,
|
||||
value,
|
||||
}: ConditionValueProps) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ const VariableModalTrigger = ({
|
|||
open={open}
|
||||
onOpenChange={() => {
|
||||
setOpen(v => !v)
|
||||
open && onClose()
|
||||
if (open)
|
||||
onClose()
|
||||
}}
|
||||
placement='left-start'
|
||||
offset={{
|
||||
|
|
@ -45,7 +46,8 @@ const VariableModalTrigger = ({
|
|||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
setOpen(v => !v)
|
||||
open && onClose()
|
||||
if (open)
|
||||
onClose()
|
||||
}}>
|
||||
<Button variant='primary'>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ const VariableTrigger = ({
|
|||
open={open}
|
||||
onOpenChange={() => {
|
||||
setOpen(v => !v)
|
||||
open && onClose()
|
||||
if (open)
|
||||
onClose()
|
||||
}}
|
||||
placement='left-start'
|
||||
offset={{
|
||||
|
|
@ -43,7 +44,8 @@ const VariableTrigger = ({
|
|||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
setOpen(v => !v)
|
||||
open && onClose()
|
||||
if (open)
|
||||
onClose()
|
||||
}}>
|
||||
<Button variant='primary'>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
|
|
|
|||
|
|
@ -86,9 +86,12 @@ const RunPanel: FC<RunProps> = ({
|
|||
|
||||
const switchTab = async (tab: string) => {
|
||||
setCurrentTab(tab)
|
||||
if (tab === 'RESULT')
|
||||
runDetailUrl && await getResult()
|
||||
tracingListUrl && await getTracingList()
|
||||
if (tab === 'RESULT') {
|
||||
if (runDetailUrl)
|
||||
await getResult()
|
||||
}
|
||||
if (tracingListUrl)
|
||||
await getTracingList()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useNodeLoopInteractions } from './hooks'
|
|||
|
||||
const Node: FC<NodeProps<LoopNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
data: _data,
|
||||
}) => {
|
||||
const { zoom } = useViewport()
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ type MailAndPasswordAuthProps = {
|
|||
allowRegistration: boolean
|
||||
}
|
||||
|
||||
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration }: MailAndPasswordAuthProps) {
|
||||
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration: _allowRegistration }: MailAndPasswordAuthProps) {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18NContext)
|
||||
const router = useRouter()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export type Locale = typeof i18n['locales'][number]
|
|||
export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => {
|
||||
Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 })
|
||||
await changeLanguage(locale)
|
||||
reloadPage && location.reload()
|
||||
if (reloadPage)
|
||||
location.reload()
|
||||
}
|
||||
|
||||
export const getLocaleOnClient = (): Locale => {
|
||||
|
|
|
|||
|
|
@ -442,6 +442,7 @@ const translation = {
|
|||
},
|
||||
outputVars: {
|
||||
output: 'Generierter Inhalt',
|
||||
reasoning_content: 'Reasoning-Inhalt',
|
||||
usage: 'Nutzungsinformationen des Modells',
|
||||
},
|
||||
singleRun: {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue