mirror of
https://github.com/langgenius/dify.git
synced 2026-04-14 20:42:39 +00:00
Compare commits
14 Commits
feat/creat
...
4-14-noUnc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58aaf69426 | ||
|
|
8f18ac8c1a | ||
|
|
00eeb82e85 | ||
|
|
eefd9ee6c7 | ||
|
|
aaeb806b36 | ||
|
|
25885f2fa8 | ||
|
|
2c065e8a21 | ||
|
|
648dde5e96 | ||
|
|
a3042e6332 | ||
|
|
e5fd3133f4 | ||
|
|
e1bbe57f9c | ||
|
|
d4783e8c14 | ||
|
|
736880e046 | ||
|
|
bd7a9b5fcf |
@@ -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)"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
5
api/configs/middleware/cache/redis_config.py
vendored
5
api/configs/middleware/cache/redis_config.py
vendored
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
32
api/extensions/redis_names.py
Normal file
32
api/extensions/redis_names.py
Normal 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)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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**:
|
||||
|
||||
|
||||
@@ -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
28
packages/cli/bin/dify-cli.js
Executable 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
20
packages/cli/package.json
Normal 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
54
packages/cli/src/cli.ts
Normal 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
|
||||
}
|
||||
1744
packages/cli/src/no-unchecked-indexed-access/migrate.ts
Normal file
1744
packages/cli/src/no-unchecked-indexed-access/migrate.ts
Normal file
File diff suppressed because it is too large
Load Diff
51
packages/cli/src/no-unchecked-indexed-access/normalize.ts
Normal file
51
packages/cli/src/no-unchecked-indexed-access/normalize.ts
Normal 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()
|
||||
}
|
||||
232
packages/cli/src/no-unchecked-indexed-access/run.ts
Normal file
232
packages/cli/src/no-unchecked-indexed-access/run.ts
Normal 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))
|
||||
}
|
||||
17
packages/cli/vite.config.ts
Normal file
17
packages/cli/vite.config.ts
Normal 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
19
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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]}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
(
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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' })}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)',
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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' }))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user