Compare commits

..

14 Commits

Author SHA1 Message Date
Stephen Zhou
58aaf69426 add build 2026-04-15 00:18:48 +08:00
Stephen Zhou
8f18ac8c1a update 2026-04-15 00:14:02 +08:00
Stephen Zhou
00eeb82e85 update 2026-04-14 23:57:40 +08:00
Stephen Zhou
eefd9ee6c7 update 2026-04-14 23:51:58 +08:00
Stephen Zhou
aaeb806b36 chore: update 2026-04-14 23:45:08 +08:00
Stephen Zhou
25885f2fa8 cli package 2026-04-14 22:51:15 +08:00
Stephen Zhou
2c065e8a21 chore: enable noUncheckedIndexedAccess 2026-04-14 22:14:49 +08:00
Asuka Minato
648dde5e96 ci: Fix path in coverage markdown rendering step (#35136) 2026-04-14 13:49:03 +00:00
bohdansolovie
a3042e6332 test: migrate clean_notion_document integration tests to SQLAlchemy 2… (#35147)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-14 13:31:42 +00:00
bohdansolovie
e5fd3133f4 test: migrate task integration tests to SQLAlchemy 2.0 query APIs (#35170)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-14 13:27:39 +00:00
yyh
e1bbe57f9c refactor(web): re-design button api (#35166)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-14 13:22:23 +00:00
Joel
d4783e8c14 chore: url in tool description support clicking jump directly (#35163)
Some checks failed
Trigger i18n Sync on Push / trigger (push) Waiting to run
autofix.ci / autofix (push) Has been cancelled
Build and Push API & Web / build (api, {{defaultContext}}:api, Dockerfile, DIFY_API_IMAGE_NAME, linux/amd64, ubuntu-latest, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, {{defaultContext}}:api, Dockerfile, DIFY_API_IMAGE_NAME, linux/arm64, ubuntu-24.04-arm, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, {{defaultContext}}, web/Dockerfile, DIFY_WEB_IMAGE_NAME, linux/amd64, ubuntu-latest, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, {{defaultContext}}, web/Dockerfile, DIFY_WEB_IMAGE_NAME, linux/arm64, ubuntu-24.04-arm, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Main CI Pipeline / Skip Duplicate Checks (push) Has been cancelled
Main CI Pipeline / Check Changed Files (push) Has been cancelled
Main CI Pipeline / Run API Tests (push) Has been cancelled
Main CI Pipeline / Skip API Tests (push) Has been cancelled
Main CI Pipeline / API Tests (push) Has been cancelled
Main CI Pipeline / Run Web Tests (push) Has been cancelled
Main CI Pipeline / Skip Web Tests (push) Has been cancelled
Main CI Pipeline / Web Tests (push) Has been cancelled
Main CI Pipeline / Run Web Full-Stack E2E (push) Has been cancelled
Main CI Pipeline / Skip Web Full-Stack E2E (push) Has been cancelled
Main CI Pipeline / Web Full-Stack E2E (push) Has been cancelled
Main CI Pipeline / Style Check (push) Has been cancelled
Main CI Pipeline / Run VDB Tests (push) Has been cancelled
Main CI Pipeline / Skip VDB Tests (push) Has been cancelled
Main CI Pipeline / VDB Tests (push) Has been cancelled
Main CI Pipeline / Run DB Migration Test (push) Has been cancelled
Main CI Pipeline / Skip DB Migration Test (push) Has been cancelled
Main CI Pipeline / DB Migration Test (push) Has been cancelled
2026-04-14 09:55:55 +00:00
Blackoutta
736880e046 feat: support configurable redis key prefix (#35139) 2026-04-14 09:31:41 +00:00
wdeveloper16
bd7a9b5fcf refactor: replace bare dict with dict[str, Any] in model provider service and core modules (#35122)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
2026-04-14 09:18:30 +00:00
495 changed files with 4696 additions and 3944 deletions

View File

@@ -62,7 +62,7 @@ jobs:
- name: Render coverage markdown from structured data
id: render
run: |
comment_body="$(uv run --directory api python api/libs/pyrefly_type_coverage.py \
comment_body="$(uv run --directory api python libs/pyrefly_type_coverage.py \
--base base_report.json \
< pr_report.json)"

View File

@@ -57,6 +57,9 @@ REDIS_SSL_CERTFILE=
REDIS_SSL_KEYFILE=
# Path to client private key file for SSL authentication
REDIS_DB=0
# Optional global prefix for Redis keys, topics, streams, and Celery Redis transport artifacts.
# Leave empty to preserve current unprefixed behavior.
REDIS_KEY_PREFIX=
# redis Sentinel configuration.
REDIS_USE_SENTINEL=false

View File

@@ -32,6 +32,11 @@ class RedisConfig(BaseSettings):
default=0,
)
REDIS_KEY_PREFIX: str = Field(
description="Optional global prefix for Redis keys, topics, and transport artifacts",
default="",
)
REDIS_USE_SSL: bool = Field(
description="Enable SSL/TLS for the Redis connection",
default=False,

View File

@@ -1,7 +1,7 @@
import json
import re
from collections.abc import Generator
from typing import Union
from typing import Any, Union
from graphon.model_runtime.entities.llm_entities import LLMResultChunk
@@ -11,7 +11,7 @@ from core.agent.entities import AgentScratchpadUnit
class CotAgentOutputParser:
@classmethod
def handle_react_stream_output(
cls, llm_response: Generator[LLMResultChunk, None, None], usage_dict: dict
cls, llm_response: Generator[LLMResultChunk, None, None], usage_dict: dict[str, Any]
) -> Generator[Union[str, AgentScratchpadUnit.Action], None, None]:
def parse_action(action) -> Union[str, AgentScratchpadUnit.Action]:
action_name = None

View File

@@ -254,7 +254,7 @@ def resolve_dify_schema_refs(
return resolver.resolve(schema)
def _remove_metadata_fields(schema: dict) -> dict:
def _remove_metadata_fields(schema: dict[str, Any]) -> dict[str, Any]:
"""
Remove metadata fields from schema that shouldn't be included in resolved output

View File

@@ -9,6 +9,7 @@ from typing_extensions import TypedDict
from configs import dify_config
from dify_app import DifyApp
from extensions.redis_names import normalize_redis_key_prefix
class _CelerySentinelKwargsDict(TypedDict):
@@ -16,9 +17,10 @@ class _CelerySentinelKwargsDict(TypedDict):
password: str | None
class CelerySentinelTransportDict(TypedDict):
class CelerySentinelTransportDict(TypedDict, total=False):
master_name: str | None
sentinel_kwargs: _CelerySentinelKwargsDict
global_keyprefix: str
class CelerySSLOptionsDict(TypedDict):
@@ -61,15 +63,31 @@ def get_celery_ssl_options() -> CelerySSLOptionsDict | None:
def get_celery_broker_transport_options() -> CelerySentinelTransportDict | dict[str, Any]:
"""Get broker transport options (e.g. Redis Sentinel) for Celery connections."""
transport_options: CelerySentinelTransportDict | dict[str, Any]
if dify_config.CELERY_USE_SENTINEL:
return CelerySentinelTransportDict(
transport_options = CelerySentinelTransportDict(
master_name=dify_config.CELERY_SENTINEL_MASTER_NAME,
sentinel_kwargs=_CelerySentinelKwargsDict(
socket_timeout=dify_config.CELERY_SENTINEL_SOCKET_TIMEOUT,
password=dify_config.CELERY_SENTINEL_PASSWORD,
),
)
return {}
else:
transport_options = {}
global_keyprefix = get_celery_redis_global_keyprefix()
if global_keyprefix:
transport_options["global_keyprefix"] = global_keyprefix
return transport_options
def get_celery_redis_global_keyprefix() -> str | None:
"""Return the Redis transport prefix for Celery when namespace isolation is enabled."""
normalized_prefix = normalize_redis_key_prefix(dify_config.REDIS_KEY_PREFIX)
if not normalized_prefix:
return None
return f"{normalized_prefix}:"
def init_app(app: DifyApp) -> Celery:

View File

@@ -3,7 +3,7 @@ import logging
import ssl
from collections.abc import Callable
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Union
from typing import Any, Union, cast
import redis
from redis import RedisError
@@ -18,17 +18,26 @@ from typing_extensions import TypedDict
from configs import dify_config
from dify_app import DifyApp
from extensions.redis_names import (
normalize_redis_key_prefix,
serialize_redis_name,
serialize_redis_name_arg,
serialize_redis_name_args,
)
from libs.broadcast_channel.channel import BroadcastChannel as BroadcastChannelProtocol
from libs.broadcast_channel.redis.channel import BroadcastChannel as RedisBroadcastChannel
from libs.broadcast_channel.redis.sharded_channel import ShardedRedisBroadcastChannel
from libs.broadcast_channel.redis.streams_channel import StreamsBroadcastChannel
if TYPE_CHECKING:
from redis.lock import Lock
logger = logging.getLogger(__name__)
_normalize_redis_key_prefix = normalize_redis_key_prefix
_serialize_redis_name = serialize_redis_name
_serialize_redis_name_arg = serialize_redis_name_arg
_serialize_redis_name_args = serialize_redis_name_args
class RedisClientWrapper:
"""
A wrapper class for the Redis client that addresses the issue where the global
@@ -59,68 +68,148 @@ class RedisClientWrapper:
if self._client is None:
self._client = client
if TYPE_CHECKING:
# Type hints for IDE support and static analysis
# These are not executed at runtime but provide type information
def get(self, name: str | bytes) -> Any: ...
def set(
self,
name: str | bytes,
value: Any,
ex: int | None = None,
px: int | None = None,
nx: bool = False,
xx: bool = False,
keepttl: bool = False,
get: bool = False,
exat: int | None = None,
pxat: int | None = None,
) -> Any: ...
def setex(self, name: str | bytes, time: int | timedelta, value: Any) -> Any: ...
def setnx(self, name: str | bytes, value: Any) -> Any: ...
def delete(self, *names: str | bytes) -> Any: ...
def incr(self, name: str | bytes, amount: int = 1) -> Any: ...
def expire(
self,
name: str | bytes,
time: int | timedelta,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> Any: ...
def lock(
self,
name: str,
timeout: float | None = None,
sleep: float = 0.1,
blocking: bool = True,
blocking_timeout: float | None = None,
thread_local: bool = True,
) -> Lock: ...
def zadd(
self,
name: str | bytes,
mapping: dict[str | bytes | int | float, float | int | str | bytes],
nx: bool = False,
xx: bool = False,
ch: bool = False,
incr: bool = False,
gt: bool = False,
lt: bool = False,
) -> Any: ...
def zremrangebyscore(self, name: str | bytes, min: float | str, max: float | str) -> Any: ...
def zcard(self, name: str | bytes) -> Any: ...
def getdel(self, name: str | bytes) -> Any: ...
def pubsub(self) -> PubSub: ...
def pipeline(self, transaction: bool = True, shard_hint: str | None = None) -> Any: ...
def __getattr__(self, item: str) -> Any:
def _require_client(self) -> redis.Redis | RedisCluster:
if self._client is None:
raise RuntimeError("Redis client is not initialized. Call init_app first.")
return getattr(self._client, item)
return self._client
def _get_prefix(self) -> str:
return dify_config.REDIS_KEY_PREFIX
def get(self, name: str | bytes) -> Any:
return self._require_client().get(_serialize_redis_name_arg(name, self._get_prefix()))
def set(
self,
name: str | bytes,
value: Any,
ex: int | None = None,
px: int | None = None,
nx: bool = False,
xx: bool = False,
keepttl: bool = False,
get: bool = False,
exat: int | None = None,
pxat: int | None = None,
) -> Any:
return self._require_client().set(
_serialize_redis_name_arg(name, self._get_prefix()),
value,
ex=ex,
px=px,
nx=nx,
xx=xx,
keepttl=keepttl,
get=get,
exat=exat,
pxat=pxat,
)
def setex(self, name: str | bytes, time: int | timedelta, value: Any) -> Any:
return self._require_client().setex(_serialize_redis_name_arg(name, self._get_prefix()), time, value)
def setnx(self, name: str | bytes, value: Any) -> Any:
return self._require_client().setnx(_serialize_redis_name_arg(name, self._get_prefix()), value)
def delete(self, *names: str | bytes) -> Any:
return self._require_client().delete(*_serialize_redis_name_args(names, self._get_prefix()))
def incr(self, name: str | bytes, amount: int = 1) -> Any:
return self._require_client().incr(_serialize_redis_name_arg(name, self._get_prefix()), amount)
def expire(
self,
name: str | bytes,
time: int | timedelta,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> Any:
return self._require_client().expire(
_serialize_redis_name_arg(name, self._get_prefix()),
time,
nx=nx,
xx=xx,
gt=gt,
lt=lt,
)
def exists(self, *names: str | bytes) -> Any:
return self._require_client().exists(*_serialize_redis_name_args(names, self._get_prefix()))
def ttl(self, name: str | bytes) -> Any:
return self._require_client().ttl(_serialize_redis_name_arg(name, self._get_prefix()))
def getdel(self, name: str | bytes) -> Any:
return self._require_client().getdel(_serialize_redis_name_arg(name, self._get_prefix()))
def lock(
self,
name: str,
timeout: float | None = None,
sleep: float = 0.1,
blocking: bool = True,
blocking_timeout: float | None = None,
thread_local: bool = True,
) -> Any:
return self._require_client().lock(
_serialize_redis_name(name, self._get_prefix()),
timeout=timeout,
sleep=sleep,
blocking=blocking,
blocking_timeout=blocking_timeout,
thread_local=thread_local,
)
def hset(self, name: str | bytes, *args: Any, **kwargs: Any) -> Any:
return self._require_client().hset(_serialize_redis_name_arg(name, self._get_prefix()), *args, **kwargs)
def hgetall(self, name: str | bytes) -> Any:
return self._require_client().hgetall(_serialize_redis_name_arg(name, self._get_prefix()))
def hdel(self, name: str | bytes, *keys: str | bytes) -> Any:
return self._require_client().hdel(_serialize_redis_name_arg(name, self._get_prefix()), *keys)
def hlen(self, name: str | bytes) -> Any:
return self._require_client().hlen(_serialize_redis_name_arg(name, self._get_prefix()))
def zadd(
self,
name: str | bytes,
mapping: dict[str | bytes | int | float, float | int | str | bytes],
nx: bool = False,
xx: bool = False,
ch: bool = False,
incr: bool = False,
gt: bool = False,
lt: bool = False,
) -> Any:
return self._require_client().zadd(
_serialize_redis_name_arg(name, self._get_prefix()),
cast(Any, mapping),
nx=nx,
xx=xx,
ch=ch,
incr=incr,
gt=gt,
lt=lt,
)
def zremrangebyscore(self, name: str | bytes, min: float | str, max: float | str) -> Any:
return self._require_client().zremrangebyscore(_serialize_redis_name_arg(name, self._get_prefix()), min, max)
def zcard(self, name: str | bytes) -> Any:
return self._require_client().zcard(_serialize_redis_name_arg(name, self._get_prefix()))
def pubsub(self) -> PubSub:
return self._require_client().pubsub()
def pipeline(self, transaction: bool = True, shard_hint: str | None = None) -> Any:
return self._require_client().pipeline(transaction=transaction, shard_hint=shard_hint)
def __getattr__(self, item: str) -> Any:
return getattr(self._require_client(), item)
redis_client: RedisClientWrapper = RedisClientWrapper()

View File

@@ -0,0 +1,32 @@
from configs import dify_config
def normalize_redis_key_prefix(prefix: str | None) -> str:
"""Normalize the configured Redis key prefix for consistent runtime use."""
if prefix is None:
return ""
return prefix.strip()
def get_redis_key_prefix() -> str:
"""Read and normalize the current Redis key prefix from config."""
return normalize_redis_key_prefix(dify_config.REDIS_KEY_PREFIX)
def serialize_redis_name(name: str, prefix: str | None = None) -> str:
"""Convert a logical Redis name into the physical name used in Redis."""
normalized_prefix = get_redis_key_prefix() if prefix is None else normalize_redis_key_prefix(prefix)
if not normalized_prefix:
return name
return f"{normalized_prefix}:{name}"
def serialize_redis_name_arg(name: str | bytes, prefix: str | None = None) -> str | bytes:
"""Prefix string Redis names while preserving bytes inputs unchanged."""
if isinstance(name, bytes):
return name
return serialize_redis_name(name, prefix)
def serialize_redis_name_args(names: tuple[str | bytes, ...], prefix: str | None = None) -> tuple[str | bytes, ...]:
return tuple(serialize_redis_name_arg(name, prefix) for name in names)

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from typing import Any
from extensions.redis_names import serialize_redis_name
from libs.broadcast_channel.channel import Producer, Subscriber, Subscription
from redis import Redis, RedisCluster
@@ -32,12 +33,13 @@ class Topic:
def __init__(self, redis_client: Redis | RedisCluster, topic: str):
self._client = redis_client
self._topic = topic
self._redis_topic = serialize_redis_name(topic)
def as_producer(self) -> Producer:
return self
def publish(self, payload: bytes) -> None:
self._client.publish(self._topic, payload)
self._client.publish(self._redis_topic, payload)
def as_subscriber(self) -> Subscriber:
return self
@@ -46,7 +48,7 @@ class Topic:
return _RedisSubscription(
client=self._client,
pubsub=self._client.pubsub(),
topic=self._topic,
topic=self._redis_topic,
)

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from typing import Any
from extensions.redis_names import serialize_redis_name
from libs.broadcast_channel.channel import Producer, Subscriber, Subscription
from redis import Redis, RedisCluster
@@ -30,12 +31,13 @@ class ShardedTopic:
def __init__(self, redis_client: Redis | RedisCluster, topic: str):
self._client = redis_client
self._topic = topic
self._redis_topic = serialize_redis_name(topic)
def as_producer(self) -> Producer:
return self
def publish(self, payload: bytes) -> None:
self._client.spublish(self._topic, payload) # type: ignore[attr-defined,union-attr]
self._client.spublish(self._redis_topic, payload) # type: ignore[attr-defined,union-attr]
def as_subscriber(self) -> Subscriber:
return self
@@ -44,7 +46,7 @@ class ShardedTopic:
return _RedisShardedSubscription(
client=self._client,
pubsub=self._client.pubsub(),
topic=self._topic,
topic=self._redis_topic,
)

View File

@@ -6,6 +6,7 @@ import threading
from collections.abc import Iterator
from typing import Self
from extensions.redis_names import serialize_redis_name
from libs.broadcast_channel.channel import Producer, Subscriber, Subscription
from libs.broadcast_channel.exc import SubscriptionClosedError
from redis import Redis, RedisCluster
@@ -35,7 +36,7 @@ class StreamsTopic:
def __init__(self, redis_client: Redis | RedisCluster, topic: str, *, retention_seconds: int = 600):
self._client = redis_client
self._topic = topic
self._key = f"stream:{topic}"
self._key = serialize_redis_name(f"stream:{topic}")
self._retention_seconds = retention_seconds
self.max_length = 5000

View File

@@ -103,7 +103,10 @@ class DbMigrationAutoRenewLock:
timeout=self._ttl_seconds,
thread_local=False,
)
acquired = bool(self._lock.acquire(*args, **kwargs))
lock = self._lock
if lock is None:
raise RuntimeError("Redis lock initialization failed.")
acquired = bool(lock.acquire(*args, **kwargs))
self._acquired = acquired
if acquired:
self._start_heartbeat()

View File

@@ -1,4 +1,5 @@
import logging
from typing import Any
from graphon.model_runtime.entities.model_entities import ModelType, ParameterRule
@@ -168,7 +169,9 @@ class ModelProviderService:
model_name=model,
)
def get_provider_credential(self, tenant_id: str, provider: str, credential_id: str | None = None) -> dict | None:
def get_provider_credential(
self, tenant_id: str, provider: str, credential_id: str | None = None
) -> dict[str, Any] | None:
"""
get provider credentials.
@@ -180,7 +183,7 @@ class ModelProviderService:
provider_configuration = self._get_provider_configuration(tenant_id, provider)
return provider_configuration.get_provider_credential(credential_id=credential_id)
def validate_provider_credentials(self, tenant_id: str, provider: str, credentials: dict):
def validate_provider_credentials(self, tenant_id: str, provider: str, credentials: dict[str, Any]):
"""
validate provider credentials before saving.
@@ -192,7 +195,7 @@ class ModelProviderService:
provider_configuration.validate_provider_credentials(credentials)
def create_provider_credential(
self, tenant_id: str, provider: str, credentials: dict, credential_name: str | None
self, tenant_id: str, provider: str, credentials: dict[str, Any], credential_name: str | None
) -> None:
"""
Create and save new provider credentials.
@@ -210,7 +213,7 @@ class ModelProviderService:
self,
tenant_id: str,
provider: str,
credentials: dict,
credentials: dict[str, Any],
credential_id: str,
credential_name: str | None,
) -> None:
@@ -254,7 +257,7 @@ class ModelProviderService:
def get_model_credential(
self, tenant_id: str, provider: str, model_type: str, model: str, credential_id: str | None
) -> dict | None:
) -> dict[str, Any] | None:
"""
Retrieve model-specific credentials.
@@ -270,7 +273,9 @@ class ModelProviderService:
model_type=ModelType.value_of(model_type), model=model, credential_id=credential_id
)
def validate_model_credentials(self, tenant_id: str, provider: str, model_type: str, model: str, credentials: dict):
def validate_model_credentials(
self, tenant_id: str, provider: str, model_type: str, model: str, credentials: dict[str, Any]
):
"""
validate model credentials.
@@ -287,7 +292,13 @@ class ModelProviderService:
)
def create_model_credential(
self, tenant_id: str, provider: str, model_type: str, model: str, credentials: dict, credential_name: str | None
self,
tenant_id: str,
provider: str,
model_type: str,
model: str,
credentials: dict[str, Any],
credential_name: str | None,
) -> None:
"""
create and save model credentials.
@@ -314,7 +325,7 @@ class ModelProviderService:
provider: str,
model_type: str,
model: str,
credentials: dict,
credentials: dict[str, Any],
credential_id: str,
credential_name: str | None,
) -> None:

View File

@@ -33,6 +33,7 @@ REDIS_USERNAME=
REDIS_PASSWORD=difyai123456
REDIS_USE_SSL=false
REDIS_DB=0
REDIS_KEY_PREFIX=
# PostgreSQL database configuration
DB_USERNAME=postgres

View File

@@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
@@ -530,22 +531,18 @@ class TestAddDocumentToIndexTask:
redis_client.set(indexing_cache_key, "processing", ex=300)
# Verify logs exist before processing
existing_logs = (
db_session_with_containers.query(DatasetAutoDisableLog)
.where(DatasetAutoDisableLog.document_id == document.id)
.all()
)
existing_logs = db_session_with_containers.scalars(
select(DatasetAutoDisableLog).where(DatasetAutoDisableLog.document_id == document.id)
).all()
assert len(existing_logs) == 2
# Act: Execute the task
add_document_to_index_task(document.id)
# Assert: Verify auto disable logs were deleted
remaining_logs = (
db_session_with_containers.query(DatasetAutoDisableLog)
.where(DatasetAutoDisableLog.document_id == document.id)
.all()
)
remaining_logs = db_session_with_containers.scalars(
select(DatasetAutoDisableLog).where(DatasetAutoDisableLog.document_id == document.id)
).all()
assert len(remaining_logs) == 0
# Verify index processing occurred normally

View File

@@ -11,6 +11,7 @@ from unittest.mock import Mock, patch
import pytest
from faker import Faker
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType
@@ -267,11 +268,13 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit() # Ensure all changes are committed
# Check that segment is deleted
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
# Check that upload file is deleted
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1))
assert deleted_file is None
def test_batch_clean_document_task_with_image_files(
@@ -319,7 +322,9 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Check that segment is deleted
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
# Verify that the task completed successfully by checking the log output
@@ -360,14 +365,14 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Check that upload file is deleted
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1))
assert deleted_file is None
# Verify database cleanup
db_session_with_containers.commit()
# Check that upload file is deleted
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1))
assert deleted_file is None
def test_batch_clean_document_task_dataset_not_found(
@@ -410,7 +415,9 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Document should still exist since cleanup failed
existing_document = db_session_with_containers.query(Document).filter_by(id=document_id).first()
existing_document = db_session_with_containers.scalar(
select(Document).where(Document.id == document_id).limit(1)
)
assert existing_document is not None
def test_batch_clean_document_task_storage_cleanup_failure(
@@ -453,11 +460,13 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Check that segment is deleted from database
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
# Check that upload file is deleted from database
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1))
assert deleted_file is None
def test_batch_clean_document_task_multiple_documents(
@@ -510,12 +519,16 @@ class TestBatchCleanDocumentTask:
# Check that all segments are deleted
for segment_id in segment_ids:
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
# Check that all upload files are deleted
for file_id in file_ids:
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(
select(UploadFile).where(UploadFile.id == file_id).limit(1)
)
assert deleted_file is None
def test_batch_clean_document_task_different_doc_forms(
@@ -564,7 +577,9 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Check that segment is deleted
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
except Exception as e:
@@ -574,7 +589,9 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Check if the segment still exists (task may have failed before deletion)
existing_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
existing_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
if existing_segment is not None:
# If segment still exists, the task failed before deletion
# This is acceptable in test environments with external service issues
@@ -645,12 +662,16 @@ class TestBatchCleanDocumentTask:
# Check that all segments are deleted
for segment_id in segment_ids:
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
# Check that all upload files are deleted
for file_id in file_ids:
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(
select(UploadFile).where(UploadFile.id == file_id).limit(1)
)
assert deleted_file is None
def test_batch_clean_document_task_integration_with_real_database(
@@ -699,8 +720,16 @@ class TestBatchCleanDocumentTask:
db_session_with_containers.commit()
# Verify initial state
assert db_session_with_containers.query(DocumentSegment).filter_by(document_id=document.id).count() == 3
assert db_session_with_containers.query(UploadFile).filter_by(id=upload_file.id).first() is not None
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 3
)
assert (
db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == upload_file.id).limit(1))
is not None
)
# Store original IDs for verification
document_id = document.id
@@ -720,13 +749,20 @@ class TestBatchCleanDocumentTask:
# Check that all segments are deleted
for segment_id in segment_ids:
deleted_segment = db_session_with_containers.query(DocumentSegment).filter_by(id=segment_id).first()
deleted_segment = db_session_with_containers.scalar(
select(DocumentSegment).where(DocumentSegment.id == segment_id).limit(1)
)
assert deleted_segment is None
# Check that upload file is deleted
deleted_file = db_session_with_containers.query(UploadFile).filter_by(id=file_id).first()
deleted_file = db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1))
assert deleted_file is None
# Verify final database state
assert db_session_with_containers.query(DocumentSegment).filter_by(document_id=document_id).count() == 0
assert db_session_with_containers.query(UploadFile).filter_by(id=file_id).first() is None
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document_id)
)
== 0
)
assert db_session_with_containers.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1)) is None

View File

@@ -17,6 +17,7 @@ from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
@@ -37,13 +38,13 @@ class TestBatchCreateSegmentToIndexTask:
from extensions.ext_redis import redis_client
# Clear all test data
db_session_with_containers.query(DocumentSegment).delete()
db_session_with_containers.query(Document).delete()
db_session_with_containers.query(Dataset).delete()
db_session_with_containers.query(UploadFile).delete()
db_session_with_containers.query(TenantAccountJoin).delete()
db_session_with_containers.query(Tenant).delete()
db_session_with_containers.query(Account).delete()
db_session_with_containers.execute(delete(DocumentSegment))
db_session_with_containers.execute(delete(Document))
db_session_with_containers.execute(delete(Dataset))
db_session_with_containers.execute(delete(UploadFile))
db_session_with_containers.execute(delete(TenantAccountJoin))
db_session_with_containers.execute(delete(Tenant))
db_session_with_containers.execute(delete(Account))
db_session_with_containers.commit()
# Clear Redis cache
@@ -292,12 +293,9 @@ class TestBatchCreateSegmentToIndexTask:
# Verify results
# Check that segments were created
segments = (
db_session_with_containers.query(DocumentSegment)
.filter_by(document_id=document.id)
.order_by(DocumentSegment.position)
.all()
)
segments = db_session_with_containers.scalars(
select(DocumentSegment).where(DocumentSegment.document_id == document.id).order_by(DocumentSegment.position)
).all()
assert len(segments) == 3
# Verify segment content and metadata
@@ -367,11 +365,11 @@ class TestBatchCreateSegmentToIndexTask:
# Verify no segments were created (since dataset doesn't exist)
segments = db_session_with_containers.query(DocumentSegment).all()
segments = db_session_with_containers.scalars(select(DocumentSegment)).all()
assert len(segments) == 0
# Verify no documents were modified
documents = db_session_with_containers.query(Document).all()
documents = db_session_with_containers.scalars(select(Document)).all()
assert len(documents) == 0
def test_batch_create_segment_to_index_task_document_not_found(
@@ -415,12 +413,14 @@ class TestBatchCreateSegmentToIndexTask:
# Verify no segments were created
segments = db_session_with_containers.query(DocumentSegment).all()
segments = db_session_with_containers.scalars(select(DocumentSegment)).all()
assert len(segments) == 0
# Verify dataset remains unchanged (no segments were added to the dataset)
db_session_with_containers.refresh(dataset)
segments_for_dataset = db_session_with_containers.query(DocumentSegment).filter_by(dataset_id=dataset.id).all()
segments_for_dataset = db_session_with_containers.scalars(
select(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id)
).all()
assert len(segments_for_dataset) == 0
def test_batch_create_segment_to_index_task_document_not_available(
@@ -516,7 +516,9 @@ class TestBatchCreateSegmentToIndexTask:
assert cache_value == b"error"
# Verify no segments were created
segments = db_session_with_containers.query(DocumentSegment).filter_by(document_id=document.id).all()
segments = db_session_with_containers.scalars(
select(DocumentSegment).where(DocumentSegment.document_id == document.id)
).all()
assert len(segments) == 0
def test_batch_create_segment_to_index_task_upload_file_not_found(
@@ -560,7 +562,7 @@ class TestBatchCreateSegmentToIndexTask:
# Verify no segments were created
segments = db_session_with_containers.query(DocumentSegment).all()
segments = db_session_with_containers.scalars(select(DocumentSegment)).all()
assert len(segments) == 0
# Verify document remains unchanged
@@ -611,7 +613,7 @@ class TestBatchCreateSegmentToIndexTask:
# Verify error handling
# Since exception was raised, no segments should be created
segments = db_session_with_containers.query(DocumentSegment).all()
segments = db_session_with_containers.scalars(select(DocumentSegment)).all()
assert len(segments) == 0
# Verify document remains unchanged
@@ -682,12 +684,9 @@ class TestBatchCreateSegmentToIndexTask:
# Verify results
# Check that new segments were created with correct positions
all_segments = (
db_session_with_containers.query(DocumentSegment)
.filter_by(document_id=document.id)
.order_by(DocumentSegment.position)
.all()
)
all_segments = db_session_with_containers.scalars(
select(DocumentSegment).where(DocumentSegment.document_id == document.id).order_by(DocumentSegment.position)
).all()
assert len(all_segments) == 6 # 3 existing + 3 new
# Verify position ordering

View File

@@ -11,6 +11,7 @@ from unittest.mock import Mock, patch
import pytest
from faker import Faker
from sqlalchemy import func, select
from core.rag.index_processor.constant.index_type import IndexStructureType
from models.dataset import Dataset, Document, DocumentSegment
@@ -145,11 +146,16 @@ class TestCleanNotionDocumentTask:
db_session_with_containers.commit()
# Verify data exists before cleanup
assert db_session_with_containers.query(Document).filter(Document.id.in_(document_ids)).count() == 3
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id.in_(document_ids))
.count()
db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.id.in_(document_ids))
)
== 3
)
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids))
)
== 6
)
@@ -158,9 +164,9 @@ class TestCleanNotionDocumentTask:
# Verify segments are deleted
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id.in_(document_ids))
.count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids))
)
== 0
)
@@ -323,9 +329,9 @@ class TestCleanNotionDocumentTask:
# Verify segments are deleted
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id == document.id)
.count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 0
)
@@ -411,7 +417,9 @@ class TestCleanNotionDocumentTask:
# Verify segments are deleted
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 0
)
@@ -499,9 +507,16 @@ class TestCleanNotionDocumentTask:
db_session_with_containers.commit()
# Verify all data exists before cleanup
assert db_session_with_containers.query(Document).filter(Document.dataset_id == dataset.id).count() == 5
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.dataset_id == dataset.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id)
)
== 5
)
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id)
)
== 10
)
@@ -514,19 +529,26 @@ class TestCleanNotionDocumentTask:
# Verify only specified documents' segments are deleted
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id.in_(documents_to_clean))
.count()
db_session_with_containers.scalar(
select(func.count())
.select_from(DocumentSegment)
.where(DocumentSegment.document_id.in_(documents_to_clean))
)
== 0
)
# Verify remaining documents and segments are intact
remaining_docs = [doc.id for doc in documents[3:]]
assert db_session_with_containers.query(Document).filter(Document.id.in_(remaining_docs)).count() == 2
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id.in_(remaining_docs))
.count()
db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.id.in_(remaining_docs))
)
== 2
)
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(remaining_docs))
)
== 4
)
@@ -613,7 +635,9 @@ class TestCleanNotionDocumentTask:
# Verify all segments exist before cleanup
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 4
)
@@ -622,7 +646,9 @@ class TestCleanNotionDocumentTask:
# Verify all segments are deleted regardless of status
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 0
)
@@ -795,11 +821,15 @@ class TestCleanNotionDocumentTask:
# Verify all data exists before cleanup
assert (
db_session_with_containers.query(Document).filter(Document.dataset_id == dataset.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id)
)
== num_documents
)
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.dataset_id == dataset.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id)
)
== num_documents * num_segments_per_doc
)
@@ -809,7 +839,9 @@ class TestCleanNotionDocumentTask:
# Verify all segments are deleted
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.dataset_id == dataset.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id)
)
== 0
)
@@ -906,8 +938,8 @@ class TestCleanNotionDocumentTask:
# Verify all data exists before cleanup
# Note: There may be documents from previous tests, so we check for at least 3
assert db_session_with_containers.query(Document).count() >= 3
assert db_session_with_containers.query(DocumentSegment).count() >= 9
assert db_session_with_containers.scalar(select(func.count()).select_from(Document)) >= 3
assert db_session_with_containers.scalar(select(func.count()).select_from(DocumentSegment)) >= 9
# Clean up documents from only the first dataset
target_dataset = datasets[0]
@@ -919,19 +951,26 @@ class TestCleanNotionDocumentTask:
# Verify only documents' segments from target dataset are deleted
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id == target_document.id)
.count()
db_session_with_containers.scalar(
select(func.count())
.select_from(DocumentSegment)
.where(DocumentSegment.document_id == target_document.id)
)
== 0
)
# Verify documents from other datasets remain intact
remaining_docs = [doc.id for doc in all_documents[1:]]
assert db_session_with_containers.query(Document).filter(Document.id.in_(remaining_docs)).count() == 2
assert (
db_session_with_containers.query(DocumentSegment)
.filter(DocumentSegment.document_id.in_(remaining_docs))
.count()
db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.id.in_(remaining_docs))
)
== 2
)
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(remaining_docs))
)
== 6
)
@@ -1028,11 +1067,13 @@ class TestCleanNotionDocumentTask:
db_session_with_containers.commit()
# Verify all data exists before cleanup
assert db_session_with_containers.query(Document).filter(Document.dataset_id == dataset.id).count() == len(
document_statuses
)
assert db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id)
) == len(document_statuses)
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.dataset_id == dataset.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id)
)
== len(document_statuses) * 2
)
@@ -1042,7 +1083,9 @@ class TestCleanNotionDocumentTask:
# Verify all segments are deleted regardless of status
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.dataset_id == dataset.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id)
)
== 0
)
@@ -1142,9 +1185,16 @@ class TestCleanNotionDocumentTask:
db_session_with_containers.commit()
# Verify data exists before cleanup
assert db_session_with_containers.query(Document).filter(Document.id == document.id).count() == 1
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(Document).where(Document.id == document.id)
)
== 1
)
assert (
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 3
)
@@ -1153,7 +1203,9 @@ class TestCleanNotionDocumentTask:
# Verify segments are deleted
assert (
db_session_with_containers.query(DocumentSegment).filter(DocumentSegment.document_id == document.id).count()
db_session_with_containers.scalar(
select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id)
)
== 0
)

View File

@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from sqlalchemy import select
from core.indexing_runner import DocumentIsPausedError
from core.rag.index_processor.constant.index_type import IndexTechniqueType
@@ -175,7 +176,7 @@ class TestDatasetIndexingTaskIntegration:
def _query_document(self, db_session_with_containers, document_id: str) -> Document | None:
"""Return the latest persisted document state."""
return db_session_with_containers.query(Document).where(Document.id == document_id).first()
return db_session_with_containers.scalar(select(Document).where(Document.id == document_id).limit(1))
def _assert_documents_parsing(self, db_session_with_containers, document_ids: Sequence[str]) -> None:
"""Assert all target documents are persisted in parsing status."""

View File

@@ -12,6 +12,7 @@ from unittest.mock import Mock, patch
from uuid import uuid4
import pytest
from sqlalchemy import delete, func, select, update
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
@@ -254,8 +255,8 @@ class TestDocumentIndexingSyncTask:
"""Test that task raises error when data_source_info is empty."""
# Arrange
context = self._create_notion_sync_context(db_session_with_containers, data_source_info=None)
db_session_with_containers.query(Document).where(Document.id == context["document"].id).update(
{"data_source_info": None}
db_session_with_containers.execute(
update(Document).where(Document.id == context["document"].id).values(data_source_info=None)
)
db_session_with_containers.commit()
@@ -274,8 +275,8 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
assert updated_document is not None
assert updated_document.indexing_status == IndexingStatus.ERROR
@@ -294,13 +295,13 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
remaining_segments = (
db_session_with_containers.query(DocumentSegment)
remaining_segments = db_session_with_containers.scalar(
select(func.count())
.select_from(DocumentSegment)
.where(DocumentSegment.document_id == context["document"].id)
.count()
)
assert updated_document is not None
assert updated_document.indexing_status == IndexingStatus.COMPLETED
@@ -319,13 +320,13 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
remaining_segments = (
db_session_with_containers.query(DocumentSegment)
remaining_segments = db_session_with_containers.scalar(
select(func.count())
.select_from(DocumentSegment)
.where(DocumentSegment.document_id == context["document"].id)
.count()
)
assert updated_document is not None
@@ -354,7 +355,7 @@ class TestDocumentIndexingSyncTask:
context = self._create_notion_sync_context(db_session_with_containers)
def _delete_dataset_before_clean() -> str:
db_session_with_containers.query(Dataset).where(Dataset.id == context["dataset"].id).delete()
db_session_with_containers.execute(delete(Dataset).where(Dataset.id == context["dataset"].id))
db_session_with_containers.commit()
return "2024-01-02T00:00:00Z"
@@ -367,8 +368,8 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
assert updated_document is not None
assert updated_document.indexing_status == IndexingStatus.PARSING
@@ -386,13 +387,13 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
remaining_segments = (
db_session_with_containers.query(DocumentSegment)
remaining_segments = db_session_with_containers.scalar(
select(func.count())
.select_from(DocumentSegment)
.where(DocumentSegment.document_id == context["document"].id)
.count()
)
assert updated_document is not None
assert updated_document.indexing_status == IndexingStatus.PARSING
@@ -410,8 +411,8 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
assert updated_document is not None
assert updated_document.indexing_status == IndexingStatus.PARSING
@@ -428,8 +429,8 @@ class TestDocumentIndexingSyncTask:
# Assert
db_session_with_containers.expire_all()
updated_document = (
db_session_with_containers.query(Document).where(Document.id == context["document"].id).first()
updated_document = db_session_with_containers.scalar(
select(Document).where(Document.id == context["document"].id).limit(1)
)
assert updated_document is not None
assert updated_document.indexing_status == IndexingStatus.ERROR

View File

@@ -236,6 +236,41 @@ def test_pubsub_redis_url_required_when_default_unavailable(monkeypatch: pytest.
_ = DifyConfig().normalized_pubsub_redis_url
def test_dify_config_exposes_redis_key_prefix_default(monkeypatch: pytest.MonkeyPatch):
os.environ.clear()
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
monkeypatch.setenv("DB_TYPE", "postgresql")
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(_env_file=None)
assert config.REDIS_KEY_PREFIX == ""
def test_dify_config_reads_redis_key_prefix_from_env(monkeypatch: pytest.MonkeyPatch):
os.environ.clear()
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
monkeypatch.setenv("DB_TYPE", "postgresql")
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("REDIS_KEY_PREFIX", "enterprise-a")
config = DifyConfig(_env_file=None)
assert config.REDIS_KEY_PREFIX == "enterprise-a"
@pytest.mark.parametrize(
("broker_url", "expected_host", "expected_port", "expected_username", "expected_password", "expected_db"),
[

View File

@@ -7,6 +7,47 @@ from unittest.mock import MagicMock, patch
class TestCelerySSLConfiguration:
"""Test suite for Celery SSL configuration."""
def test_get_celery_broker_transport_options_includes_global_keyprefix_for_redis(self):
mock_config = MagicMock()
mock_config.CELERY_USE_SENTINEL = False
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
with patch("extensions.ext_celery.dify_config", mock_config):
from extensions.ext_celery import get_celery_broker_transport_options
result = get_celery_broker_transport_options()
assert result["global_keyprefix"] == "enterprise-a:"
def test_get_celery_broker_transport_options_omits_global_keyprefix_when_prefix_empty(self):
mock_config = MagicMock()
mock_config.CELERY_USE_SENTINEL = False
mock_config.REDIS_KEY_PREFIX = " "
with patch("extensions.ext_celery.dify_config", mock_config):
from extensions.ext_celery import get_celery_broker_transport_options
result = get_celery_broker_transport_options()
assert "global_keyprefix" not in result
def test_get_celery_broker_transport_options_keeps_sentinel_and_adds_global_keyprefix(self):
mock_config = MagicMock()
mock_config.CELERY_USE_SENTINEL = True
mock_config.CELERY_SENTINEL_MASTER_NAME = "mymaster"
mock_config.CELERY_SENTINEL_SOCKET_TIMEOUT = 0.1
mock_config.CELERY_SENTINEL_PASSWORD = "secret"
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
with patch("extensions.ext_celery.dify_config", mock_config):
from extensions.ext_celery import get_celery_broker_transport_options
result = get_celery_broker_transport_options()
assert result["master_name"] == "mymaster"
assert result["sentinel_kwargs"]["password"] == "secret"
assert result["global_keyprefix"] == "enterprise-a:"
def test_get_celery_ssl_options_when_ssl_disabled(self):
"""Test SSL options when BROKER_USE_SSL is False."""
from configs import DifyConfig
@@ -151,3 +192,49 @@ class TestCelerySSLConfiguration:
# Check that SSL is also applied to Redis backend
assert "redis_backend_use_ssl" in celery_app.conf
assert celery_app.conf["redis_backend_use_ssl"] is not None
def test_celery_init_applies_global_keyprefix_to_broker_and_backend_transport(self):
mock_config = MagicMock()
mock_config.BROKER_USE_SSL = False
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
mock_config.HUMAN_INPUT_TIMEOUT_TASK_INTERVAL = 1
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
mock_config.CELERY_BACKEND = "redis"
mock_config.CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
mock_config.CELERY_USE_SENTINEL = False
mock_config.LOG_FORMAT = "%(message)s"
mock_config.LOG_TZ = "UTC"
mock_config.LOG_FILE = None
mock_config.CELERY_TASK_ANNOTATIONS = {}
mock_config.CELERY_BEAT_SCHEDULER_TIME = 1
mock_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK = False
mock_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK = False
mock_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK = False
mock_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK = False
mock_config.ENABLE_CLEAN_MESSAGES = False
mock_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK = False
mock_config.ENABLE_DATASETS_QUEUE_MONITOR = False
mock_config.ENABLE_HUMAN_INPUT_TIMEOUT_TASK = False
mock_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK = False
mock_config.MARKETPLACE_ENABLED = False
mock_config.WORKFLOW_LOG_CLEANUP_ENABLED = False
mock_config.ENABLE_WORKFLOW_RUN_CLEANUP_TASK = False
mock_config.ENABLE_WORKFLOW_SCHEDULE_POLLER_TASK = False
mock_config.WORKFLOW_SCHEDULE_POLLER_INTERVAL = 1
mock_config.ENABLE_TRIGGER_PROVIDER_REFRESH_TASK = False
mock_config.TRIGGER_PROVIDER_REFRESH_INTERVAL = 15
mock_config.ENABLE_API_TOKEN_LAST_USED_UPDATE_TASK = False
mock_config.API_TOKEN_LAST_USED_UPDATE_INTERVAL = 30
mock_config.ENTERPRISE_ENABLED = False
mock_config.ENTERPRISE_TELEMETRY_ENABLED = False
with patch("extensions.ext_celery.dify_config", mock_config):
from dify_app import DifyApp
from extensions.ext_celery import init_app
app = DifyApp(__name__)
celery_app = init_app(app)
assert celery_app.conf["broker_transport_options"]["global_keyprefix"] == "enterprise-a:"
assert celery_app.conf["result_backend_transport_options"]["global_keyprefix"] == "enterprise-a:"

View File

@@ -6,6 +6,7 @@ from libs.broadcast_channel.redis.sharded_channel import ShardedRedisBroadcastCh
def test_get_pubsub_broadcast_channel_defaults_to_pubsub(monkeypatch):
monkeypatch.setattr(dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", "pubsub")
monkeypatch.setattr(ext_redis, "_pubsub_redis_client", object())
channel = ext_redis.get_pubsub_broadcast_channel()
@@ -14,6 +15,7 @@ def test_get_pubsub_broadcast_channel_defaults_to_pubsub(monkeypatch):
def test_get_pubsub_broadcast_channel_sharded(monkeypatch):
monkeypatch.setattr(dify_config, "PUBSUB_REDIS_CHANNEL_TYPE", "sharded")
monkeypatch.setattr(ext_redis, "_pubsub_redis_client", object())
channel = ext_redis.get_pubsub_broadcast_channel()

View File

@@ -1,12 +1,15 @@
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from redis import RedisError
from redis.retry import Retry
from extensions.ext_redis import (
RedisClientWrapper,
_get_base_redis_params,
_get_cluster_connection_health_params,
_get_connection_health_params,
_normalize_redis_key_prefix,
_serialize_redis_name,
redis_fallback,
)
@@ -123,3 +126,99 @@ class TestRedisFallback:
assert test_func.__name__ == "test_func"
assert test_func.__doc__ == "Test function docstring"
class TestRedisKeyPrefixHelpers:
def test_normalize_redis_key_prefix_trims_whitespace(self):
assert _normalize_redis_key_prefix(" enterprise-a ") == "enterprise-a"
def test_normalize_redis_key_prefix_treats_whitespace_only_as_empty(self):
assert _normalize_redis_key_prefix(" ") == ""
def test_serialize_redis_name_returns_original_when_prefix_empty(self):
assert _serialize_redis_name("model_lb_index:test", "") == "model_lb_index:test"
def test_serialize_redis_name_adds_single_colon_separator(self):
assert _serialize_redis_name("model_lb_index:test", "enterprise-a") == "enterprise-a:model_lb_index:test"
class TestRedisClientWrapperKeyPrefix:
def test_wrapper_get_prefixes_string_keys(self):
mock_client = MagicMock()
wrapper = RedisClientWrapper()
wrapper.initialize(mock_client)
with patch("extensions.ext_redis.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
wrapper.get("oauth_state:abc")
mock_client.get.assert_called_once_with("enterprise-a:oauth_state:abc")
def test_wrapper_delete_prefixes_multiple_keys(self):
mock_client = MagicMock()
wrapper = RedisClientWrapper()
wrapper.initialize(mock_client)
with patch("extensions.ext_redis.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
wrapper.delete("key:a", "key:b")
mock_client.delete.assert_called_once_with("enterprise-a:key:a", "enterprise-a:key:b")
def test_wrapper_lock_prefixes_lock_name(self):
mock_client = MagicMock()
wrapper = RedisClientWrapper()
wrapper.initialize(mock_client)
with patch("extensions.ext_redis.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
wrapper.lock("resource-lock", timeout=10)
mock_client.lock.assert_called_once()
args, kwargs = mock_client.lock.call_args
assert args == ("enterprise-a:resource-lock",)
assert kwargs["timeout"] == 10
def test_wrapper_hash_operations_prefix_key_name(self):
mock_client = MagicMock()
wrapper = RedisClientWrapper()
wrapper.initialize(mock_client)
with patch("extensions.ext_redis.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
wrapper.hset("hash:key", "field", "value")
wrapper.hgetall("hash:key")
mock_client.hset.assert_called_once_with("enterprise-a:hash:key", "field", "value")
mock_client.hgetall.assert_called_once_with("enterprise-a:hash:key")
def test_wrapper_zadd_prefixes_sorted_set_name(self):
mock_client = MagicMock()
wrapper = RedisClientWrapper()
wrapper.initialize(mock_client)
with patch("extensions.ext_redis.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
wrapper.zadd("zset:key", {"member": 1})
mock_client.zadd.assert_called_once()
args, kwargs = mock_client.zadd.call_args
assert args == ("enterprise-a:zset:key", {"member": 1})
assert kwargs["nx"] is False
def test_wrapper_preserves_keys_when_prefix_is_empty(self):
mock_client = MagicMock()
wrapper = RedisClientWrapper()
wrapper.initialize(mock_client)
with patch("extensions.ext_redis.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = " "
wrapper.get("plain:key")
mock_client.get.assert_called_once_with("plain:key")

View File

@@ -139,6 +139,28 @@ class TestTopic:
mock_redis_client.publish.assert_called_once_with("test-topic", payload)
def test_publish_prefixes_regular_topic(self, mock_redis_client: MagicMock):
with patch("extensions.redis_names.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
topic = Topic(mock_redis_client, "test-topic")
topic.publish(b"test message")
mock_redis_client.publish.assert_called_once_with("enterprise-a:test-topic", b"test message")
def test_subscribe_prefixes_regular_topic(self, mock_redis_client: MagicMock):
with patch("extensions.redis_names.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
topic = Topic(mock_redis_client, "test-topic")
subscription = topic.subscribe()
try:
subscription._start_if_needed()
finally:
subscription.close()
mock_redis_client.pubsub.return_value.subscribe.assert_called_once_with("enterprise-a:test-topic")
class TestShardedTopic:
"""Test cases for the ShardedTopic class."""
@@ -176,6 +198,15 @@ class TestShardedTopic:
mock_redis_client.spublish.assert_called_once_with("test-sharded-topic", payload)
def test_publish_prefixes_sharded_topic(self, mock_redis_client: MagicMock):
with patch("extensions.redis_names.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
sharded_topic = ShardedTopic(mock_redis_client, "test-sharded-topic")
sharded_topic.publish(b"test sharded message")
mock_redis_client.spublish.assert_called_once_with("enterprise-a:test-sharded-topic", b"test sharded message")
def test_subscribe_returns_sharded_subscription(self, sharded_topic: ShardedTopic, mock_redis_client: MagicMock):
"""Test that subscribe() returns a _RedisShardedSubscription instance."""
subscription = sharded_topic.subscribe()
@@ -185,6 +216,19 @@ class TestShardedTopic:
assert subscription._pubsub is mock_redis_client.pubsub.return_value
assert subscription._topic == "test-sharded-topic"
def test_subscribe_prefixes_sharded_topic(self, mock_redis_client: MagicMock):
with patch("extensions.redis_names.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
sharded_topic = ShardedTopic(mock_redis_client, "test-sharded-topic")
subscription = sharded_topic.subscribe()
try:
subscription._start_if_needed()
finally:
subscription.close()
mock_redis_client.pubsub.return_value.ssubscribe.assert_called_once_with("enterprise-a:test-sharded-topic")
@dataclasses.dataclass(frozen=True)
class SubscriptionTestCase:

View File

@@ -2,6 +2,7 @@ import threading
import time
from dataclasses import dataclass
from typing import cast
from unittest.mock import patch
import pytest
@@ -150,6 +151,25 @@ class TestStreamsBroadcastChannel:
# Expire called after publish
assert fake_redis._expire_calls.get("stream:beta", 0) >= 1
def test_topic_uses_prefixed_stream_key(self, fake_redis: FakeStreamsRedis):
with patch("extensions.redis_names.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
topic = StreamsBroadcastChannel(fake_redis, retention_seconds=60).topic("alpha")
assert topic._topic == "alpha"
assert topic._key == "enterprise-a:stream:alpha"
def test_publish_uses_prefixed_stream_key(self, fake_redis: FakeStreamsRedis):
with patch("extensions.redis_names.dify_config") as mock_config:
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
topic = StreamsBroadcastChannel(fake_redis, retention_seconds=60).topic("beta")
topic.publish(b"hello")
assert fake_redis._store["enterprise-a:stream:beta"][0][1] == {b"data": b"hello"}
assert fake_redis._expire_calls.get("enterprise-a:stream:beta", 0) >= 1
def test_topic_exposes_self_as_producer_and_subscriber(self, streams_channel: StreamsBroadcastChannel):
topic = streams_channel.topic("producer-subscriber")

View File

@@ -351,6 +351,9 @@ REDIS_SSL_CERTFILE=
REDIS_SSL_KEYFILE=
# Path to client private key file for SSL authentication
REDIS_DB=0
# Optional global prefix for Redis keys, topics, streams, and Celery Redis transport artifacts.
# Leave empty to preserve current unprefixed behavior.
REDIS_KEY_PREFIX=
# Optional: limit total Redis connections used by API/Worker (unset for default)
# Align with API's REDIS_MAX_CONNECTIONS in configs
REDIS_MAX_CONNECTIONS=

View File

@@ -88,6 +88,7 @@ The `.env.example` file provided in the Docker setup is extensive and covers a w
1. **Redis Configuration**:
- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis server connection settings.
- `REDIS_KEY_PREFIX`: Optional global namespace prefix for Redis keys, topics, streams, and Celery Redis transport artifacts.
1. **Celery Configuration**:

View File

@@ -90,6 +90,7 @@ x-shared-env: &shared-api-worker-env
REDIS_SSL_CERTFILE: ${REDIS_SSL_CERTFILE:-}
REDIS_SSL_KEYFILE: ${REDIS_SSL_KEYFILE:-}
REDIS_DB: ${REDIS_DB:-0}
REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-}
REDIS_MAX_CONNECTIONS: ${REDIS_MAX_CONNECTIONS:-}
REDIS_USE_SENTINEL: ${REDIS_USE_SENTINEL:-false}
REDIS_SENTINELS: ${REDIS_SENTINELS:-}

28
packages/cli/bin/dify-cli.js Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const entryFile = path.join(packageRoot, 'dist', 'cli.mjs')
if (!fs.existsSync(entryFile))
throw new Error(`Built CLI entry not found at ${entryFile}. Run "pnpm --filter @dify/cli build" first.`)
const result = spawnSync(
process.execPath,
[entryFile, ...process.argv.slice(2)],
{
cwd: process.cwd(),
env: process.env,
stdio: 'inherit',
},
)
if (result.error)
throw result.error
process.exit(result.status ?? 1)

20
packages/cli/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "@dify/cli",
"private": true,
"version": "0.0.0-private",
"type": "module",
"bin": {
"dify-cli": "./bin/dify-cli.js"
},
"scripts": {
"build": "vp pack"
},
"dependencies": {
"typescript": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
"vite": "catalog:",
"vite-plus": "catalog:"
}
}

54
packages/cli/src/cli.ts Normal file
View File

@@ -0,0 +1,54 @@
import process from 'node:process'
import { runMigrationCommand } from './no-unchecked-indexed-access/migrate'
import { runNormalizeCommand } from './no-unchecked-indexed-access/normalize'
import { runBatchMigrationCommand } from './no-unchecked-indexed-access/run'
type CommandHandler = (argv: string[]) => Promise<void>
const COMMANDS = new Map<string, CommandHandler>([
['migrate', runMigrationCommand],
['normalize', runNormalizeCommand],
['run', runBatchMigrationCommand],
])
function printUsage() {
console.log(`Usage:
dify-cli no-unchecked-indexed-access migrate [options]
dify-cli no-unchecked-indexed-access run [options]
dify-cli no-unchecked-indexed-access normalize`)
}
async function main() {
const [group, command, ...restArgs] = process.argv.slice(2)
if (!group || group === 'help' || group === '--help' || group === '-h') {
printUsage()
return
}
if (group !== 'no-unchecked-indexed-access') {
printUsage()
throw new Error(`Unknown command group: ${group}`)
}
if (!command || command === 'help' || command === '--help' || command === '-h') {
printUsage()
return
}
const handler = COMMANDS.get(command)
if (!handler) {
printUsage()
throw new Error(`Unknown command: ${command}`)
}
await handler(restArgs)
}
try {
await main()
}
catch (error) {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { normalizeMalformedAssertions } from './migrate'
const ROOT = process.cwd()
const EXTENSIONS = new Set(['.ts', '.tsx'])
async function collectFiles(directory: string): Promise<string[]> {
const entries = await fs.readdir(directory, { withFileTypes: true })
const files: string[] = []
for (const entry of entries) {
if (entry.name === 'node_modules' || entry.name === '.next')
continue
const absolutePath = path.join(directory, entry.name)
if (entry.isDirectory()) {
files.push(...await collectFiles(absolutePath))
continue
}
if (!EXTENSIONS.has(path.extname(entry.name)))
continue
files.push(absolutePath)
}
return files
}
async function main() {
const files = await collectFiles(ROOT)
let changedFileCount = 0
await Promise.all(files.map(async (fileName) => {
const currentText = await fs.readFile(fileName, 'utf8')
const nextText = normalizeMalformedAssertions(currentText)
if (nextText === currentText)
return
await fs.writeFile(fileName, nextText)
changedFileCount += 1
}))
console.log(`Normalized malformed assertion syntax in ${changedFileCount} file(s).`)
}
export async function runNormalizeCommand(_argv: string[]) {
await main()
}

View File

@@ -0,0 +1,232 @@
import { execFile } from 'node:child_process'
import path from 'node:path'
import process from 'node:process'
import { promisify } from 'node:util'
import { runMigration, SUPPORTED_DIAGNOSTIC_CODES } from './migrate'
const execFileAsync = promisify(execFile)
const DIAGNOSTIC_PATTERN = /^(.+?\.(?:ts|tsx))\((\d+),(\d+)\): error TS(\d+): (.+)$/
const DEFAULT_BATCH_SIZE = 100
const DEFAULT_BATCH_ITERATIONS = 5
const DEFAULT_MAX_ROUNDS = 20
type CliOptions = {
batchIterations: number
batchSize: number
maxRounds: number
project: string
verbose: boolean
}
type DiagnosticEntry = {
code: number
fileName: string
line: number
message: string
}
function parseArgs(argv: string[]): CliOptions {
const options: CliOptions = {
batchIterations: DEFAULT_BATCH_ITERATIONS,
batchSize: DEFAULT_BATCH_SIZE,
maxRounds: DEFAULT_MAX_ROUNDS,
project: 'tsconfig.json',
verbose: false,
}
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]
if (arg === '--')
continue
if (arg === '--verbose') {
options.verbose = true
continue
}
if (arg === '--project') {
const value = argv[i + 1]
if (!value)
throw new Error('Missing value for --project')
options.project = value
i += 1
continue
}
if (arg === '--batch-size') {
const value = Number(argv[i + 1])
if (!Number.isInteger(value) || value <= 0)
throw new Error('Invalid value for --batch-size')
options.batchSize = value
i += 1
continue
}
if (arg === '--batch-iterations') {
const value = Number(argv[i + 1])
if (!Number.isInteger(value) || value <= 0)
throw new Error('Invalid value for --batch-iterations')
options.batchIterations = value
i += 1
continue
}
if (arg === '--max-rounds') {
const value = Number(argv[i + 1])
if (!Number.isInteger(value) || value <= 0)
throw new Error('Invalid value for --max-rounds')
options.maxRounds = value
i += 1
continue
}
throw new Error(`Unknown option: ${arg}`)
}
return options
}
async function runTypeCheck(project: string): Promise<{ diagnostics: DiagnosticEntry[], exitCode: number, rawOutput: string }> {
const projectDirectory = path.dirname(path.resolve(process.cwd(), project))
try {
const { stdout, stderr } = await execFileAsync('pnpm', ['exec', 'tsc', '--noEmit', '--pretty', 'false', '--incremental', 'false', '--project', project], {
cwd: projectDirectory,
env: {
...process.env,
NODE_OPTIONS: process.env.NODE_OPTIONS ?? '--max-old-space-size=8192',
},
maxBuffer: 1024 * 1024 * 32,
})
const rawOutput = `${stdout}${stderr}`.trim()
return {
diagnostics: parseDiagnostics(rawOutput, projectDirectory),
exitCode: 0,
rawOutput,
}
}
catch (error) {
const exitCode = typeof error === 'object' && error && 'code' in error && typeof error.code === 'number'
? error.code
: 1
const stdout = typeof error === 'object' && error && 'stdout' in error && typeof error.stdout === 'string'
? error.stdout
: ''
const stderr = typeof error === 'object' && error && 'stderr' in error && typeof error.stderr === 'string'
? error.stderr
: ''
const rawOutput = `${stdout}${stderr}`.trim()
return {
diagnostics: parseDiagnostics(rawOutput, projectDirectory),
exitCode,
rawOutput,
}
}
}
function parseDiagnostics(rawOutput: string, projectDirectory: string): DiagnosticEntry[] {
return rawOutput
.split('\n')
.map(line => line.trim())
.flatMap((line) => {
const match = line.match(DIAGNOSTIC_PATTERN)
if (!match)
return []
return [{
code: Number(match[4]),
fileName: path.resolve(projectDirectory, match[1]!),
line: Number(match[2]),
message: match[5] ?? '',
}]
})
}
function summarizeCodes(diagnostics: DiagnosticEntry[]): string {
const counts = new Map<number, number>()
for (const diagnostic of diagnostics)
counts.set(diagnostic.code, (counts.get(diagnostic.code) ?? 0) + 1)
return Array.from(counts.entries())
.sort((left, right) => right[1] - left[1])
.slice(0, 8)
.map(([code, count]) => `TS${code}:${count}`)
.join(', ')
}
function chunk<T>(items: T[], size: number): T[][] {
const batches: T[][] = []
for (let i = 0; i < items.length; i += size)
batches.push(items.slice(i, i + size))
return batches
}
async function runBatchMigration(options: CliOptions) {
for (let round = 1; round <= options.maxRounds; round += 1) {
const { diagnostics, exitCode, rawOutput } = await runTypeCheck(options.project)
if (exitCode === 0) {
console.log(`Type check passed after ${round - 1} migration round(s).`)
return
}
const supportedDiagnostics = diagnostics.filter(diagnostic => SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code))
const unsupportedDiagnostics = diagnostics.filter(diagnostic => !SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code))
const supportedFiles = Array.from(new Set(supportedDiagnostics.map(diagnostic => diagnostic.fileName)))
console.log(`Round ${round}: ${diagnostics.length} diagnostic(s). ${summarizeCodes(diagnostics)}`)
if (options.verbose) {
for (const diagnostic of diagnostics.slice(0, 40))
console.log(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`)
}
if (supportedFiles.length === 0) {
console.error('No supported diagnostics remain to migrate.')
if (unsupportedDiagnostics.length > 0) {
console.error('Remaining unsupported diagnostics:')
for (const diagnostic of unsupportedDiagnostics.slice(0, 40))
console.error(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`)
}
if (rawOutput)
process.stderr.write(`${rawOutput}\n`)
process.exitCode = 1
return
}
let roundEdits = 0
const batches = chunk(supportedFiles, options.batchSize)
for (const [index, batch] of batches.entries()) {
console.log(` Batch ${index + 1}/${batches.length}: ${batch.length} file(s)`)
const result = await runMigration({
files: batch,
maxIterations: options.batchIterations,
project: options.project,
verbose: options.verbose,
write: true,
})
roundEdits += result.totalEdits
}
if (roundEdits === 0) {
console.error('Migration script made no edits in this round; stopping to avoid an infinite loop.')
process.exitCode = 1
return
}
}
console.error(`Reached --max-rounds=${options.maxRounds} before type check passed.`)
process.exitCode = 1
}
export async function runBatchMigrationCommand(argv: string[]) {
await runBatchMigration(parseArgs(argv))
}

View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite-plus'
export default defineConfig({
pack: {
clean: true,
deps: {
neverBundle: ['typescript'],
},
entry: ['src/cli.ts'],
format: ['esm'],
outDir: 'dist',
platform: 'node',
sourcemap: true,
target: 'node22',
treeshake: true,
},
})

19
pnpm-lock.yaml generated
View File

@@ -599,6 +599,22 @@ importers:
specifier: 'catalog:'
version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)
packages/cli:
dependencies:
typescript:
specifier: 'catalog:'
version: 6.0.2
devDependencies:
'@types/node':
specifier: 'catalog:'
version: 25.6.0
vite:
specifier: npm:@voidzero-dev/vite-plus-core@0.1.16
version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
vite-plus:
specifier: 'catalog:'
version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)
packages/iconify-collections:
devDependencies:
iconify-import-svg:
@@ -953,6 +969,9 @@ importers:
'@chromatic-com/storybook':
specifier: 'catalog:'
version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
'@dify/cli':
specifier: workspace:*
version: link:../packages/cli
'@dify/iconify-collections':
specifier: workspace:*
version: link:../packages/iconify-collections

View File

@@ -5,7 +5,6 @@ import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
@@ -14,6 +13,7 @@ import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import { docURL } from './config'
@@ -621,7 +621,7 @@ const ProviderConfigModal: FC<Props> = ({
</div>
<div className="my-8 flex h-8 items-center justify-between">
<a
className="flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]"
className="flex items-center space-x-1 text-xs leading-[18px] font-normal text-[#155EEF]"
target="_blank"
href={docURL[type]}
>

View File

@@ -1,5 +1,5 @@
'use client'
import type { ButtonProps } from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/ui/button'
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { SiteInfo } from '@/models/share'
import type { HumanInputFormError } from '@/service/use-share'
@@ -13,12 +13,12 @@ import * as React from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item'
import ExpirationTime from '@/app/components/base/chat/chat/answer/human-input-content/expiration-time'
import { getButtonStyle } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
import Loading from '@/app/components/base/loading'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { Button } from '@/app/components/base/ui/button'
import useDocumentTitle from '@/hooks/use-document-title'
import { useParams } from '@/next/navigation'
import { useGetHumanInputForm, useSubmitHumanInputForm } from '@/service/use-share'
@@ -100,7 +100,7 @@ const FormContent = () => {
if (success) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className="min-w-[480px] max-w-[640px]">
<div className="max-w-[640px] min-w-[480px]">
<div className="border-components-divider-subtle flex h-[320px] flex-col gap-4 radius-3xl border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-xs">
<div className="h-[56px] w-[56px] shrink-0 rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3">
<RiCheckboxCircleFill className="h-8 w-8 text-text-success" />
@@ -109,7 +109,7 @@ const FormContent = () => {
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.thanks', { ns: 'share' })}</div>
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.recorded', { ns: 'share' })}</div>
</div>
<div className="system-2xs-regular-uppercase shrink-0 text-text-tertiary">{t('humanInput.submissionID', { id: token, ns: 'share' })}</div>
<div className="shrink-0 system-2xs-regular-uppercase text-text-tertiary">{t('humanInput.submissionID', { id: token, ns: 'share' })}</div>
</div>
<div className="flex flex-row-reverse px-2 py-3">
<div className={cn(
@@ -128,7 +128,7 @@ const FormContent = () => {
if (expired) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className="min-w-[480px] max-w-[640px]">
<div className="max-w-[640px] min-w-[480px]">
<div className="border-components-divider-subtle flex h-[320px] flex-col gap-4 radius-3xl border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-xs">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3">
<RiInformation2Fill className="h-8 w-8 text-text-accent" />
@@ -137,7 +137,7 @@ const FormContent = () => {
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.sorry', { ns: 'share' })}</div>
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.expired', { ns: 'share' })}</div>
</div>
<div className="system-2xs-regular-uppercase shrink-0 text-text-tertiary">{t('humanInput.submissionID', { id: token, ns: 'share' })}</div>
<div className="shrink-0 system-2xs-regular-uppercase text-text-tertiary">{t('humanInput.submissionID', { id: token, ns: 'share' })}</div>
</div>
<div className="flex flex-row-reverse px-2 py-3">
<div className={cn(
@@ -156,7 +156,7 @@ const FormContent = () => {
if (submitted) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className="min-w-[480px] max-w-[640px]">
<div className="max-w-[640px] min-w-[480px]">
<div className="border-components-divider-subtle flex h-[320px] flex-col gap-4 radius-3xl border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-xs">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3">
<RiInformation2Fill className="h-8 w-8 text-text-accent" />
@@ -165,7 +165,7 @@ const FormContent = () => {
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.sorry', { ns: 'share' })}</div>
<div className="title-4xl-semi-bold text-text-primary">{t('humanInput.completed', { ns: 'share' })}</div>
</div>
<div className="system-2xs-regular-uppercase shrink-0 text-text-tertiary">{t('humanInput.submissionID', { id: token, ns: 'share' })}</div>
<div className="shrink-0 system-2xs-regular-uppercase text-text-tertiary">{t('humanInput.submissionID', { id: token, ns: 'share' })}</div>
</div>
<div className="flex flex-row-reverse px-2 py-3">
<div className={cn(
@@ -184,7 +184,7 @@ const FormContent = () => {
if (rateLimitExceeded) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className="min-w-[480px] max-w-[640px]">
<div className="max-w-[640px] min-w-[480px]">
<div className="border-components-divider-subtle flex h-[320px] flex-col gap-4 radius-3xl border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-xs">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3">
<RiErrorWarningFill className="h-8 w-8 text-text-destructive" />
@@ -210,7 +210,7 @@ const FormContent = () => {
if (!formData) {
return (
<div className={cn('flex h-full w-full flex-col items-center justify-center')}>
<div className="min-w-[480px] max-w-[640px]">
<div className="max-w-[640px] min-w-[480px]">
<div className="border-components-divider-subtle flex h-[320px] flex-col gap-4 radius-3xl border bg-chat-bubble-bg p-10 pb-9 shadow-lg backdrop-blur-xs">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge p-3">
<RiErrorWarningFill className="h-8 w-8 text-text-destructive" />
@@ -245,7 +245,7 @@ const FormContent = () => {
background={site.icon_background}
imageUrl={site.icon_url}
/>
<div className="system-xl-semibold grow text-text-primary">{site.title}</div>
<div className="grow system-xl-semibold text-text-primary">{site.title}</div>
</div>
<div className="h-0 w-full grow overflow-y-auto">
<div className="border-components-divider-subtle radius-3xl border bg-chat-bubble-bg p-4 shadow-lg backdrop-blur-xs">

View File

@@ -2,8 +2,8 @@
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import Countdown from '@/app/components/signin/countdown'
import { useLocale } from '@/context/i18n'
@@ -62,9 +62,9 @@ export default function CheckCode() {
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg">
<RiMailSendFill className="h-6 w-6 text-2xl" />
</div>
<div className="pb-4 pt-2">
<div className="pt-2 pb-4">
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<p className="mt-2 body-md-regular text-text-secondary">
<span>
{t('checkCode.tipsPrefix', { ns: 'login' })}
<strong>{email}</strong>
@@ -76,7 +76,7 @@ export default function CheckCode() {
<form action="">
<input type="text" className="hidden" />
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<label htmlFor="code" className="mb-1 system-md-semibold text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('checkCode.verificationCodePlaceholder', { ns: 'login' }) || ''} />
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('checkCode.verify', { ns: 'login' })}</Button>
<Countdown onResend={resendCode} />
@@ -88,7 +88,7 @@ export default function CheckCode() {
<div className="bg-background-default-dimm inline-block rounded-full p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
<span className="ml-2 system-xs-regular">{t('back', { ns: 'login' })}</span>
</div>
</div>
)

View File

@@ -3,8 +3,8 @@ import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
@@ -64,9 +64,9 @@ export default function CheckCode() {
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg">
<RiLockPasswordLine className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<div className="pt-2 pb-4">
<h2 className="title-4xl-semi-bold text-text-primary">{t('resetPassword', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<p className="mt-2 body-md-regular text-text-secondary">
{t('resetPasswordDesc', { ns: 'login' })}
</p>
</div>
@@ -74,7 +74,7 @@ export default function CheckCode() {
<form onSubmit={noop}>
<input type="text" className="hidden" />
<div className="mb-2">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
<label htmlFor="email" className="my-2 system-md-semibold text-text-secondary">{t('email', { ns: 'login' })}</label>
<div className="mt-1">
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
</div>
@@ -90,7 +90,7 @@ export default function CheckCode() {
<div className="inline-block rounded-full bg-background-default-dimmed p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('backToLogin', { ns: 'login' })}</span>
<span className="ml-2 system-xs-regular">{t('backToLogin', { ns: 'login' })}</span>
</Link>
</div>
)

View File

@@ -3,8 +3,8 @@ import { RiCheckboxCircleFill } from '@remixicon/react'
import { useCountDown } from 'ahooks'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { validPassword } from '@/config'
import { useRouter, useSearchParams } from '@/next/navigation'
@@ -91,7 +91,7 @@ const ChangePasswordForm = () => {
<h2 className="title-4xl-semi-bold text-text-primary">
{t('changePassword', { ns: 'login' })}
</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<p className="mt-2 body-md-regular text-text-secondary">
{t('changePasswordTip', { ns: 'login' })}
</p>
</div>
@@ -100,7 +100,7 @@ const ChangePasswordForm = () => {
<div className="bg-white">
{/* Password */}
<div className="mb-5">
<label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
<label htmlFor="password" className="my-2 system-md-semibold text-text-secondary">
{t('account.newPassword', { ns: 'common' })}
</label>
<div className="relative mt-1">
@@ -122,11 +122,11 @@ const ChangePasswordForm = () => {
</Button>
</div>
</div>
<div className="body-xs-regular mt-1 text-text-secondary">{t('error.passwordInvalid', { ns: 'login' })}</div>
<div className="mt-1 body-xs-regular text-text-secondary">{t('error.passwordInvalid', { ns: 'login' })}</div>
</div>
{/* Confirm Password */}
<div className="mb-5">
<label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
<label htmlFor="confirmPassword" className="my-2 system-md-semibold text-text-secondary">
{t('account.confirmPassword', { ns: 'common' })}
</label>
<div className="relative mt-1">

View File

@@ -3,8 +3,8 @@ import type { FormEvent } from 'react'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import Countdown from '@/app/components/signin/countdown'
import { useLocale } from '@/context/i18n'
@@ -100,9 +100,9 @@ export default function CheckCode() {
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg">
<RiMailSendFill className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<div className="pt-2 pb-4">
<h2 className="title-4xl-semi-bold text-text-primary">{t('checkCode.checkYourEmail', { ns: 'login' })}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<p className="mt-2 body-md-regular text-text-secondary">
<span>
{t('checkCode.tipsPrefix', { ns: 'login' })}
<strong>{email}</strong>
@@ -113,7 +113,7 @@ export default function CheckCode() {
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<label htmlFor="code" className="mb-1 system-md-semibold text-text-secondary">{t('checkCode.verificationCode', { ns: 'login' })}</label>
<Input
ref={codeInputRef}
id="code"
@@ -133,7 +133,7 @@ export default function CheckCode() {
<div className="bg-background-default-dimm inline-block rounded-full p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('back', { ns: 'login' })}</span>
<span className="ml-2 system-xs-regular">{t('back', { ns: 'login' })}</span>
</div>
</div>
)

View File

@@ -1,8 +1,8 @@
import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
@@ -52,7 +52,7 @@ export default function MailAndCodeAuth() {
<form onSubmit={noop}>
<input type="text" className="hidden" />
<div className="mb-2">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('email', { ns: 'login' })}</label>
<label htmlFor="email" className="my-2 system-md-semibold text-text-secondary">{t('email', { ns: 'login' })}</label>
<div className="mt-1">
<Input id="email" type="email" value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
</div>

View File

@@ -2,8 +2,8 @@
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'
@@ -103,7 +103,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
return (
<form onSubmit={noop}>
<div className="mb-3">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
<label htmlFor="email" className="my-2 system-md-semibold text-text-secondary">
{t('email', { ns: 'login' })}
</label>
<div className="mt-1">

View File

@@ -2,8 +2,8 @@
import type { FC } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useRouter, useSearchParams } from '@/next/navigation'
import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share'

View File

@@ -10,10 +10,10 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
import { Avatar } from '@/app/components/base/ui/avatar'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { toast } from '@/app/components/base/ui/toast'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
@@ -151,7 +151,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
<Dialog open={isShowDeleteConfirm} onOpenChange={open => !open && setIsShowDeleteConfirm(false)}>
<DialogContent className="w-[362px]! p-6!">
<div className="mb-3 text-text-primary title-2xl-semi-bold">{t('avatar.deleteTitle', { ns: 'common' })}</div>
<div className="mb-3 title-2xl-semi-bold text-text-primary">{t('avatar.deleteTitle', { ns: 'common' })}</div>
<p className="mb-8 text-text-secondary">{t('avatar.deleteDescription', { ns: 'common' })}</p>
<div className="flex w-full items-center justify-center gap-2">
@@ -159,7 +159,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
<Button variant="primary" destructive className="w-full" onClick={handleDeleteAvatar}>
{t('operation.delete', { ns: 'common' })}
</Button>
</div>

View File

@@ -3,8 +3,8 @@ import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { toast } from '@/app/components/base/ui/toast'
import { useRouter } from '@/next/navigation'
@@ -183,19 +183,19 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
return (
<Dialog open={show} onOpenChange={open => !open && onClose()}>
<DialogContent className="w-[420px]! p-6!">
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
<div className="absolute top-5 right-5 cursor-pointer p-1.5" onClick={onClose}>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div>
{step === STEP.start && (
<>
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.title', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="text-text-warning body-md-medium">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
<div className="text-text-secondary body-md-regular">
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('account.changeEmail.title', { ns: 'common' })}</div>
<div className="space-y-0.5 pt-1 pb-2">
<div className="body-md-medium text-text-warning">{t('account.changeEmail.authTip', { ns: 'common' })}</div>
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="account.changeEmail.content1"
ns="common"
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
values={{ email }}
/>
</div>
@@ -220,19 +220,19 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
)}
{step === STEP.verifyOrigin && (
<>
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="text-text-secondary body-md-regular">
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('account.changeEmail.verifyEmail', { ns: 'common' })}</div>
<div className="space-y-0.5 pt-1 pb-2">
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="account.changeEmail.content2"
ns="common"
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
values={{ email }}
/>
</div>
</div>
<div className="pt-3">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
<Input
className="w-full!"
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
@@ -257,25 +257,25 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
<div className="mt-3 flex items-center gap-1 system-xs-regular text-text-tertiary">
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
{time > 0 && (
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
)}
{!time && (
<span onClick={sendCodeToOriginEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
<span onClick={sendCodeToOriginEmail} className="cursor-pointer system-xs-medium text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
)}
</div>
</>
)}
{step === STEP.newEmail && (
<>
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="text-text-secondary body-md-regular">{t('account.changeEmail.content3', { ns: 'common' })}</div>
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('account.changeEmail.newEmail', { ns: 'common' })}</div>
<div className="space-y-0.5 pt-1 pb-2">
<div className="body-md-regular text-text-secondary">{t('account.changeEmail.content3', { ns: 'common' })}</div>
</div>
<div className="pt-3">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('account.changeEmail.emailLabel', { ns: 'common' })}</div>
<Input
className="w-full!"
placeholder={t('account.changeEmail.emailPlaceholder', { ns: 'common' })}
@@ -284,10 +284,10 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
destructive={newEmailExited || unAvailableEmail}
/>
{newEmailExited && (
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
<div className="mt-1 py-0.5 body-xs-regular text-text-destructive">{t('account.changeEmail.existingEmail', { ns: 'common' })}</div>
)}
{unAvailableEmail && (
<div className="mt-1 py-0.5 text-text-destructive body-xs-regular">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
<div className="mt-1 py-0.5 body-xs-regular text-text-destructive">{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}</div>
)}
</div>
<div className="mt-3 space-y-2">
@@ -310,19 +310,19 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
)}
{step === STEP.verifyNew && (
<>
<div className="pb-3 text-text-primary title-2xl-semi-bold">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="text-text-secondary body-md-regular">
<div className="pb-3 title-2xl-semi-bold text-text-primary">{t('account.changeEmail.verifyNew', { ns: 'common' })}</div>
<div className="space-y-0.5 pt-1 pb-2">
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="account.changeEmail.content4"
ns="common"
components={{ email: <span className="text-text-primary body-md-medium"></span> }}
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
values={{ email: mail }}
/>
</div>
</div>
<div className="pt-3">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-medium">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">{t('account.changeEmail.codeLabel', { ns: 'common' })}</div>
<Input
className="w-full!"
placeholder={t('account.changeEmail.codePlaceholder', { ns: 'common' })}
@@ -347,13 +347,13 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
<div className="mt-3 flex items-center gap-1 text-text-tertiary system-xs-regular">
<div className="mt-3 flex items-center gap-1 system-xs-regular text-text-tertiary">
<span>{t('account.changeEmail.resendTip', { ns: 'common' })}</span>
{time > 0 && (
<span>{t('account.changeEmail.resendCount', { ns: 'common', count: time })}</span>
)}
{!time && (
<span onClick={sendCodeToNewEmail} className="cursor-pointer text-text-accent-secondary system-xs-medium">{t('account.changeEmail.resend', { ns: 'common' })}</span>
<span onClick={sendCodeToNewEmail} className="cursor-pointer system-xs-medium text-text-accent-secondary">{t('account.changeEmail.resend', { ns: 'common' })}</span>
)}
</div>
</>

View File

@@ -8,9 +8,9 @@ import { useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import PremiumBadge from '@/app/components/base/premium-badge'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { toast } from '@/app/components/base/ui/toast'
import Collapse from '@/app/components/header/account-setting/collapse'
@@ -140,20 +140,20 @@ export default function AccountPage() {
imageUrl={icon_url}
/>
</div>
<div className="mt-[3px] text-text-secondary system-sm-medium">{item.name}</div>
<div className="mt-[3px] system-sm-medium text-text-secondary">{item.name}</div>
</div>
)
}
return (
<>
<div className="pb-3 pt-2">
<h4 className="text-text-primary title-2xl-semi-bold">{t('account.myAccount', { ns: 'common' })}</h4>
<div className="pt-2 pb-3">
<h4 className="title-2xl-semi-bold text-text-primary">{t('account.myAccount', { ns: 'common' })}</h4>
</div>
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size="3xl" />
<div className="ml-4">
<p className="text-text-primary system-xl-semibold">
<p className="system-xl-semibold text-text-primary">
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
@@ -162,16 +162,16 @@ export default function AccountPage() {
</PremiumBadge>
)}
</p>
<p className="text-text-tertiary system-xs-regular">{userProfile.email}</p>
<p className="system-xs-regular text-text-tertiary">{userProfile.email}</p>
</div>
</div>
<div className="mb-8">
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
<div className="mt-2 flex w-full items-center justify-between gap-2">
<div className="flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled system-sm-regular">
<div className="flex-1 rounded-lg bg-components-input-bg-normal p-2 system-sm-regular text-components-input-text-filled">
<span className="pl-1">{userProfile.name}</span>
</div>
<div className="cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text system-sm-medium" onClick={handleEditName}>
<div className="cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 system-sm-medium text-components-button-tertiary-text" onClick={handleEditName}>
{t('operation.edit', { ns: 'common' })}
</div>
</div>
@@ -179,11 +179,11 @@ export default function AccountPage() {
<div className="mb-8">
<div className={titleClassName}>{t('account.email', { ns: 'common' })}</div>
<div className="mt-2 flex w-full items-center justify-between gap-2">
<div className="flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled system-sm-regular">
<div className="flex-1 rounded-lg bg-components-input-bg-normal p-2 system-sm-regular text-components-input-text-filled">
<span className="pl-1">{userProfile.email}</span>
</div>
{systemFeatures.enable_change_email && (
<div className="cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text system-sm-medium" onClick={() => setShowUpdateEmail(true)}>
<div className="cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 system-sm-medium text-components-button-tertiary-text" onClick={() => setShowUpdateEmail(true)}>
{t('operation.change', { ns: 'common' })}
</div>
)}
@@ -193,8 +193,8 @@ export default function AccountPage() {
systemFeatures.enable_email_password_login && (
<div className="mb-8 flex justify-between gap-2">
<div>
<div className="mb-1 text-text-secondary system-sm-semibold">{t('account.password', { ns: 'common' })}</div>
<div className="mb-2 text-text-tertiary body-xs-regular">{t('account.passwordTip', { ns: 'common' })}</div>
<div className="mb-1 system-sm-semibold text-text-secondary">{t('account.password', { ns: 'common' })}</div>
<div className="mb-2 body-xs-regular text-text-tertiary">{t('account.passwordTip', { ns: 'common' })}</div>
</div>
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</Button>
</div>
@@ -218,7 +218,7 @@ export default function AccountPage() {
editNameModalVisible && (
<Dialog open={editNameModalVisible} onOpenChange={open => !open && setEditNameModalVisible(false)}>
<DialogContent className="w-[420px]! p-6!">
<div className="mb-6 text-text-primary title-2xl-semi-bold">{t('account.editName', { ns: 'common' })}</div>
<div className="mb-6 title-2xl-semi-bold text-text-primary">{t('account.editName', { ns: 'common' })}</div>
<div className={titleClassName}>{t('account.name', { ns: 'common' })}</div>
<Input
className="mt-2"
@@ -243,7 +243,7 @@ export default function AccountPage() {
editPasswordModalVisible && (
<Dialog open={editPasswordModalVisible} onOpenChange={open => !open && (setEditPasswordModalVisible(false), resetPasswordForm())}>
<DialogContent className="w-[420px]! p-6!">
<div className="mb-6 text-text-primary title-2xl-semi-bold">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
<div className="mb-6 title-2xl-semi-bold text-text-primary">{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}</div>
{userProfile.is_password_set && (
<>
<div className={titleClassName}>{t('account.currentPassword', { ns: 'common' })}</div>
@@ -266,7 +266,7 @@ export default function AccountPage() {
</div>
</>
)}
<div className="mt-8 text-text-secondary system-sm-semibold">
<div className="mt-8 system-sm-semibold text-text-secondary">
{userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })}
</div>
<div className="relative mt-2">
@@ -285,7 +285,7 @@ export default function AccountPage() {
</Button>
</div>
</div>
<div className="mt-8 text-text-secondary system-sm-semibold">{t('account.confirmPassword', { ns: 'common' })}</div>
<div className="mt-8 system-sm-semibold text-text-secondary">{t('account.confirmPassword', { ns: 'common' })}</div>
<div className="relative mt-2">
<Input
type={showConfirmPassword ? 'text' : 'password'}

View File

@@ -1,8 +1,8 @@
'use client'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { useAppContext } from '@/context/app-context'
import Link from '@/next/link'
import { useSendDeleteAccountEmail } from '../state'
@@ -30,14 +30,14 @@ export default function CheckEmail(props: DeleteAccountProps) {
return (
<>
<div className="body-md-medium py-1 text-text-destructive">
<div className="py-1 body-md-medium text-text-destructive">
{t('account.deleteTip', { ns: 'common' })}
</div>
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
<div className="pt-1 pb-2 body-md-regular text-text-secondary">
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
</div>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.deleteLabel', { ns: 'common' })}</label>
<label className="mt-3 mb-1 flex h-6 items-center system-sm-semibold text-text-secondary">{t('account.deleteLabel', { ns: 'common' })}</label>
<Input
placeholder={t('account.deletePlaceholder', { ns: 'common' }) as string}
onChange={(e) => {

View File

@@ -1,9 +1,9 @@
'use client'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import CustomDialog from '@/app/components/base/dialog'
import Textarea from '@/app/components/base/textarea'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useAppContext } from '@/context/app-context'
import { useRouter } from '@/next/navigation'
@@ -54,7 +54,7 @@ export default function FeedBack(props: DeleteAccountProps) {
className="max-w-[480px]"
footer={false}
>
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
<label className="mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
<Textarea
rows={6}
value={userFeedback}

View File

@@ -1,8 +1,8 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import Countdown from '@/app/components/signin/countdown'
import Link from '@/next/link'
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'
@@ -36,14 +36,14 @@ export default function VerifyEmail(props: DeleteAccountProps) {
}, [emailToken, verificationCode, confirmDeleteAccount, props])
return (
<>
<div className="body-md-medium pt-1 text-text-destructive">
<div className="pt-1 body-md-medium text-text-destructive">
{t('account.deleteTip', { ns: 'common' })}
</div>
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
<div className="pt-1 pb-2 body-md-regular text-text-secondary">
{t('account.deletePrivacyLinkTip', { ns: 'common' })}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('account.deletePrivacyLink', { ns: 'common' })}</Link>
</div>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('account.verificationLabel', { ns: 'common' })}</label>
<label className="mt-3 mb-1 flex h-6 items-center system-sm-semibold text-text-secondary">{t('account.verificationLabel', { ns: 'common' })}</label>
<Input
minLength={6}
maxLength={6}
@@ -53,7 +53,7 @@ export default function VerifyEmail(props: DeleteAccountProps) {
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="primary" destructive onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
<Countdown onResend={sendEmail} />
</div>

View File

@@ -2,8 +2,8 @@
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { Button } from '@/app/components/base/ui/button'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useRouter } from '@/next/navigation'
import Avatar from './avatar'
@@ -32,10 +32,10 @@ const Header = () => {
: <DifyLogo />}
</div>
<div className="h-4 w-px origin-center rotate-[11.31deg] bg-divider-regular" />
<p className="title-3xl-semi-bold relative mt-[-2px] text-text-primary">{t('account.account', { ns: 'common' })}</p>
<p className="relative mt-[-2px] title-3xl-semi-bold text-text-primary">{t('account.account', { ns: 'common' })}</p>
</div>
<div className="flex shrink-0 items-center gap-3">
<Button className="system-sm-medium gap-2 px-3 py-2" onClick={goToStudio}>
<Button className="gap-2 px-3 py-2 system-sm-medium" onClick={goToStudio}>
<RiRobot2Line className="h-4 w-4" />
<p>{t('account.studio', { ns: 'common' })}</p>
<RiArrowRightUpLine className="h-4 w-4" />

View File

@@ -10,9 +10,9 @@ import {
import * as React from 'react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { Avatar } from '@/app/components/base/ui/avatar'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
@@ -122,13 +122,13 @@ export default function OAuthAuthorize() {
</div>
)}
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
<div className={`mt-5 mb-4 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
<div className="title-4xl-semi-bold">
{isLoggedIn && <div className="text-text-primary">{t('connect', { ns: 'oauth' })}</div>}
<div className="text-(--color-saas-dify-blue-inverted)">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })}</div>
{!isLoggedIn && <div className="text-text-primary">{t('tips.notLoggedIn', { ns: 'oauth' })}</div>}
</div>
<div className="text-text-secondary body-md-regular">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}</div>
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('unknownApp', { ns: 'oauth' })} ${t('tips.loggedIn', { ns: 'oauth' })}` : t('tips.needLogin', { ns: 'oauth' })}</div>
</div>
{isLoggedIn && userProfile && (
@@ -137,7 +137,7 @@ export default function OAuthAuthorize() {
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
<div>
<div className="system-md-semi-bold text-text-secondary">{userProfile.name}</div>
<div className="text-text-tertiary system-xs-regular">{userProfile.email}</div>
<div className="system-xs-regular text-text-tertiary">{userProfile.email}</div>
</div>
</div>
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('switchAccount', { ns: 'oauth' })}</Button>
@@ -149,7 +149,7 @@ export default function OAuthAuthorize() {
{authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
const Icon = SCOPE_INFO_MAP[scope]
return (
<div key={scope} className="flex items-center gap-2 text-text-secondary body-sm-medium">
<div key={scope} className="flex items-center gap-2 body-sm-medium text-text-secondary">
{Icon ? <Icon.icon className="h-4 w-4" /> : <RiAccountCircleLine className="h-4 w-4" />}
{Icon.label}
</div>
@@ -182,7 +182,7 @@ export default function OAuthAuthorize() {
</defs>
</svg>
</div>
<div className="mt-3 text-text-tertiary system-xs-regular">{t('tips.common', { ns: 'oauth' })}</div>
<div className="mt-3 system-xs-regular text-text-tertiary">{t('tips.common', { ns: 'oauth' })}</div>
</div>
)
}

View File

@@ -1,8 +1,8 @@
'use client'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { Button } from '@/app/components/base/ui/button'
import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter, useSearchParams } from '@/next/navigation'

View File

@@ -9,7 +9,6 @@ import {
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
import { rememberCreateAppExternalAttribution } from '@/utils/create-app-tracking'
import { sendGAEvent } from '@/utils/gtag'
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
@@ -46,8 +45,6 @@ export const AppInitializer = ({
(async () => {
const action = searchParams.get('action')
rememberCreateAppExternalAttribution({ searchParams })
if (oauthNewUser) {
let utmInfo = null
const utmInfoStr = Cookies.get('utm_info')

View File

@@ -35,8 +35,8 @@ vi.mock('@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view',
),
}))
vi.mock('@/app/components/base/button', () => ({
default: ({ children, onClick, className, size, variant }: {
vi.mock('@/app/components/base/ui/button', () => ({
Button: ({ children, onClick, className, size, variant }: {
children: React.ReactNode
onClick?: () => void
className?: string

View File

@@ -4,8 +4,8 @@ import userEvent from '@testing-library/user-event'
import * as React from 'react'
import AppOperations from '../app-operations'
vi.mock('../../../base/button', () => ({
default: ({ children, onClick, className, size, variant, id, tabIndex, ...rest }: {
vi.mock('../../../base/ui/button', () => ({
Button: ({ children, onClick, className, size, variant, id, tabIndex, ...rest }: {
'children': React.ReactNode
'onClick'?: () => void
'className'?: string

View File

@@ -13,8 +13,8 @@ import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
import Button from '@/app/components/base/button'
import ContentDialog from '@/app/components/base/content-dialog'
import { Button } from '@/app/components/base/ui/button'
import { AppModeEnum } from '@/types/app'
import AppIcon from '../../base/app-icon'
import { getAppModeLabel } from './app-mode-labels'
@@ -97,7 +97,7 @@ const AppInfoDetailPanel = ({
<ContentDialog
show={show}
onClose={onClose}
className="absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl p-0!"
className="absolute top-2 bottom-2 left-2 flex w-[420px] flex-col rounded-2xl p-0!"
>
<div className="flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4">
<div className="flex items-center gap-3 self-stretch">
@@ -109,14 +109,14 @@ const AppInfoDetailPanel = ({
imageUrl={appDetail.icon_url}
/>
<div className="flex flex-1 flex-col items-start justify-center overflow-hidden">
<div className="w-full truncate text-text-secondary system-md-semibold">{appDetail.name}</div>
<div className="text-text-tertiary system-2xs-medium-uppercase">
<div className="w-full truncate system-md-semibold text-text-secondary">{appDetail.name}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">
{getAppModeLabel(appDetail.mode, t)}
</div>
</div>
</div>
{appDetail.description && (
<div className="overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal wrap-break-word text-text-tertiary system-xs-regular">
<div className="overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto system-xs-regular wrap-break-word whitespace-normal text-text-tertiary">
{appDetail.description}
</div>
)}
@@ -140,7 +140,7 @@ const AppInfoDetailPanel = ({
onClick={switchOperation.onClick}
>
{switchOperation.icon}
<span className="text-text-tertiary system-sm-medium">{switchOperation.title}</span>
<span className="system-sm-medium text-text-tertiary">{switchOperation.title}</span>
</Button>
</div>
)}

View File

@@ -2,7 +2,7 @@ import type { JSX } from 'react'
import { RiMoreLine } from '@remixicon/react'
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
export type Operation = {
@@ -134,7 +134,7 @@ const AppOperations = ({
tabIndex={-1}
>
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
<span className="text-components-button-secondary-text system-xs-medium">
<span className="system-xs-medium text-components-button-secondary-text">
{operation.title}
</span>
</Button>
@@ -147,7 +147,7 @@ const AppOperations = ({
tabIndex={-1}
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<span className="text-components-button-secondary-text system-xs-medium">
<span className="system-xs-medium text-components-button-secondary-text">
{t('operation.more', { ns: 'common' })}
</span>
</Button>
@@ -163,7 +163,7 @@ const AppOperations = ({
onClick={operation.onClick}
>
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
<span className="text-components-button-secondary-text system-xs-medium">
<span className="system-xs-medium text-components-button-secondary-text">
{operation.title}
</span>
</Button>
@@ -182,7 +182,7 @@ const AppOperations = ({
className="gap-px"
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<span className="text-components-button-secondary-text system-xs-medium">
<span className="system-xs-medium text-components-button-secondary-text">
{t('operation.more', { ns: 'common' })}
</span>
</Button>
@@ -200,7 +200,7 @@ const AppOperations = ({
onClick={item.onClick}
>
{cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
<span className="text-text-secondary system-md-regular">{item.title}</span>
<span className="system-md-regular text-text-secondary">{item.title}</span>
</div>
))}
</div>

View File

@@ -1,8 +1,8 @@
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/app/components/base/ui/button'
import { cn } from '@/utils/classnames'
import Button from '../base/button'
import Tooltip from '../base/tooltip'
import ShortcutsName from '../workflow/shortcuts-name'
@@ -19,7 +19,7 @@ const TooltipContent = ({
return (
<div className="flex items-center gap-x-1">
<span className="px-0.5 text-text-secondary system-xs-medium">{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}</span>
<span className="px-0.5 system-xs-medium text-text-secondary">{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}</span>
<ShortcutsName keys={TOGGLE_SHORTCUT} textColor="secondary" />
</div>
)

View File

@@ -4,9 +4,9 @@ import type { AnnotationItemBasic } from '../type'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Checkbox from '@/app/components/base/checkbox'
import Drawer from '@/app/components/base/drawer-plus'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import AnnotationFull from '@/app/components/billing/annotation-full'
import { useProviderContext } from '@/context/provider-context'
@@ -92,11 +92,11 @@ const AddAnnotationModal: FC<Props> = ({
(
<div>
{isAnnotationFull && (
<div className="mb-4 mt-6 px-6">
<div className="mt-6 mb-4 px-6">
<AnnotationFull />
</div>
)}
<div className="system-sm-medium flex h-16 items-center justify-between rounded-bl-xl rounded-br-xl border-t border-divider-subtle bg-background-section-burn px-4 text-text-tertiary">
<div className="flex h-16 items-center justify-between rounded-br-xl rounded-bl-xl border-t border-divider-subtle bg-background-section-burn px-4 system-sm-medium text-text-tertiary">
<div
className="flex items-center space-x-2"
>

View File

@@ -4,8 +4,8 @@ import { RiDeleteBinLine } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { cn } from '@/utils/classnames'
@@ -100,7 +100,7 @@ const CSVUploader: FC<Props> = ({
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('batchModal.browse', { ns: 'appAnnotation' })}</span>
</div>
</div>
{dragging && <div ref={dragRef} className="absolute left-0 top-0 h-full w-full" />}
{dragging && <div ref={dragRef} className="absolute top-0 left-0 h-full w-full" />}
</div>
)}
{file && (

View File

@@ -5,8 +5,8 @@ import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import AnnotationFull from '@/app/components/billing/annotation-full'
import { useProviderContext } from '@/context/provider-context'
@@ -89,8 +89,8 @@ const BatchModal: FC<IBatchModalProps> = ({
return (
<Modal isShow={isShow} onClose={noop} className="max-w-[520px]! rounded-xl! px-8 py-6">
<div className="system-xl-medium relative pb-1 text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div>
<div className="absolute right-4 top-4 cursor-pointer p-2" onClick={onCancel}>
<div className="relative pb-1 system-xl-medium text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div>
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<CSVUploader
@@ -106,7 +106,7 @@ const BatchModal: FC<IBatchModalProps> = ({
)}
<div className="mt-[28px] flex justify-end pt-6">
<Button className="system-sm-medium mr-2 text-text-tertiary" onClick={onCancel}>
<Button className="mr-2 system-sm-medium text-text-tertiary" onClick={onCancel}>
{t('batchModal.cancel', { ns: 'appAnnotation' })}
</Button>
<Button

View File

@@ -4,9 +4,9 @@ import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
import Textarea from '@/app/components/base/textarea'
import { Button } from '@/app/components/base/ui/button'
import { cn } from '@/utils/classnames'
export enum EditItemType {
@@ -21,7 +21,7 @@ type Props = {
}
export const EditTitle: FC<{ className?: string, title: string }> = ({ className, title }) => (
<div className={cn(className, 'system-xs-medium flex h-[18px] items-center text-text-tertiary')}>
<div className={cn(className, 'flex h-[18px] items-center system-xs-medium text-text-tertiary')}>
<RiEditFill className="mr-1 h-3.5 w-3.5" />
<div>{title}</div>
<div
@@ -75,7 +75,7 @@ const EditItem: FC<Props> = ({
{avatar}
</div>
<div className="grow">
<div className="system-xs-semibold mb-1 text-text-primary">{name}</div>
<div className="mb-1 system-xs-semibold text-text-primary">{name}</div>
<div className="system-sm-regular text-text-primary">{content}</div>
{!isEdit
? (
@@ -83,13 +83,13 @@ const EditItem: FC<Props> = ({
{showNewContent && (
<div className="mt-3">
<EditTitle title={editTitle} />
<div className="system-sm-regular mt-1 text-text-primary">{newContent}</div>
<div className="mt-1 system-sm-regular text-text-primary">{newContent}</div>
</div>
)}
<div className="mt-2 flex items-center">
{!readonly && (
<div
className="system-xs-medium flex cursor-pointer items-center space-x-1 text-text-accent"
className="flex cursor-pointer items-center space-x-1 system-xs-medium text-text-accent"
onClick={() => {
setIsEdit(true)
}}
@@ -100,7 +100,7 @@ const EditItem: FC<Props> = ({
)}
{showNewContent && (
<div className="system-xs-medium ml-2 flex items-center text-text-tertiary">
<div className="ml-2 flex items-center system-xs-medium text-text-tertiary">
<div className="mr-2">·</div>
<div
className="flex cursor-pointer items-center space-x-1"

View File

@@ -16,13 +16,13 @@ import {
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
import CustomPopover from '@/app/components/base/popover'
import { Button } from '@/app/components/base/ui/button'
import { useLocale } from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
import { cn } from '@/utils/classnames'
import { downloadBlob } from '@/utils/download'
import Button from '../../../base/button'
import AddAnnotationModal from '../add-annotation-modal'
import BatchAddModal from '../batch-add-annotation-modal'
import ClearAllAnnotationsConfirmModal from '../clear-all-annotations-confirm-modal'
@@ -103,12 +103,12 @@ const HeaderOptions: FC<Props> = ({
}}
>
<FilePlus02 className="h-4 w-4 text-text-tertiary" />
<span className="system-sm-regular grow text-left text-text-secondary">{t('table.header.bulkImport', { ns: 'appAnnotation' })}</span>
<span className="grow text-left system-sm-regular text-text-secondary">{t('table.header.bulkImport', { ns: 'appAnnotation' })}</span>
</button>
<Menu as="div" className="relative h-full w-full">
<MenuButton className="mx-1 flex h-9 w-[calc(100%-8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50">
<FileDownload02 className="h-4 w-4 text-text-tertiary" />
<span className="system-sm-regular grow text-left text-text-secondary">{t('table.header.bulkExport', { ns: 'appAnnotation' })}</span>
<span className="grow text-left system-sm-regular text-text-secondary">{t('table.header.bulkExport', { ns: 'appAnnotation' })}</span>
<ChevronRight className="h-[14px] w-[14px] shrink-0 text-text-tertiary" />
</MenuButton>
<Transition
@@ -122,7 +122,7 @@ const HeaderOptions: FC<Props> = ({
>
<MenuItems
className={cn(
'absolute left-1 top-px z-10 min-w-[100px] origin-top-right -translate-x-full rounded-xl border-[0.5px] border-components-panel-on-panel-item-bg bg-components-panel-bg py-1 shadow-xs',
'absolute top-px left-1 z-10 min-w-[100px] origin-top-right -translate-x-full rounded-xl border-[0.5px] border-components-panel-on-panel-item-bg bg-components-panel-bg py-1 shadow-xs',
)}
>
<CSVDownloader
@@ -135,11 +135,11 @@ const HeaderOptions: FC<Props> = ({
]}
>
<button type="button" disabled={annotationUnavailable} className="mx-1 flex h-9 w-[calc(100%-8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50">
<span className="system-sm-regular grow text-left text-text-secondary">CSV</span>
<span className="grow text-left system-sm-regular text-text-secondary">CSV</span>
</button>
</CSVDownloader>
<button type="button" disabled={annotationUnavailable} className={cn('mx-1 flex h-9 w-[calc(100%-8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 hover:bg-components-panel-on-panel-item-bg-hover disabled:opacity-50', 'border-0!')} onClick={JSONLOutput}>
<span className="system-sm-regular grow text-left text-text-secondary">JSONL</span>
<span className="grow text-left system-sm-regular text-text-secondary">JSONL</span>
</button>
</MenuItems>
</Transition>
@@ -150,7 +150,7 @@ const HeaderOptions: FC<Props> = ({
className="mx-1 flex h-9 w-[calc(100%-8px)] cursor-pointer items-center space-x-2 rounded-lg px-3 py-2 text-red-600 hover:bg-red-50 disabled:opacity-50"
>
<RiDeleteBinLine className="h-4 w-4" />
<span className="system-sm-regular grow text-left">
<span className="grow text-left system-sm-regular">
{t('table.header.clearAll', { ns: 'appAnnotation' })}
</span>
</button>

View File

@@ -6,12 +6,12 @@ import { useDebounce } from 'ahooks'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/app/components/base/ui/avatar'
import { Button } from '@/app/components/base/ui/button'
import { useSelector } from '@/context/app-context'
import { SubjectType } from '@/models/access-control'
import { useSearchForWhiteListCandidates } from '@/service/access-control'
import { cn } from '@/utils/classnames'
import useAccessControlStore from '../../../../context/access-control-store'
import Button from '../../base/button'
import Checkbox from '../../base/checkbox'
import Input from '../../base/input'
import Loading from '../../base/loading'
@@ -118,7 +118,7 @@ function SelectedGroupsBreadCrumb() {
<span className={cn('system-xs-regular text-text-tertiary', selectedGroupsForBreadcrumb.length > 0 && 'cursor-pointer text-text-accent')} onClick={handleReset}>{t('accessControlDialog.operateGroupAndMember.allMembers', { ns: 'app' })}</span>
{selectedGroupsForBreadcrumb.map((group, index) => {
return (
<div key={index} className="system-xs-regular flex items-center gap-x-0.5 text-text-tertiary">
<div key={index} className="flex items-center gap-x-0.5 system-xs-regular text-text-tertiary">
<span>/</span>
<span className={index === selectedGroupsForBreadcrumb.length - 1 ? '' : 'cursor-pointer text-text-accent'} onClick={() => handleBreadCrumbClick(index)}>{group.name}</span>
</div>
@@ -161,7 +161,7 @@ function GroupItem({ group }: GroupItemProps) {
<RiOrganizationChart className="h-[14px] w-[14px] text-components-avatar-shape-fill-stop-0" />
</div>
</div>
<p className="system-sm-medium mr-1 text-text-secondary">{group.name}</p>
<p className="mr-1 system-sm-medium text-text-secondary">{group.name}</p>
<p className="system-xs-regular text-text-tertiary">{group.groupSize}</p>
</div>
<Button
@@ -206,7 +206,7 @@ function MemberItem({ member }: MemberItemProps) {
<Avatar size="xxs" avatar={null} name={member.name} />
</div>
</div>
<p className="system-sm-medium mr-1 text-text-secondary">{member.name}</p>
<p className="mr-1 system-sm-medium text-text-secondary">{member.name}</p>
{currentUser.email === member.email && (
<p className="system-xs-regular text-text-tertiary">
(

View File

@@ -5,12 +5,12 @@ import { Description as DialogDescription, DialogTitle } from '@headlessui/react
import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
import { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode, SubjectType } from '@/models/access-control'
import { useUpdateAccessMode } from '@/service/access-control'
import useAccessControlStore from '../../../../context/access-control-store'
import Button from '../../base/button'
import AccessControlDialog from './access-control-dialog'
import AccessControlItem from './access-control-item'
import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members'
@@ -67,9 +67,9 @@ export default function AccessControl(props: AccessControlProps) {
return (
<AccessControlDialog show onClose={onClose}>
<div className="flex flex-col gap-y-3">
<div className="pb-3 pl-6 pr-14 pt-6">
<div className="pt-6 pr-14 pb-3 pl-6">
<DialogTitle className="title-2xl-semi-bold text-text-primary">{t('accessControlDialog.title', { ns: 'app' })}</DialogTitle>
<DialogDescription className="system-xs-regular mt-1 text-text-tertiary">{t('accessControlDialog.description', { ns: 'app' })}</DialogDescription>
<DialogDescription className="mt-1 system-xs-regular text-text-tertiary">{t('accessControlDialog.description', { ns: 'app' })}</DialogDescription>
</div>
<div className="flex flex-col gap-y-1 px-6 pb-3">
<div className="leading-6">

View File

@@ -13,12 +13,12 @@ import { useTranslation } from 'react-i18next'
import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store'
import { trackEvent } from '@/app/components/base/amplitude'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Button } from '@/app/components/base/ui/button'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
@@ -224,7 +224,7 @@ const AppPublisher = ({
<PortalToFollowElemTrigger onClick={handleTrigger}>
<Button
variant="primary"
className="py-2 pl-3 pr-2"
className="py-2 pr-2 pl-3"
disabled={disabled}
>
{t('common.publish', { ns: 'workflow' })}

View File

@@ -4,12 +4,12 @@ import type { Model, ModelItem } from '@/app/components/header/account-setting/m
import { RiArrowDownSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Button } from '@/app/components/base/ui/button'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useProviderContext } from '@/context/provider-context'
import ModelIcon from '../../header/account-setting/model-provider-page/model-icon'

View File

@@ -3,10 +3,10 @@ import type { ModelAndParameter } from '../configuration/debug/types'
import type { AppPublisherProps } from './index'
import type { PublishWorkflowParams } from '@/types/workflow'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
import Loading from '@/app/components/base/loading'
import { Button } from '@/app/components/base/ui/button'
import {
Tooltip,
TooltipContent,

View File

@@ -5,8 +5,8 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import Button from '../../base/button'
import Input from '../../base/input'
import Textarea from '../../base/textarea'
@@ -67,17 +67,17 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
return (
<Modal className="p-0" isShow={isOpen} onClose={onClose}>
<div className="relative w-full p-6 pb-4 pr-14">
<div className="relative w-full p-6 pr-14 pb-4">
<div className="title-2xl-semi-bold text-text-primary first-letter:capitalize">
{versionInfo?.marked_name ? t('versionHistory.editVersionInfo', { ns: 'workflow' }) : t('versionHistory.nameThisVersion', { ns: 'workflow' })}
</div>
<div className="absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={onClose}>
<div className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={onClose}>
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
</div>
</div>
<div className="flex flex-col gap-y-4 px-6 py-3">
<div className="flex flex-col gap-y-1">
<div className="system-sm-semibold flex h-6 items-center text-text-secondary">
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
{t('versionHistory.editField.title', { ns: 'workflow' })}
</div>
<Input
@@ -88,7 +88,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
/>
</div>
<div className="flex flex-col gap-y-1">
<div className="system-sm-semibold flex h-6 items-center text-text-secondary">
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
{t('versionHistory.editField.releaseNotes', { ns: 'workflow' })}
</div>
<Textarea

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import WarningMask from '.'
type IFormattingChangedProps = {

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import WarningMask from '.'
type IFormattingChangedProps = {

View File

@@ -13,13 +13,13 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { ADD_EXTERNAL_DATA_TOOL } from '@/app/components/app/configuration/config-var'
import Button from '@/app/components/base/button'
import {
Copy,
CopyCheck,
} from '@/app/components/base/icons/src/vender/line/files'
import PromptEditor from '@/app/components/base/prompt-editor'
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import {
Tooltip,
@@ -148,14 +148,14 @@ const AdvancedPromptInput: FC<Props> = ({
const [editorHeight, setEditorHeight] = React.useState(isChatMode ? 200 : 508)
const contextMissing = (
<div
className="flex h-11 items-center justify-between rounded-tl-xl rounded-tr-xl pb-1 pl-4 pr-3 pt-2"
className="flex h-11 items-center justify-between rounded-tl-xl rounded-tr-xl pt-2 pr-3 pb-1 pl-4"
style={{
background: 'linear-gradient(180deg, #FEF0C7 0%, rgba(254, 240, 199, 0) 100%)',
}}
>
<div className="flex items-center pr-2">
<RiErrorWarningFill className="mr-1 h-4 w-4 text-[#F79009]" />
<div className="text-[13px] font-medium leading-[18px] text-[#DC6803]">{t('promptMode.contextMissing', { ns: 'appDebug' })}</div>
<div className="text-[13px] leading-[18px] font-medium text-[#DC6803]">{t('promptMode.contextMissing', { ns: 'appDebug' })}</div>
</div>
<Button
size="small"
@@ -172,7 +172,7 @@ const AdvancedPromptInput: FC<Props> = ({
{isContextMissing
? contextMissing
: (
<div className={cn(s.boxHeader, 'flex h-11 items-center justify-between rounded-tl-xl rounded-tr-xl bg-background-default pb-1 pl-4 pr-3 pt-2 hover:shadow-xs')}>
<div className={cn(s.boxHeader, 'flex h-11 items-center justify-between rounded-tl-xl rounded-tr-xl bg-background-default pt-2 pr-3 pb-1 pl-4 hover:shadow-xs')}>
{isChatMode
? (
<MessageTypeSelector value={type} onChange={onTypeChange} />
@@ -180,13 +180,13 @@ const AdvancedPromptInput: FC<Props> = ({
: (
<div className="flex items-center space-x-1">
<div className="text-sm font-semibold uppercase text-indigo-800">
<div className="text-sm font-semibold text-indigo-800 uppercase">
{t('pageTitle.line1', { ns: 'appDebug' })}
</div>
<Tooltip>
<TooltipTrigger
render={(
<span className="i-ri-question-line ml-1 h-4 w-4 shrink-0 text-text-quaternary" />
<span className="ml-1 i-ri-question-line h-4 w-4 shrink-0 text-text-quaternary" />
)}
/>
<TooltipContent>

View File

@@ -3,7 +3,7 @@ import type { FC } from 'react'
import * as React from 'react'
import { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import VarHighlight from '../../base/var-highlight'
type IConfirmAddVarProps = {
@@ -35,7 +35,7 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
// }, mainContentRef)
return (
<div
className="absolute inset-0 flex items-center justify-center rounded-xl"
className="absolute inset-0 flex items-center justify-center rounded-xl"
style={{
backgroundColor: 'rgba(35, 56, 118, 0.2)',
}}

View File

@@ -4,8 +4,8 @@ import type { ConversationHistoriesRole } from '@/models/debug'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
type Props = {
isShow: boolean
@@ -30,7 +30,7 @@ const EditModal: FC<Props> = ({
isShow={isShow}
onClose={onClose}
>
<div className="mt-6 text-sm font-medium leading-[21px] text-text-primary">{t('feature.conversationHistory.editModal.userPrefix', { ns: 'appDebug' })}</div>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.userPrefix', { ns: 'appDebug' })}</div>
<input
className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10"
value={tempData.user_prefix}
@@ -40,7 +40,7 @@ const EditModal: FC<Props> = ({
})}
/>
<div className="mt-6 text-sm font-medium leading-[21px] text-text-primary">{t('feature.conversationHistory.editModal.assistantPrefix', { ns: 'appDebug' })}</div>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.assistantPrefix', { ns: 'appDebug' })}</div>
<input
className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10"
value={tempData.assistant_prefix}

View File

@@ -10,7 +10,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AdvancedMessageInput from '@/app/components/app/configuration/config-prompt/advanced-prompt-input'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config'
import ConfigContext from '@/context/debug-configuration'
import { PromptRole } from '@/models/debug'

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
type IModalFootProps = {
onConfirm: () => void

View File

@@ -3,12 +3,12 @@ import type { FC } from 'react'
import { RiSettings2Line } from '@remixicon/react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Button } from '@/app/components/base/ui/button'
import { cn } from '@/utils/classnames'
import ParamConfigContent from './param-config-content'

View File

@@ -5,7 +5,7 @@ import { RiSettings2Line } from '@remixicon/react'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import AgentSetting from './agent/agent-setting'
type Props = {

View File

@@ -6,9 +6,9 @@ import { useClickAway } from 'ahooks'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
import { Unblur } from '@/app/components/base/icons/src/vender/solid/education'
import { Button } from '@/app/components/base/ui/button'
import { Slider } from '@/app/components/base/ui/slider'
import { DEFAULT_AGENT_PROMPT, MAX_ITERATIONS_NUM } from '@/config'
import ItemPanel from './item-panel'
@@ -59,7 +59,7 @@ const AgentSetting: FC<Props> = ({
ref={ref}
className="flex h-full w-[640px] flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
>
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-regular pl-6 pr-5">
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-regular pr-5 pl-6">
<div className="flex flex-col text-base font-semibold text-text-primary">
<div className="leading-6">{t('agent.setting.name', { ns: 'appDebug' })}</div>
</div>
@@ -74,7 +74,7 @@ const AgentSetting: FC<Props> = ({
</div>
{/* Body */}
<div
className="grow overflow-y-auto border-b p-6 pb-[68px] pt-5"
className="grow overflow-y-auto border-b p-6 pt-5 pb-[68px]"
style={{
borderBottom: 'rgba(0, 0, 0, 0.05)',
}}
@@ -88,7 +88,7 @@ const AgentSetting: FC<Props> = ({
name={t('agent.agentMode', { ns: 'appDebug' })}
description={t('agent.agentModeDes', { ns: 'appDebug' })}
>
<div className="text-[13px] font-medium leading-[18px] text-text-primary">{isFunctionCall ? t('agent.agentModeType.functionCall', { ns: 'appDebug' }) : t('agent.agentModeType.ReACT', { ns: 'appDebug' })}</div>
<div className="text-[13px] leading-[18px] font-medium text-text-primary">{isFunctionCall ? t('agent.agentModeType.functionCall', { ns: 'appDebug' }) : t('agent.agentModeType.ReACT', { ns: 'appDebug' })}</div>
</ItemPanel>
<ItemPanel
@@ -119,7 +119,7 @@ const AgentSetting: FC<Props> = ({
min={maxIterationsMin}
max={MAX_ITERATIONS_NUM}
step={1}
className="block h-7 w-11 rounded-lg border-0 bg-components-input-bg-normal px-1.5 pl-1 leading-7 text-text-primary placeholder:text-text-tertiary focus:ring-1 focus:ring-inset focus:ring-primary-600"
className="block h-7 w-11 rounded-lg border-0 bg-components-input-bg-normal px-1.5 pl-1 leading-7 text-text-primary placeholder:text-text-tertiary focus:ring-1 focus:ring-primary-600 focus:ring-inset"
value={tempPayload.max_iteration}
onChange={(e) => {
let value = Number.parseInt(e.target.value, 10)
@@ -139,12 +139,12 @@ const AgentSetting: FC<Props> = ({
{!isFunctionCall && (
<div className="rounded-xl bg-background-section-burn py-2 shadow-xs">
<div className="flex h-8 items-center px-4 text-sm font-semibold leading-6 text-text-secondary">{t('builtInPromptTitle', { ns: 'tools' })}</div>
<div className="h-[396px] overflow-y-auto whitespace-pre-line px-4 text-sm font-normal leading-5 text-text-secondary">
<div className="flex h-8 items-center px-4 text-sm leading-6 font-semibold text-text-secondary">{t('builtInPromptTitle', { ns: 'tools' })}</div>
<div className="h-[396px] overflow-y-auto px-4 text-sm leading-5 font-normal whitespace-pre-line text-text-secondary">
{isChatModel ? DEFAULT_AGENT_PROMPT.chat : DEFAULT_AGENT_PROMPT.completion}
</div>
<div className="px-4">
<div className="inline-flex h-5 items-center rounded-md bg-components-input-bg-normal px-1 text-xs font-medium leading-[18px] text-text-tertiary">{(isChatModel ? DEFAULT_AGENT_PROMPT.chat : DEFAULT_AGENT_PROMPT.completion).length}</div>
<div className="inline-flex h-5 items-center rounded-md bg-components-input-bg-normal px-1 text-xs leading-[18px] font-medium text-text-tertiary">{(isChatModel ? DEFAULT_AGENT_PROMPT.chat : DEFAULT_AGENT_PROMPT.completion).length}</div>
</div>
</div>
)}

View File

@@ -18,11 +18,11 @@ import { useContext } from 'use-context-selector'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import { Button } from '@/app/components/base/ui/button'
import Indicator from '@/app/components/header/indicator'
import { CollectionType } from '@/app/components/tools/types'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
@@ -165,7 +165,7 @@ const AgentTools: FC = () => {
)}
headerRight={(
<div className="flex items-center">
<div className="text-xs font-normal leading-[18px] text-text-tertiary">
<div className="text-xs leading-[18px] font-normal text-text-tertiary">
{tools.filter(item => !!item.enabled).length}
/
{tools.length}
@@ -174,7 +174,7 @@ const AgentTools: FC = () => {
</div>
{tools.length < MAX_TOOLS_NUM && !readonly && (
<>
<div className="ml-3 mr-1 h-3.5 w-px bg-divider-regular"></div>
<div className="mr-1 ml-3 h-3.5 w-px bg-divider-regular"></div>
<ToolPicker
trigger={<OperationBtn type="add" />}
isShow={isShowChooseTool}
@@ -209,11 +209,11 @@ const AgentTools: FC = () => {
)}
<div
className={cn(
'system-xs-regular ml-1.5 flex w-0 grow items-center truncate',
'ml-1.5 flex w-0 grow items-center truncate system-xs-regular',
(item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '',
)}
>
<span className="system-xs-medium pr-1.5 text-text-secondary">{getProviderShowName(item)}</span>
<span className="pr-1.5 system-xs-medium text-text-secondary">{getProviderShowName(item)}</span>
<span className="text-text-tertiary">{item.tool_label}</span>
{!item.isDeleted && !readonly && (
<Tooltip
@@ -268,7 +268,7 @@ const AgentTools: FC = () => {
needsDelay={false}
>
<div
className="cursor-pointer rounded-md p-1 hover:bg-black/5"
className="cursor-pointer rounded-md p-1 hover:bg-black/5"
onClick={() => {
setCurrentTool(item)
setIsShowSettingTool(true)

View File

@@ -10,10 +10,10 @@ import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer'
import Loading from '@/app/components/base/loading'
import TabSlider from '@/app/components/base/tab-slider-plain'
import { Button } from '@/app/components/base/ui/button'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import Icon from '@/app/components/plugins/card/base/card-icon'
import Description from '@/app/components/plugins/card/base/description'
@@ -140,7 +140,7 @@ const SettingBuiltInTool: FC<Props> = ({
)}
</div>
{item.human_description && (
<div className="system-xs-regular mt-0.5 text-text-tertiary">
<div className="mt-0.5 system-xs-regular text-text-tertiary">
{item.human_description?.[language]}
</div>
)}
@@ -171,7 +171,7 @@ const SettingBuiltInTool: FC<Props> = ({
footer={null}
mask={false}
positionCenter={false}
panelClassName={cn('mb-2 mr-2 mt-[64px] w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
>
<>
{isLoading && <Loading type="app" />}
@@ -179,14 +179,14 @@ const SettingBuiltInTool: FC<Props> = ({
<>
{/* header */}
<div className="relative border-b border-divider-subtle p-4 pb-3">
<div className="absolute right-3 top-3">
<div className="absolute top-3 right-3">
<ActionButton onClick={onHide}>
<RiCloseLine className="h-4 w-4" />
</ActionButton>
</div>
{showBackButton && (
<div
className="system-xs-semibold-uppercase mb-2 flex cursor-pointer items-center gap-1 text-text-accent-secondary"
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
onClick={onHide}
>
<RiArrowLeftLine className="h-4 w-4" />
@@ -201,9 +201,9 @@ const SettingBuiltInTool: FC<Props> = ({
packageName={collection.name.split('/').pop() || ''}
/>
</div>
<div className="system-md-semibold mt-1 text-text-primary">{currTool?.label[language]}</div>
<div className="mt-1 system-md-semibold text-text-primary">{currTool?.label[language]}</div>
{!!currTool?.description[language] && (
<Description className="mb-2 mt-3 h-auto" text={currTool.description[language]} descriptionLineRows={2}></Description>
<Description className="mt-3 mb-2 h-auto" text={currTool.description[language]} descriptionLineRows={2}></Description>
)}
{
collection.allow_delete && collection.type === CollectionType.builtIn && (
@@ -240,13 +240,13 @@ const SettingBuiltInTool: FC<Props> = ({
/>
)
: (
<div className="system-sm-semibold-uppercase p-4 pb-1 text-text-primary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
)}
<div className="h-0 grow overflow-y-auto px-4">
{isInfoActive ? infoUI : settingUI}
{!readonly && !isInfoActive && (
<div className="flex shrink-0 justify-end space-x-2 rounded-b-[10px] bg-components-panel-bg py-2">
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium " onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium" variant="primary" disabled={!isValid} onClick={() => onSave?.(tempSetting)}>{t('operation.save', { ns: 'common' })}</Button>
</div>
)}

View File

@@ -5,7 +5,7 @@ import {
} from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
type IAutomaticBtnProps = {
onClick: () => void

View File

@@ -19,11 +19,11 @@ import { useBoolean, useSessionStorageState } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@@ -62,7 +62,7 @@ const TryLabel: FC<{
}> = ({ Icon, text, onClick }) => {
return (
<div
className="mr-1 mt-2 flex h-7 shrink-0 cursor-pointer items-center rounded-lg bg-components-button-secondary-bg px-2"
className="mt-2 mr-1 flex h-7 shrink-0 cursor-pointer items-center rounded-lg bg-components-button-secondary-bg px-2"
onClick={onClick}
>
<Icon className="h-4 w-4 text-text-tertiary"></Icon>
@@ -283,7 +283,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
<div className="flex h-[680px] flex-wrap">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5">
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('generate.title', { ns: 'appDebug' })}</div>
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('generate.title', { ns: 'appDebug' })}</div>
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('generate.description', { ns: 'appDebug' })}</div>
</div>
<div>
@@ -301,7 +301,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
{isBasicMode && (
<div className="mt-4">
<div className="flex items-center">
<div className="mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary">{t('generate.tryIt', { ns: 'appDebug' })}</div>
<div className="mr-3 shrink-0 text-xs leading-[18px] font-semibold text-text-tertiary uppercase">{t('generate.tryIt', { ns: 'appDebug' })}</div>
<div
className="h-px grow"
style={{
@@ -326,7 +326,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
{/* inputs */}
<div className="mt-4">
<div>
<div className="system-sm-semibold-uppercase mb-1.5 text-text-secondary">{t('generate.instruction', { ns: 'appDebug' })}</div>
<div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('generate.instruction', { ns: 'appDebug' })}</div>
{isBasicMode
? (
<InstructionEditorInBasic

View File

@@ -5,7 +5,7 @@ import { RiClipboardLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
import PromptRes from './prompt-res'
@@ -42,7 +42,7 @@ const Result: FC<Props> = ({
<div className="flex h-full flex-col">
<div className="mb-3 flex shrink-0 items-center justify-between">
<div>
<div className="shrink-0 text-base font-semibold leading-[160%] text-text-secondary">{t('generate.resTitle', { ns: 'appDebug' })}</div>
<div className="shrink-0 text-base leading-[160%] font-semibold text-text-secondary">{t('generate.resTitle', { ns: 'appDebug' })}</div>
<VersionSelector
versionLen={versions.length}
value={currentVersionIndex}

View File

@@ -10,11 +10,11 @@ import {
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
@@ -202,7 +202,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
<div className="relative flex h-[680px] flex-wrap">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5">
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('codegen.title', { ns: 'appDebug' })}</div>
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('codegen.title', { ns: 'appDebug' })}</div>
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('codegen.description', { ns: 'appDebug' })}</div>
</div>
<div className="mb-4">
@@ -219,7 +219,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
</div>
<div>
<div className="text-[0px]">
<div className="mb-1.5 text-text-secondary system-sm-semibold-uppercase">{t('codegen.instruction', { ns: 'appDebug' })}</div>
<div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('codegen.instruction', { ns: 'appDebug' })}</div>
<InstructionEditor
editorKey={editorKey}
value={instruction}

View File

@@ -10,7 +10,6 @@ import EditHistoryModal from '@/app/components/app/configuration/config-prompt/c
import AgentSettingButton from '@/app/components/app/configuration/config/agent-setting-button'
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
import Debug from '@/app/components/app/configuration/debug'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Drawer from '@/app/components/base/drawer'
import { FeaturesProvider } from '@/app/components/base/features'
@@ -25,6 +24,7 @@ import {
AlertDialogDescription,
AlertDialogTitle,
} from '@/app/components/base/ui/alert-dialog'
import { Button } from '@/app/components/base/ui/button'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import PluginDependency from '@/app/components/workflow/plugin-dependency'
import ConfigContext from '@/context/debug-configuration'

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import s from './style.module.css'
type IContrlBtnGroupProps = {
@@ -14,7 +14,7 @@ const ContrlBtnGroup: FC<IContrlBtnGroupProps> = ({ onSave, onReset }) => {
const { t } = useTranslation()
return (
<div className="fixed bottom-0 left-[224px] h-[64px] w-[519px]">
<div className={`${s.ctrlBtn} flex h-full items-center gap-2 bg-white pl-4`}>
<div className={`${s.ctrlBtn} flex h-full items-center gap-2 bg-white pl-4`}>
<Button variant="primary" onClick={onSave} data-testid="apply-btn">{t('operation.applyConfig', { ns: 'appDebug' })}</Button>
<Button onClick={onReset} data-testid="reset-btn">{t('operation.resetConfig', { ns: 'appDebug' })}</Button>
</div>

View File

@@ -5,8 +5,8 @@ import { RiEqualizer2Line } from '@remixicon/react'
import { memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'

View File

@@ -7,9 +7,9 @@ import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { Button } from '@/app/components/base/ui/button'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import FeatureIcon from '@/app/components/header/account-setting/model-provider-page/model-selector/feature-icon'
import { useKnowledge } from '@/hooks/use-knowledge'
@@ -134,7 +134,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
</div>
<div className={cn('max-w-[200px] truncate text-[13px] font-medium text-text-secondary', !item.embedding_available && 'max-w-[120px]! opacity-30')}>{item.name}</div>
{!item.embedding_available && (
<span className="ml-1 shrink-0 rounded-md border border-divider-deep px-1 text-xs font-normal leading-[18px] text-text-tertiary">{t('unavailable', { ns: 'dataset' })}</span>
<span className="ml-1 shrink-0 rounded-md border border-divider-deep px-1 text-xs leading-[18px] font-normal text-text-tertiary">{t('unavailable', { ns: 'dataset' })}</span>
)}
</div>
{item.is_multimodal && (

View File

@@ -6,9 +6,9 @@ import { RiCloseLine } from '@remixicon/react'
import { isEqual } from 'es-toolkit/predicate'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import { IndexingType } from '@/app/components/datasets/create/step-two'
@@ -192,7 +192,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
}}
ref={ref}
>
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-regular pl-6 pr-5">
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-regular pr-5 pl-6">
<div className="flex flex-col text-base font-semibold text-text-primary">
<div className="leading-6">{t('title', { ns: 'datasetSettings' })}</div>
</div>
@@ -206,10 +206,10 @@ const SettingsModal: FC<SettingsModalProps> = ({
</div>
</div>
{/* Body */}
<div className="overflow-y-auto border-b border-divider-regular p-6 pb-[68px] pt-5">
<div className="overflow-y-auto border-b border-divider-regular p-6 pt-5 pb-[68px]">
<div className={cn(rowClass, 'items-center')}>
<div className={labelClass}>
<div className="text-text-secondary system-sm-semibold">{t('form.name', { ns: 'datasetSettings' })}</div>
<div className="system-sm-semibold text-text-secondary">{t('form.name', { ns: 'datasetSettings' })}</div>
</div>
<Input
value={localeCurrentDataset.name}
@@ -220,7 +220,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
</div>
<div className={cn(rowClass)}>
<div className={labelClass}>
<div className="text-text-secondary system-sm-semibold">{t('form.desc', { ns: 'datasetSettings' })}</div>
<div className="system-sm-semibold text-text-secondary">{t('form.desc', { ns: 'datasetSettings' })}</div>
</div>
<div className="w-full">
<Textarea
@@ -233,7 +233,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
</div>
<div className={rowClass}>
<div className={labelClass}>
<div className="text-text-secondary system-sm-semibold">{t('form.permissions', { ns: 'datasetSettings' })}</div>
<div className="system-sm-semibold text-text-secondary">{t('form.permissions', { ns: 'datasetSettings' })}</div>
</div>
<div className="w-full">
<PermissionSelector
@@ -249,7 +249,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
{!!(currentDataset && currentDataset.indexing_technique) && (
<div className={cn(rowClass)}>
<div className={labelClass}>
<div className="text-text-secondary system-sm-semibold">{t('form.indexMethod', { ns: 'datasetSettings' })}</div>
<div className="system-sm-semibold text-text-secondary">{t('form.indexMethod', { ns: 'datasetSettings' })}</div>
</div>
<div className="grow">
<IndexMethod
@@ -266,7 +266,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
{indexMethod === IndexingType.QUALIFIED && (
<div className={cn(rowClass)}>
<div className={labelClass}>
<div className="text-text-secondary system-sm-semibold">{t('form.embeddingModel', { ns: 'datasetSettings' })}</div>
<div className="system-sm-semibold text-text-secondary">{t('form.embeddingModel', { ns: 'datasetSettings' })}</div>
</div>
<div className="w-full">
<div className="h-8 w-full rounded-lg bg-components-input-bg-normal opacity-60">

View File

@@ -25,10 +25,10 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import TextGeneration from '@/app/components/app/text-generate/item'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import AgentLogModal from '@/app/components/base/agent-log-modal'
import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import {
Tooltip,
@@ -394,8 +394,8 @@ const Debug: FC<IDebug> = ({
return (
<>
<div className="shrink-0">
<div className="flex items-center justify-between px-4 pb-2 pt-3">
<div className="text-text-primary system-xl-semibold">{t('inputs.title', { ns: 'appDebug' })}</div>
<div className="flex items-center justify-between px-4 pt-3 pb-2">
<div className="system-xl-semibold text-text-primary">{t('inputs.title', { ns: 'appDebug' })}</div>
<div className="flex items-center">
{
debugWithMultipleModel
@@ -432,14 +432,14 @@ const Debug: FC<IDebug> = ({
{
varList.length > 0 && (
<div className="relative ml-1 mr-2">
<div className="relative mr-2 ml-1">
<Tooltip>
<TooltipTrigger render={<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}><RiEqualizer2Line className="h-4 w-4" /></ActionButton>} />
<TooltipContent>
{t('panel.userInputField', { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
{expanded && <div className="absolute bottom-[-14px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg" />}
{expanded && <div className="absolute right-[5px] bottom-[-14px] z-10 h-3 w-3 rotate-45 border-t-[0.5px] border-l-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg" />}
</div>
)
}
@@ -517,8 +517,8 @@ const Debug: FC<IDebug> = ({
</div>
</div>
<div className="flex flex-col gap-1">
<div className="text-text-secondary system-md-semibold">{t('noModelSelected', { ns: 'appDebug' })}</div>
<div className="text-text-tertiary system-xs-regular">{t('noModelSelectedTip', { ns: 'appDebug' })}</div>
<div className="system-md-semibold text-text-secondary">{t('noModelSelected', { ns: 'appDebug' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('noModelSelectedTip', { ns: 'appDebug' })}</div>
</div>
</div>
</div>
@@ -557,7 +557,7 @@ const Debug: FC<IDebug> = ({
{!completionRes && !isResponding && (
<div className="flex grow flex-col items-center justify-center gap-2">
<RiSparklingFill className="h-12 w-12 text-text-empty-state-icon" />
<div className="text-text-quaternary system-sm-regular">{t('noResult', { ns: 'appDebug' })}</div>
<div className="system-sm-regular text-text-quaternary">{t('noResult', { ns: 'appDebug' })}</div>
</div>
)}
</>

View File

@@ -12,13 +12,13 @@ import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useStore as useAppStore } from '@/app/components/app/store'
import Button from '@/app/components/base/button'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import Input from '@/app/components/base/input'
import Select from '@/app/components/base/select'
import Textarea from '@/app/components/base/textarea'
import Tooltip from '@/app/components/base/tooltip'
import { Button } from '@/app/components/base/ui/button'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import ConfigContext from '@/context/debug-configuration'
import { AppModeEnum, ModelModeType } from '@/types/app'
@@ -117,11 +117,11 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
{!userInputFieldCollapse && <RiArrowDownSLine className="h-4 w-4 text-text-secondary" />}
</div>
{!userInputFieldCollapse && (
<div className="system-xs-regular mt-1 text-text-tertiary">{t('inputs.completionVarTip', { ns: 'appDebug' })}</div>
<div className="mt-1 system-xs-regular text-text-tertiary">{t('inputs.completionVarTip', { ns: 'appDebug' })}</div>
)}
</div>
{!userInputFieldCollapse && promptVariables.length > 0 && (
<div className="px-4 pb-4 pt-3">
<div className="px-4 pt-3 pb-4">
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
<div
key={key}
@@ -129,7 +129,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
>
<div>
{type !== 'checkbox' && (
<div className="system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary">
<div className="mb-1 flex h-6 items-center gap-1 system-sm-semibold text-text-secondary">
<div className="truncate">{name || key}</div>
{!required && <span className="system-xs-regular text-text-tertiary">{t('panel.optional', { ns: 'workflow' })}</span>}
</div>

View File

@@ -6,10 +6,10 @@ import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import EmojiPicker from '@/app/components/base/emoji-picker'
import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
import { toast } from '@/app/components/base/ui/toast'
@@ -116,7 +116,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
{`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`}
</div>
<div className="py-2">
<div className="text-sm font-medium leading-9 text-text-primary">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('apiBasedExtension.type', { ns: 'common' })}
</div>
<Select
@@ -136,7 +136,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
</Select>
</div>
<div className="py-2">
<div className="text-sm font-medium leading-9 text-text-primary">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('feature.tools.modal.name.title', { ns: 'appDebug' })}
</div>
<div className="flex items-center">
@@ -156,7 +156,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
</div>
</div>
<div className="py-2">
<div className="text-sm font-medium leading-9 text-text-primary">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('feature.tools.modal.variableName.title', { ns: 'appDebug' })}
</div>
<input

View File

@@ -6,7 +6,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import { Button } from '@/app/components/base/ui/button'
import AppListContext from '@/context/app-list-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'
@@ -34,7 +34,7 @@ const AppCard = ({
}
}, [setShowTryAppPanel, app.category])
return (
<div className={cn('group relative flex h-[132px] cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs hover:shadow-lg')}>
<div className={cn('group relative flex h-[132px] cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs hover:shadow-lg')}>
<div className="flex shrink-0 grow-0 items-center gap-3 pb-2">
<div className="relative shrink-0">
<AppIcon
@@ -57,13 +57,13 @@ const AppCard = ({
<AppTypeLabel className="system-2xs-medium-uppercase text-text-tertiary" type={app.app.mode} />
</div>
</div>
<div className="system-xs-regular py-1 text-text-tertiary">
<div className="py-1 system-xs-regular text-text-tertiary">
<div className="line-clamp-3">
{app.description}
</div>
</div>
{(canCreate || isTrialApp) && (
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-linear-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
<div className={cn('absolute right-0 bottom-0 left-0 hidden bg-linear-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
<div className={cn('grid h-8 w-full grid-cols-1 items-center space-x-2', canCreate && 'grid-cols-2')}>
{canCreate && (
<Button variant="primary" onClick={() => onCreate()}>

View File

@@ -4,6 +4,7 @@ import { AppModeEnum } from '@/types/app'
import Apps from '../index'
const mockUseExploreAppList = vi.fn()
const mockTrackEvent = vi.fn()
const mockImportDSL = vi.fn()
const mockFetchAppDetail = vi.fn()
const mockHandleCheckPluginDependencies = vi.fn()
@@ -11,7 +12,6 @@ const mockGetRedirection = vi.fn()
const mockPush = vi.fn()
const mockToastSuccess = vi.fn()
const mockToastError = vi.fn()
const mockTrackCreateApp = vi.fn()
let latestDebounceFn = () => {}
vi.mock('ahooks', () => ({
@@ -92,8 +92,8 @@ vi.mock('@/app/components/base/ui/toast', () => ({
error: (...args: unknown[]) => mockToastError(...args),
},
}))
vi.mock('@/utils/create-app-tracking', () => ({
trackCreateApp: (...args: unknown[]) => mockTrackCreateApp(...args),
vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
}))
vi.mock('@/service/apps', () => ({
importDSL: (...args: unknown[]) => mockImportDSL(...args),
@@ -246,9 +246,10 @@ describe('Apps', () => {
}))
})
expect(mockTrackCreateApp).toHaveBeenCalledWith({
appMode: AppModeEnum.CHAT,
})
expect(mockTrackEvent).toHaveBeenCalledWith('create_app_with_template', expect.objectContaining({
template_id: 'Alpha',
template_name: 'Alpha',
}))
expect(mockToastSuccess).toHaveBeenCalledWith('app.newApp.appCreated')
expect(onSuccess).toHaveBeenCalled()
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('created-app-id')

View File

@@ -8,6 +8,7 @@ import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppTypeSelector from '@/app/components/app/type-selector'
import { trackEvent } from '@/app/components/base/amplitude'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import Loading from '@/app/components/base/loading'
@@ -24,7 +25,6 @@ import { useExploreAppList } from '@/service/use-explore'
import { AppModeEnum } from '@/types/app'
import { getRedirection } from '@/utils/app-redirection'
import { cn } from '@/utils/classnames'
import { trackCreateApp } from '@/utils/create-app-tracking'
import AppCard from '../app-card'
import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
@@ -127,7 +127,14 @@ const Apps = ({
icon_background,
description,
})
trackCreateApp({ appMode: mode })
// Track app creation from template
trackEvent('create_app_with_template', {
app_mode: mode,
template_id: currApp?.app.id,
template_name: currApp?.app.name,
description,
})
setIsShowCreateModal(false)
toast.success(t('newApp.appCreated', { ns: 'app' }))

View File

@@ -1,6 +1,7 @@
import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { trackEvent } from '@/app/components/base/amplitude'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
@@ -9,7 +10,6 @@ import { useRouter } from '@/next/navigation'
import { createApp } from '@/service/apps'
import { AppModeEnum } from '@/types/app'
import { getRedirection } from '@/utils/app-redirection'
import { trackCreateApp } from '@/utils/create-app-tracking'
import CreateAppModal from '../index'
const ahooksMocks = vi.hoisted(() => ({
@@ -31,8 +31,8 @@ vi.mock('ahooks', () => ({
vi.mock('@/next/navigation', () => ({
useRouter: vi.fn(),
}))
vi.mock('@/utils/create-app-tracking', () => ({
trackCreateApp: vi.fn(),
vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: vi.fn(),
}))
vi.mock('@/service/apps', () => ({
createApp: vi.fn(),
@@ -87,7 +87,7 @@ vi.mock('@/hooks/use-theme', () => ({
const mockUseRouter = vi.mocked(useRouter)
const mockPush = vi.fn()
const mockCreateApp = vi.mocked(createApp)
const mockTrackCreateApp = vi.mocked(trackCreateApp)
const mockTrackEvent = vi.mocked(trackEvent)
const mockGetRedirection = vi.mocked(getRedirection)
const mockUseProviderContext = vi.mocked(useProviderContext)
const mockUseAppContext = vi.mocked(useAppContext)
@@ -178,7 +178,10 @@ describe('CreateAppModal', () => {
mode: AppModeEnum.ADVANCED_CHAT,
}))
expect(mockTrackCreateApp).toHaveBeenCalledWith({ appMode: AppModeEnum.ADVANCED_CHAT })
expect(mockTrackEvent).toHaveBeenCalledWith('create_app', {
app_mode: AppModeEnum.ADVANCED_CHAT,
description: '',
})
expect(mockToastSuccess).toHaveBeenCalledWith('app.newApp.appCreated')
expect(onSuccess).toHaveBeenCalled()
expect(onClose).toHaveBeenCalled()

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