mirror of
https://github.com/langgenius/dify.git
synced 2026-01-20 03:59:30 +08:00
Compare commits
2 Commits
refactor/u
...
feat/sqlal
| Author | SHA1 | Date | |
|---|---|---|---|
| 225238b4b2 | |||
| c4ea3e47fd |
2
.github/workflows/autofix.yml
vendored
2
.github/workflows/autofix.yml
vendored
@ -53,6 +53,8 @@ jobs:
|
||||
# Fix forward references that were incorrectly converted (Python doesn't support "Type" | None syntax)
|
||||
find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \;
|
||||
find . -name "*.py.bak" -type f -delete
|
||||
# Rewrite SQLAlchemy with Type Annotations
|
||||
uvx --from ast-grep-cli sg scan -r dev/ast-grep/rules/remove-nullable-arg.yaml api/models -U
|
||||
|
||||
- name: mdformat
|
||||
run: |
|
||||
|
||||
@ -1601,7 +1601,7 @@ def transform_datasource_credentials():
|
||||
"integration_secret": api_key,
|
||||
}
|
||||
datasource_provider = DatasourceProvider(
|
||||
provider="jinareader",
|
||||
provider="jina",
|
||||
tenant_id=tenant_id,
|
||||
plugin_id=jina_plugin_id,
|
||||
auth_type=api_key_credential_type.value,
|
||||
|
||||
@ -22,11 +22,6 @@ class WeaviateConfig(BaseSettings):
|
||||
default=True,
|
||||
)
|
||||
|
||||
WEAVIATE_GRPC_ENDPOINT: str | None = Field(
|
||||
description="URL of the Weaviate gRPC server (e.g., 'grpc://localhost:50051' or 'grpcs://weaviate.example.com:443')",
|
||||
default=None,
|
||||
)
|
||||
|
||||
WEAVIATE_BATCH_SIZE: PositiveInt = Field(
|
||||
description="Number of objects to be processed in a single batch operation (default is 100)",
|
||||
default=100,
|
||||
|
||||
@ -102,18 +102,7 @@ class DraftWorkflowApi(Resource):
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(
|
||||
200,
|
||||
"Draft workflow synced successfully",
|
||||
api.model(
|
||||
"SyncDraftWorkflowResponse",
|
||||
{
|
||||
"result": fields.String,
|
||||
"hash": fields.String,
|
||||
"updated_at": fields.String,
|
||||
},
|
||||
),
|
||||
)
|
||||
@api.response(200, "Draft workflow synced successfully", workflow_fields)
|
||||
@api.response(400, "Invalid workflow configuration")
|
||||
@api.response(403, "Permission denied")
|
||||
@edit_permission_required
|
||||
|
||||
@ -67,7 +67,6 @@ def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: Fe
|
||||
|
||||
kwargs["app_model"] = app_model
|
||||
|
||||
# If caller needs end-user context, attach EndUser to current_user
|
||||
if fetch_user_arg:
|
||||
if fetch_user_arg.fetch_from == WhereisUserArg.QUERY:
|
||||
user_id = request.args.get("user")
|
||||
@ -76,6 +75,7 @@ def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: Fe
|
||||
elif fetch_user_arg.fetch_from == WhereisUserArg.FORM:
|
||||
user_id = request.form.get("user")
|
||||
else:
|
||||
# use default-user
|
||||
user_id = None
|
||||
|
||||
if not user_id and fetch_user_arg.required:
|
||||
@ -90,28 +90,6 @@ def validate_app_token(view: Callable[P, R] | None = None, *, fetch_user_arg: Fe
|
||||
# Set EndUser as current logged-in user for flask_login.current_user
|
||||
current_app.login_manager._update_request_context_with_user(end_user) # type: ignore
|
||||
user_logged_in.send(current_app._get_current_object(), user=end_user) # type: ignore
|
||||
else:
|
||||
# For service API without end-user context, ensure an Account is logged in
|
||||
# so services relying on current_account_with_tenant() work correctly.
|
||||
tenant_owner_info = (
|
||||
db.session.query(Tenant, Account)
|
||||
.join(TenantAccountJoin, Tenant.id == TenantAccountJoin.tenant_id)
|
||||
.join(Account, TenantAccountJoin.account_id == Account.id)
|
||||
.where(
|
||||
Tenant.id == app_model.tenant_id,
|
||||
TenantAccountJoin.role == "owner",
|
||||
Tenant.status == TenantStatus.NORMAL,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if tenant_owner_info:
|
||||
tenant_model, account = tenant_owner_info
|
||||
account.current_tenant = tenant_model
|
||||
current_app.login_manager._update_request_context_with_user(account) # type: ignore
|
||||
user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore
|
||||
else:
|
||||
raise Unauthorized("Tenant owner account not found or tenant is not active.")
|
||||
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
|
||||
@ -6,7 +6,10 @@ from core.helper.code_executor.template_transformer import TemplateTransformer
|
||||
class NodeJsTemplateTransformer(TemplateTransformer):
|
||||
@classmethod
|
||||
def get_runner_script(cls) -> str:
|
||||
runner_script = dedent(f""" {cls._code_placeholder}
|
||||
runner_script = dedent(
|
||||
f"""
|
||||
// declare main function
|
||||
{cls._code_placeholder}
|
||||
|
||||
// decode and prepare input object
|
||||
var inputs_obj = JSON.parse(Buffer.from('{cls._inputs_placeholder}', 'base64').toString('utf-8'))
|
||||
@ -18,5 +21,6 @@ class NodeJsTemplateTransformer(TemplateTransformer):
|
||||
var output_json = JSON.stringify(output_obj)
|
||||
var result = `<<RESULT>>${{output_json}}<<RESULT>>`
|
||||
console.log(result)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
return runner_script
|
||||
|
||||
@ -6,7 +6,9 @@ from core.helper.code_executor.template_transformer import TemplateTransformer
|
||||
class Python3TemplateTransformer(TemplateTransformer):
|
||||
@classmethod
|
||||
def get_runner_script(cls) -> str:
|
||||
runner_script = dedent(f""" {cls._code_placeholder}
|
||||
runner_script = dedent(f"""
|
||||
# declare main function
|
||||
{cls._code_placeholder}
|
||||
|
||||
import json
|
||||
from base64 import b64decode
|
||||
|
||||
@ -39,13 +39,11 @@ class WeaviateConfig(BaseModel):
|
||||
|
||||
Attributes:
|
||||
endpoint: Weaviate server endpoint URL
|
||||
grpc_endpoint: Optional Weaviate gRPC server endpoint URL
|
||||
api_key: Optional API key for authentication
|
||||
batch_size: Number of objects to batch per insert operation
|
||||
"""
|
||||
|
||||
endpoint: str
|
||||
grpc_endpoint: str | None = None
|
||||
api_key: str | None = None
|
||||
batch_size: int = 100
|
||||
|
||||
@ -90,22 +88,9 @@ class WeaviateVector(BaseVector):
|
||||
http_secure = p.scheme == "https"
|
||||
http_port = p.port or (443 if http_secure else 80)
|
||||
|
||||
# Parse gRPC configuration
|
||||
if config.grpc_endpoint:
|
||||
# Urls without scheme won't be parsed correctly in some python verions,
|
||||
# see https://bugs.python.org/issue27657
|
||||
grpc_endpoint_with_scheme = (
|
||||
config.grpc_endpoint if "://" in config.grpc_endpoint else f"grpc://{config.grpc_endpoint}"
|
||||
)
|
||||
grpc_p = urlparse(grpc_endpoint_with_scheme)
|
||||
grpc_host = grpc_p.hostname or "localhost"
|
||||
grpc_port = grpc_p.port or (443 if grpc_p.scheme == "grpcs" else 50051)
|
||||
grpc_secure = grpc_p.scheme == "grpcs"
|
||||
else:
|
||||
# Infer from HTTP endpoint as fallback
|
||||
grpc_host = host
|
||||
grpc_secure = http_secure
|
||||
grpc_port = 443 if grpc_secure else 50051
|
||||
grpc_host = host
|
||||
grpc_secure = http_secure
|
||||
grpc_port = 443 if grpc_secure else 50051
|
||||
|
||||
client = weaviate.connect_to_custom(
|
||||
http_host=host,
|
||||
@ -447,7 +432,6 @@ class WeaviateVectorFactory(AbstractVectorFactory):
|
||||
collection_name=collection_name,
|
||||
config=WeaviateConfig(
|
||||
endpoint=dify_config.WEAVIATE_ENDPOINT or "",
|
||||
grpc_endpoint=dify_config.WEAVIATE_GRPC_ENDPOINT or "",
|
||||
api_key=dify_config.WEAVIATE_API_KEY,
|
||||
batch_size=dify_config.WEAVIATE_BATCH_SIZE,
|
||||
),
|
||||
|
||||
@ -210,13 +210,12 @@ class Tool(ABC):
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
def create_json_message(self, object: dict, suppress_output: bool = False) -> ToolInvokeMessage:
|
||||
def create_json_message(self, object: dict) -> ToolInvokeMessage:
|
||||
"""
|
||||
create a json message
|
||||
"""
|
||||
return ToolInvokeMessage(
|
||||
type=ToolInvokeMessage.MessageType.JSON,
|
||||
message=ToolInvokeMessage.JsonMessage(json_object=object, suppress_output=suppress_output),
|
||||
type=ToolInvokeMessage.MessageType.JSON, message=ToolInvokeMessage.JsonMessage(json_object=object)
|
||||
)
|
||||
|
||||
def create_variable_message(
|
||||
|
||||
@ -129,7 +129,6 @@ class ToolInvokeMessage(BaseModel):
|
||||
|
||||
class JsonMessage(BaseModel):
|
||||
json_object: dict
|
||||
suppress_output: bool = Field(default=False, description="Whether to suppress JSON output in result string")
|
||||
|
||||
class BlobMessage(BaseModel):
|
||||
blob: bytes
|
||||
|
||||
@ -245,9 +245,6 @@ class ToolEngine:
|
||||
+ "you do not need to create it, just tell the user to check it now."
|
||||
)
|
||||
elif response.type == ToolInvokeMessage.MessageType.JSON:
|
||||
json_message = cast(ToolInvokeMessage.JsonMessage, response.message)
|
||||
if json_message.suppress_output:
|
||||
continue
|
||||
json_parts.append(
|
||||
json.dumps(
|
||||
safe_json_value(cast(ToolInvokeMessage.JsonMessage, response.message).json_object),
|
||||
|
||||
@ -117,7 +117,7 @@ class WorkflowTool(Tool):
|
||||
self._latest_usage = self._derive_usage_from_result(data)
|
||||
|
||||
yield self.create_text_message(json.dumps(outputs, ensure_ascii=False))
|
||||
yield self.create_json_message(outputs, suppress_output=True)
|
||||
yield self.create_json_message(outputs)
|
||||
|
||||
@property
|
||||
def latest_usage(self) -> LLMUsage:
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
"""make message annotation question not nullable
|
||||
|
||||
Revision ID: 9e6fa5cbcd80
|
||||
Revises: 03f8dcbc611e
|
||||
Create Date: 2025-11-06 16:03:54.549378
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9e6fa5cbcd80'
|
||||
down_revision = '03f8dcbc611e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
message_annotations = sa.table(
|
||||
"message_annotations",
|
||||
sa.column("id", sa.String),
|
||||
sa.column("message_id", sa.String),
|
||||
sa.column("question", sa.Text),
|
||||
)
|
||||
messages = sa.table(
|
||||
"messages",
|
||||
sa.column("id", sa.String),
|
||||
sa.column("query", sa.Text),
|
||||
)
|
||||
update_question_from_message = (
|
||||
sa.update(message_annotations)
|
||||
.where(
|
||||
sa.and_(
|
||||
message_annotations.c.question.is_(None),
|
||||
message_annotations.c.message_id.isnot(None),
|
||||
)
|
||||
)
|
||||
.values(
|
||||
question=sa.select(sa.func.coalesce(messages.c.query, ""))
|
||||
.where(messages.c.id == message_annotations.c.message_id)
|
||||
.scalar_subquery()
|
||||
)
|
||||
)
|
||||
bind.execute(update_question_from_message)
|
||||
|
||||
fill_remaining_questions = (
|
||||
sa.update(message_annotations)
|
||||
.where(message_annotations.c.question.is_(None))
|
||||
.values(question="")
|
||||
)
|
||||
bind.execute(fill_remaining_questions)
|
||||
with op.batch_alter_table('message_annotations', schema=None) as batch_op:
|
||||
batch_op.alter_column('question', existing_type=sa.TEXT(), nullable=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('message_annotations', schema=None) as batch_op:
|
||||
batch_op.alter_column('question', existing_type=sa.TEXT(), nullable=True)
|
||||
@ -1373,7 +1373,7 @@ class MessageAnnotation(Base):
|
||||
app_id: Mapped[str] = mapped_column(StringUUID)
|
||||
conversation_id: Mapped[str | None] = mapped_column(StringUUID, sa.ForeignKey("conversations.id"))
|
||||
message_id: Mapped[str | None] = mapped_column(StringUUID)
|
||||
question: Mapped[str] = mapped_column(sa.Text, nullable=False)
|
||||
question = mapped_column(sa.Text, nullable=True)
|
||||
content = mapped_column(sa.Text, nullable=False)
|
||||
hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"))
|
||||
account_id = mapped_column(StringUUID, nullable=False)
|
||||
@ -1778,7 +1778,7 @@ class MessageAgentThought(Base):
|
||||
answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
|
||||
tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
|
||||
total_price = mapped_column(sa.Numeric, nullable=True)
|
||||
currency = mapped_column(String, nullable=True)
|
||||
currency: Mapped[str | None] = mapped_column()
|
||||
latency: Mapped[float | None] = mapped_column(sa.Float, nullable=True)
|
||||
created_by_role = mapped_column(String, nullable=False)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
|
||||
@ -186,12 +186,8 @@ class AppAnnotationService:
|
||||
if not app:
|
||||
raise NotFound("App not found")
|
||||
|
||||
question = args.get("question")
|
||||
if question is None:
|
||||
raise ValueError("'question' is required")
|
||||
|
||||
annotation = MessageAnnotation(
|
||||
app_id=app.id, content=args["answer"], question=question, account_id=current_user.id
|
||||
app_id=app.id, content=args["answer"], question=args["question"], account_id=current_user.id
|
||||
)
|
||||
db.session.add(annotation)
|
||||
db.session.commit()
|
||||
@ -200,7 +196,7 @@ class AppAnnotationService:
|
||||
if annotation_setting:
|
||||
add_annotation_to_index_task.delay(
|
||||
annotation.id,
|
||||
question,
|
||||
args["question"],
|
||||
current_tenant_id,
|
||||
app_id,
|
||||
annotation_setting.collection_binding_id,
|
||||
@ -225,12 +221,8 @@ class AppAnnotationService:
|
||||
if not annotation:
|
||||
raise NotFound("Annotation not found")
|
||||
|
||||
question = args.get("question")
|
||||
if question is None:
|
||||
raise ValueError("'question' is required")
|
||||
|
||||
annotation.content = args["answer"]
|
||||
annotation.question = question
|
||||
annotation.question = args["question"]
|
||||
|
||||
db.session.commit()
|
||||
# if annotation reply is enabled , add annotation to index
|
||||
|
||||
@ -220,23 +220,6 @@ class TestAnnotationService:
|
||||
# Note: In this test, no annotation setting exists, so task should not be called
|
||||
mock_external_service_dependencies["add_task"].delay.assert_not_called()
|
||||
|
||||
def test_insert_app_annotation_directly_requires_question(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Question must be provided when inserting annotations directly.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, _ = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
annotation_args = {
|
||||
"question": None,
|
||||
"answer": fake.text(max_nb_chars=200),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
|
||||
|
||||
def test_insert_app_annotation_directly_app_not_found(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider
|
||||
from core.helper.code_executor.javascript.javascript_transformer import NodeJsTemplateTransformer
|
||||
|
||||
|
||||
def test_get_runner_script():
|
||||
code = JavascriptCodeProvider.get_default_code()
|
||||
inputs = {"arg1": "hello, ", "arg2": "world!"}
|
||||
script = NodeJsTemplateTransformer.assemble_runner_script(code, inputs)
|
||||
script_lines = script.splitlines()
|
||||
code_lines = code.splitlines()
|
||||
# Check that the first lines of script are exactly the same as code
|
||||
assert script_lines[: len(code_lines)] == code_lines
|
||||
@ -1,12 +0,0 @@
|
||||
from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider
|
||||
from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer
|
||||
|
||||
|
||||
def test_get_runner_script():
|
||||
code = Python3CodeProvider.get_default_code()
|
||||
inputs = {"arg1": "hello, ", "arg2": "world!"}
|
||||
script = Python3TemplateTransformer.assemble_runner_script(code, inputs)
|
||||
script_lines = script.splitlines()
|
||||
code_lines = code.splitlines()
|
||||
# Check that the first lines of script are exactly the same as code
|
||||
assert script_lines[: len(code_lines)] == code_lines
|
||||
30
dev/ast-grep/rules/remove-nullable-arg.yaml
Normal file
30
dev/ast-grep/rules/remove-nullable-arg.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
id: remove-nullable-arg
|
||||
language: python
|
||||
rule:
|
||||
pattern: $X = mapped_column($$$ARGS)
|
||||
any:
|
||||
- pattern: $X = mapped_column($$$BEFORE, sa.String, $$$MID, nullable=True, $$$AFTER)
|
||||
- pattern: $X = mapped_column($$$BEFORE, sa.String, $$$MID, nullable=True)
|
||||
rewriters:
|
||||
- id: filter-string-nullable
|
||||
rule:
|
||||
pattern: $ARG
|
||||
inside:
|
||||
kind: argument_list
|
||||
all:
|
||||
- not:
|
||||
pattern: String
|
||||
- not:
|
||||
pattern:
|
||||
context: a(nullable=True)
|
||||
selector: keyword_argument
|
||||
fix: $ARG
|
||||
|
||||
transform:
|
||||
NEWARGS:
|
||||
rewrite:
|
||||
rewriters: [filter-string-nullable]
|
||||
source: $$$ARGS
|
||||
joinBy: ', '
|
||||
fix: |-
|
||||
$X: Mapped[str | None] = mapped_column($NEWARGS)
|
||||
@ -492,7 +492,6 @@ VECTOR_INDEX_NAME_PREFIX=Vector_index
|
||||
# The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`.
|
||||
WEAVIATE_ENDPOINT=http://weaviate:8080
|
||||
WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
|
||||
WEAVIATE_GRPC_ENDPOINT=grpc://weaviate:50051
|
||||
|
||||
# The Qdrant endpoint URL. Only available when VECTOR_STORE is `qdrant`.
|
||||
QDRANT_URL=http://qdrant:6333
|
||||
|
||||
@ -157,7 +157,6 @@ x-shared-env: &shared-api-worker-env
|
||||
VECTOR_INDEX_NAME_PREFIX: ${VECTOR_INDEX_NAME_PREFIX:-Vector_index}
|
||||
WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080}
|
||||
WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}
|
||||
WEAVIATE_GRPC_ENDPOINT: ${WEAVIATE_GRPC_ENDPOINT:-grpc://weaviate:50051}
|
||||
QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333}
|
||||
QDRANT_API_KEY: ${QDRANT_API_KEY:-difyai123456}
|
||||
QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20}
|
||||
|
||||
@ -124,7 +124,7 @@ const AppOperations = ({ operations, gap }: {
|
||||
<span className='system-xs-medium text-components-button-secondary-text'>{t('common.operation.more')}</span>
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[30]'>
|
||||
<PortalToFollowElemContent className='z-[21]'>
|
||||
<div className='flex min-w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'>
|
||||
{moreOperations.map(item => <div
|
||||
key={item.id}
|
||||
|
||||
@ -49,7 +49,7 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
<div className='flex h-6 items-center gap-1'>
|
||||
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
|
||||
{!form.required && (
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -49,7 +49,7 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
<div className='flex h-6 items-center gap-1'>
|
||||
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
|
||||
{!form.required && (
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { produce } from 'immer'
|
||||
@ -45,13 +45,7 @@ const OpeningSettingModal = ({
|
||||
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
|
||||
const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
|
||||
|
||||
const isSaveDisabled = useMemo(() => !tempValue.trim(), [tempValue])
|
||||
|
||||
const handleSave = useCallback((ignoreVariablesCheck?: boolean) => {
|
||||
// Prevent saving if opening statement is empty
|
||||
if (isSaveDisabled)
|
||||
return
|
||||
|
||||
if (!ignoreVariablesCheck) {
|
||||
const keys = getInputKeys(tempValue)
|
||||
const promptKeys = promptVariables.map(item => item.key)
|
||||
@ -81,7 +75,7 @@ const OpeningSettingModal = ({
|
||||
}
|
||||
})
|
||||
onSave(newOpening)
|
||||
}, [data, onSave, promptVariables, workflowVariables, showConfirmAddVar, tempSuggestedQuestions, tempValue, isSaveDisabled])
|
||||
}, [data, onSave, promptVariables, workflowVariables, showConfirmAddVar, tempSuggestedQuestions, tempValue])
|
||||
|
||||
const cancelAutoAddVar = useCallback(() => {
|
||||
hideConfirmAddVar()
|
||||
@ -223,7 +217,6 @@ const OpeningSettingModal = ({
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => handleSave()}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
|
||||
@ -4,27 +4,27 @@ import { RiFeedbackLine } from '@remixicon/react'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { registerCommands, unregisterCommands } from './command-bus'
|
||||
|
||||
// Forum command dependency types
|
||||
type ForumDeps = Record<string, never>
|
||||
// Feedback command dependency types
|
||||
type FeedbackDeps = Record<string, never>
|
||||
|
||||
/**
|
||||
* Forum command - Opens Dify community forum
|
||||
* Feedback command - Opens GitHub feedback discussions
|
||||
*/
|
||||
export const forumCommand: SlashCommandHandler<ForumDeps> = {
|
||||
name: 'forum',
|
||||
description: 'Open Dify community forum',
|
||||
export const feedbackCommand: SlashCommandHandler<FeedbackDeps> = {
|
||||
name: 'feedback',
|
||||
description: 'Open feedback discussions',
|
||||
mode: 'direct',
|
||||
|
||||
// Direct execution function
|
||||
execute: () => {
|
||||
const url = 'https://forum.dify.ai'
|
||||
const url = 'https://github.com/langgenius/dify/discussions/categories/feedbacks'
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
|
||||
async search(args: string, locale: string = 'en') {
|
||||
return [{
|
||||
id: 'forum',
|
||||
title: i18n.t('common.userProfile.forum', { lng: locale }),
|
||||
id: 'feedback',
|
||||
title: i18n.t('common.userProfile.communityFeedback', { lng: locale }),
|
||||
description: i18n.t('app.gotoAnything.actions.feedbackDesc', { lng: locale }) || 'Open community feedback discussions',
|
||||
type: 'command' as const,
|
||||
icon: (
|
||||
@ -32,20 +32,20 @@ export const forumCommand: SlashCommandHandler<ForumDeps> = {
|
||||
<RiFeedbackLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'navigation.forum', args: { url: 'https://forum.dify.ai' } },
|
||||
data: { command: 'navigation.feedback', args: { url: 'https://github.com/langgenius/dify/discussions/categories/feedbacks' } },
|
||||
}]
|
||||
},
|
||||
|
||||
register(_deps: ForumDeps) {
|
||||
register(_deps: FeedbackDeps) {
|
||||
registerCommands({
|
||||
'navigation.forum': async (args) => {
|
||||
const url = args?.url || 'https://forum.dify.ai'
|
||||
'navigation.feedback': async (args) => {
|
||||
const url = args?.url || 'https://github.com/langgenius/dify/discussions/categories/feedbacks'
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
unregister() {
|
||||
unregisterCommands(['navigation.forum'])
|
||||
unregisterCommands(['navigation.feedback'])
|
||||
},
|
||||
}
|
||||
@ -7,7 +7,7 @@ import { useTheme } from 'next-themes'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { themeCommand } from './theme'
|
||||
import { languageCommand } from './language'
|
||||
import { forumCommand } from './forum'
|
||||
import { feedbackCommand } from './feedback'
|
||||
import { docsCommand } from './docs'
|
||||
import { communityCommand } from './community'
|
||||
import { accountCommand } from './account'
|
||||
@ -34,7 +34,7 @@ export const registerSlashCommands = (deps: Record<string, any>) => {
|
||||
// Register command handlers to the registry system with their respective dependencies
|
||||
slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
|
||||
slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
|
||||
slashCommandRegistry.register(forumCommand, {})
|
||||
slashCommandRegistry.register(feedbackCommand, {})
|
||||
slashCommandRegistry.register(docsCommand, {})
|
||||
slashCommandRegistry.register(communityCommand, {})
|
||||
slashCommandRegistry.register(accountCommand, {})
|
||||
@ -44,7 +44,7 @@ export const unregisterSlashCommands = () => {
|
||||
// Remove command handlers from registry system (automatically calls each command's unregister method)
|
||||
slashCommandRegistry.unregister('theme')
|
||||
slashCommandRegistry.unregister('language')
|
||||
slashCommandRegistry.unregister('forum')
|
||||
slashCommandRegistry.unregister('feedback')
|
||||
slashCommandRegistry.unregister('docs')
|
||||
slashCommandRegistry.unregister('community')
|
||||
slashCommandRegistry.unregister('account')
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||
import { RiArrowRightSLine, RiArrowRightUpLine, RiChatSmile2Line, RiDiscordLine, RiDiscussLine, RiMailSendLine, RiQuestionLine } from '@remixicon/react'
|
||||
import { RiArrowRightSLine, RiArrowRightUpLine, RiChatSmile2Line, RiDiscordLine, RiFeedbackLine, RiMailSendLine, RiQuestionLine } from '@remixicon/react'
|
||||
import { Fragment } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -86,10 +86,10 @@ export default function Support({ closeAccountDropdown }: SupportProps) {
|
||||
className={cn(itemClassName, 'group justify-between',
|
||||
'data-[active]:bg-state-base-hover',
|
||||
)}
|
||||
href='https://forum.dify.ai/'
|
||||
href='https://github.com/langgenius/dify/discussions/categories/feedbacks'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<RiDiscussLine className='size-4 shrink-0 text-text-tertiary' />
|
||||
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.forum')}</div>
|
||||
<RiFeedbackLine className='size-4 shrink-0 text-text-tertiary' />
|
||||
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.communityFeedback')}</div>
|
||||
<RiArrowRightUpLine className='size-[14px] shrink-0 text-text-tertiary' />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
|
||||
@ -73,7 +73,7 @@ const DetailHeader = ({
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
const {
|
||||
id,
|
||||
installation_id,
|
||||
source,
|
||||
tenant_id,
|
||||
version,
|
||||
@ -197,7 +197,7 @@ const DetailHeader = ({
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
showDeleting()
|
||||
const res = await uninstallPlugin(id)
|
||||
const res = await uninstallPlugin(installation_id)
|
||||
hideDeleting()
|
||||
if (res.success) {
|
||||
hideDeleteConfirm()
|
||||
@ -207,7 +207,7 @@ const DetailHeader = ({
|
||||
if (PluginType.tool.includes(category))
|
||||
invalidateAllToolProviders()
|
||||
}
|
||||
}, [showDeleting, id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders])
|
||||
}, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders])
|
||||
|
||||
return (
|
||||
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
|
||||
@ -351,6 +351,7 @@ const DetailHeader = ({
|
||||
content={
|
||||
<div>
|
||||
{t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{label[locale]}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br />
|
||||
{/* {usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })} */}
|
||||
</div>
|
||||
}
|
||||
onCancel={hideDeleteConfirm}
|
||||
|
||||
@ -72,8 +72,6 @@ const PluginPage = ({
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
|
||||
|
||||
const [dependencies, setDependencies] = useState<Dependency[]>([])
|
||||
const bundleInfo = useMemo(() => {
|
||||
const info = searchParams.get(BUNDLE_INFO_KEY)
|
||||
@ -101,7 +99,6 @@ const PluginPage = ({
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setUniqueIdentifier(null)
|
||||
await sleep(100)
|
||||
if (packageId) {
|
||||
const { data } = await fetchManifestFromMarketPlace(encodeURIComponent(packageId))
|
||||
@ -111,7 +108,6 @@ const PluginPage = ({
|
||||
version: version.version,
|
||||
icon: `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon`,
|
||||
})
|
||||
setUniqueIdentifier(packageId)
|
||||
showInstallFromMarketplace()
|
||||
return
|
||||
}
|
||||
@ -287,10 +283,10 @@ const PluginPage = ({
|
||||
)}
|
||||
|
||||
{
|
||||
isShowInstallFromMarketplace && uniqueIdentifier && (
|
||||
isShowInstallFromMarketplace && (
|
||||
<InstallFromMarketplace
|
||||
manifest={manifest! as PluginManifestInMarket}
|
||||
uniqueIdentifier={uniqueIdentifier}
|
||||
uniqueIdentifier={packageId}
|
||||
isBundle={!!bundleInfo}
|
||||
dependencies={dependencies}
|
||||
onClose={hideInstallFromMarketplace}
|
||||
|
||||
@ -100,10 +100,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
: promptConfig.prompt_variables.map(item => (
|
||||
<div className='mt-4 w-full' key={item.key}>
|
||||
{item.type !== 'checkbox' && (
|
||||
<div className='system-md-semibold flex h-6 items-center gap-1 text-text-secondary'>
|
||||
<div className='truncate'>{item.name}</div>
|
||||
{!item.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
|
||||
</div>
|
||||
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
|
||||
)}
|
||||
<div className='mt-1'>
|
||||
{item.type === 'select' && (
|
||||
@ -118,7 +115,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
{item.type === 'string' && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={item.name}
|
||||
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
value={inputs[item.key]}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
@ -127,7 +124,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
{item.type === 'paragraph' && (
|
||||
<Textarea
|
||||
className='h-[104px] sm:text-xs'
|
||||
placeholder={item.name}
|
||||
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
value={inputs[item.key]}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||
/>
|
||||
@ -135,7 +132,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
{item.type === 'number' && (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={item.name}
|
||||
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
value={inputs[item.key]}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||
/>
|
||||
|
||||
@ -140,7 +140,7 @@ const FormItem: FC<Props> = ({
|
||||
<Input
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
@ -152,7 +152,7 @@ const FormItem: FC<Props> = ({
|
||||
type="number"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
@ -163,7 +163,7 @@ const FormItem: FC<Props> = ({
|
||||
<Textarea
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -106,7 +106,7 @@ const StatusPanel: FC<ResultProps> = ({
|
||||
{status === 'failed' && error && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
|
||||
<div className='system-xs-regular whitespace-pre-wrap text-text-destructive'>{error}</div>
|
||||
<div className='system-xs-regular text-text-destructive'>{error}</div>
|
||||
{
|
||||
!!exceptionCounts && (
|
||||
<>
|
||||
|
||||
@ -259,6 +259,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Variablenschlüssel',
|
||||
name: 'Name des Benutzereingabefelds',
|
||||
optional: 'Optional',
|
||||
type: 'Eingabetyp',
|
||||
action: 'Aktionen',
|
||||
typeString: 'String',
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: 'Arbeitsbereich',
|
||||
createWorkspace: 'Arbeitsbereich erstellen',
|
||||
helpCenter: 'Hilfe',
|
||||
communityFeedback: 'Rückmeldung',
|
||||
roadmap: 'Fahrplan',
|
||||
community: 'Gemeinschaft',
|
||||
about: 'Über',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
support: 'Unterstützung',
|
||||
github: 'GitHub',
|
||||
contactUs: 'Kontaktieren Sie uns',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'KONTO',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
uploadFromComputerLimit: 'Datei hochladen darf {{size}} nicht überschreiten',
|
||||
uploadFromComputerReadError: 'Lesen der Datei fehlgeschlagen, bitte versuchen Sie es erneut.',
|
||||
fileExtensionNotSupport: 'Dateiendung nicht bedient',
|
||||
fileExtensionBlocked: 'Dieser Dateityp ist aus Sicherheitsgründen gesperrt',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Läuft an einem Tag ab',
|
||||
|
||||
@ -346,6 +346,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Variable Key',
|
||||
name: 'User Input Field Name',
|
||||
optional: 'Optional',
|
||||
type: 'Input Type',
|
||||
action: 'Actions',
|
||||
typeString: 'String',
|
||||
|
||||
@ -177,7 +177,7 @@ const translation = {
|
||||
helpCenter: 'Docs',
|
||||
support: 'Support',
|
||||
compliance: 'Compliance',
|
||||
forum: 'Forum',
|
||||
communityFeedback: 'Feedback',
|
||||
roadmap: 'Roadmap',
|
||||
github: 'GitHub',
|
||||
community: 'Community',
|
||||
|
||||
@ -255,6 +255,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Clave de Variable',
|
||||
name: 'Nombre del Campo de Entrada del Usuario',
|
||||
optional: 'Opcional',
|
||||
type: 'Tipo de Entrada',
|
||||
action: 'Acciones',
|
||||
typeString: 'Cadena',
|
||||
|
||||
@ -165,6 +165,7 @@ const translation = {
|
||||
workspace: 'Espacio de trabajo',
|
||||
createWorkspace: 'Crear espacio de trabajo',
|
||||
helpCenter: 'Ayuda',
|
||||
communityFeedback: 'Comentarios',
|
||||
roadmap: 'Hoja de ruta',
|
||||
community: 'Comunidad',
|
||||
about: 'Acerca de',
|
||||
@ -173,7 +174,6 @@ const translation = {
|
||||
compliance: 'Cumplimiento',
|
||||
github: 'GitHub',
|
||||
contactUs: 'Contáctenos',
|
||||
forum: 'Foro',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'CUENTA',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
fileExtensionNotSupport: 'Extensión de archivo no compatible',
|
||||
pasteFileLinkInputPlaceholder: 'Introduzca la URL...',
|
||||
uploadFromComputerLimit: 'El archivo de carga no puede exceder {{size}}',
|
||||
fileExtensionBlocked: 'Este tipo de archivo está bloqueado por motivos de seguridad',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Caduca en un día',
|
||||
|
||||
@ -588,6 +588,7 @@ const translation = {
|
||||
typeString: 'رشته',
|
||||
name: 'نام فیلد ورودی کاربر',
|
||||
type: 'نوع ورودی',
|
||||
optional: 'اختیاری',
|
||||
},
|
||||
varKeyError: {},
|
||||
otherError: {
|
||||
|
||||
@ -165,6 +165,7 @@ const translation = {
|
||||
workspace: 'فضای کاری',
|
||||
createWorkspace: 'ایجاد فضای کاری',
|
||||
helpCenter: 'راهنما',
|
||||
communityFeedback: 'بازخورد',
|
||||
roadmap: 'نقشه راه',
|
||||
community: 'انجمن',
|
||||
about: 'درباره',
|
||||
@ -173,7 +174,6 @@ const translation = {
|
||||
compliance: 'انطباق',
|
||||
support: 'پشتیبانی',
|
||||
contactUs: 'با ما تماس بگیرید',
|
||||
forum: 'انجمن',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'حساب کاربری',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
uploadFromComputerUploadError: 'آپلود فایل انجام نشد، لطفا دوباره آپلود کنید.',
|
||||
pasteFileLink: 'پیوند فایل را جایگذاری کنید',
|
||||
uploadFromComputerLimit: 'آپلود فایل نمی تواند از {{size}} تجاوز کند',
|
||||
fileExtensionBlocked: 'این نوع فایل به دلایل امنیتی مسدود شده است',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: 'انقضا در {{count}} روز',
|
||||
|
||||
@ -259,6 +259,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Clé Variable',
|
||||
name: 'Nom du champ d\'entrée de l\'utilisateur',
|
||||
optional: 'Facultatif',
|
||||
type: 'Type d\'Entrée',
|
||||
action: 'Actions',
|
||||
typeString: 'Chaîne',
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: 'Espace de travail',
|
||||
createWorkspace: 'Créer un Espace de Travail',
|
||||
helpCenter: 'Aide',
|
||||
communityFeedback: 'Retour d\'information',
|
||||
roadmap: 'Feuille de route',
|
||||
community: 'Communauté',
|
||||
about: 'À propos',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
github: 'GitHub',
|
||||
compliance: 'Conformité',
|
||||
contactUs: 'Contactez-nous',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'COMPTE',
|
||||
@ -727,7 +727,6 @@ const translation = {
|
||||
fileExtensionNotSupport: 'Extension de fichier non prise en charge',
|
||||
pasteFileLinkInvalid: 'Lien de fichier non valide',
|
||||
uploadFromComputerLimit: 'Le fichier de téléchargement ne peut pas dépasser {{size}}',
|
||||
fileExtensionBlocked: 'Ce type de fichier est bloqué pour des raisons de sécurité',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Expirant dans un jour',
|
||||
|
||||
@ -279,6 +279,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'वेरिएबल कुंजी',
|
||||
name: 'उपयोगकर्ता इनपुट फ़ील्ड नाम',
|
||||
optional: 'वैकल्पिक',
|
||||
type: 'इनपुट प्रकार',
|
||||
action: 'क्रियाएँ',
|
||||
typeString: 'स्ट्रिंग',
|
||||
|
||||
@ -170,6 +170,7 @@ const translation = {
|
||||
workspace: 'वर्कस्पेस',
|
||||
createWorkspace: 'वर्कस्पेस बनाएं',
|
||||
helpCenter: 'सहायता',
|
||||
communityFeedback: 'प्रतिक्रिया',
|
||||
roadmap: 'रोडमैप',
|
||||
community: 'समुदाय',
|
||||
about: 'के बारे में',
|
||||
@ -178,7 +179,6 @@ const translation = {
|
||||
github: 'गिटहब',
|
||||
support: 'समर्थन',
|
||||
contactUs: 'संपर्क करें',
|
||||
forum: 'फोरम',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'खाता',
|
||||
@ -748,7 +748,6 @@ const translation = {
|
||||
pasteFileLink: 'फ़ाइल लिंक पेस्ट करें',
|
||||
fileExtensionNotSupport: 'फ़ाइल एक्सटेंशन समर्थित नहीं है',
|
||||
uploadFromComputer: 'स्थानीय अपलोड',
|
||||
fileExtensionBlocked: 'सुरक्षा कारणों से इस फ़ाइल प्रकार को अवरुद्ध कर दिया गया है',
|
||||
},
|
||||
license: {
|
||||
expiring: 'एक दिन में समाप्त हो रहा है',
|
||||
|
||||
@ -325,6 +325,7 @@ const translation = {
|
||||
variableTable: {
|
||||
action: 'Tindakan',
|
||||
typeString: 'String',
|
||||
optional: 'Fakultatif',
|
||||
typeSelect: 'Pilih',
|
||||
type: 'Jenis Masukan',
|
||||
key: 'Kunci Variabel',
|
||||
|
||||
@ -163,6 +163,7 @@ const translation = {
|
||||
helpCenter: 'Docs',
|
||||
compliance: 'Kepatuhan',
|
||||
community: 'Masyarakat',
|
||||
communityFeedback: 'Umpan balik',
|
||||
roadmap: 'Peta jalan',
|
||||
logout: 'Keluar',
|
||||
settings: 'Pengaturan',
|
||||
@ -172,7 +173,6 @@ const translation = {
|
||||
workspace: 'Workspace',
|
||||
createWorkspace: 'Membuat Ruang Kerja',
|
||||
contactUs: 'Hubungi Kami',
|
||||
forum: 'Forum',
|
||||
},
|
||||
compliance: {
|
||||
soc2Type2: 'Laporan SOC 2 Tipe II',
|
||||
@ -701,7 +701,6 @@ const translation = {
|
||||
pasteFileLinkInvalid: 'Tautan file tidak valid',
|
||||
pasteFileLinkInputPlaceholder: 'Masukkan URL...',
|
||||
uploadFromComputerReadError: 'Pembacaan file gagal, silakan coba lagi.',
|
||||
fileExtensionBlocked: 'Tipe file ini diblokir karena alasan keamanan',
|
||||
},
|
||||
tag: {
|
||||
noTag: 'Tidak ada tag',
|
||||
|
||||
@ -281,6 +281,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Chiave Variabile',
|
||||
name: 'Nome Campo Input Utente',
|
||||
optional: 'Opzionale',
|
||||
type: 'Tipo di Input',
|
||||
action: 'Azioni',
|
||||
typeString: 'Stringa',
|
||||
|
||||
@ -170,6 +170,7 @@ const translation = {
|
||||
workspace: 'Workspace',
|
||||
createWorkspace: 'Crea Workspace',
|
||||
helpCenter: 'Aiuto',
|
||||
communityFeedback: 'Feedback',
|
||||
roadmap: 'Tabella di marcia',
|
||||
community: 'Comunità',
|
||||
about: 'Informazioni',
|
||||
@ -178,7 +179,6 @@ const translation = {
|
||||
compliance: 'Conformità',
|
||||
github: 'GitHub',
|
||||
contactUs: 'Contattaci',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'ACCOUNT',
|
||||
@ -756,7 +756,6 @@ const translation = {
|
||||
uploadFromComputerUploadError: 'Caricamento del file non riuscito, carica di nuovo.',
|
||||
pasteFileLink: 'Incolla il collegamento del file',
|
||||
uploadFromComputerReadError: 'Lettura del file non riuscita, riprovare.',
|
||||
fileExtensionBlocked: 'Questo tipo di file è bloccato per motivi di sicurezza',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: 'Scadenza tra {{count}} giorni',
|
||||
|
||||
@ -340,6 +340,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: '変数キー',
|
||||
name: 'ユーザー入力フィールド名',
|
||||
optional: 'オプション',
|
||||
type: '入力タイプ',
|
||||
action: 'アクション',
|
||||
typeString: '文字列',
|
||||
|
||||
@ -173,13 +173,13 @@ const translation = {
|
||||
helpCenter: 'ヘルプ',
|
||||
support: 'サポート',
|
||||
compliance: 'コンプライアンス',
|
||||
communityFeedback: 'フィードバック',
|
||||
roadmap: 'ロードマップ',
|
||||
community: 'コミュニティ',
|
||||
about: 'Dify について',
|
||||
logout: 'ログアウト',
|
||||
github: 'GitHub',
|
||||
contactUs: 'お問い合わせ',
|
||||
forum: 'フォーラム',
|
||||
},
|
||||
compliance: {
|
||||
soc2Type1: 'SOC 2 Type I 報告書',
|
||||
@ -740,7 +740,6 @@ const translation = {
|
||||
uploadFromComputerReadError: 'ファイルの読み取りに失敗しました。もう一度やり直してください。',
|
||||
fileExtensionNotSupport: 'ファイル拡張子はサポートされていません',
|
||||
pasteFileLinkInvalid: '無効なファイルリンク',
|
||||
fileExtensionBlocked: 'このファイルタイプは、セキュリティ上の理由でブロックされています',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: '有効期限 {{count}} 日',
|
||||
|
||||
@ -255,6 +255,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: '변수 키',
|
||||
name: '사용자 입력 필드명',
|
||||
optional: '옵션',
|
||||
type: '입력 타입',
|
||||
action: '액션',
|
||||
typeString: '문자열',
|
||||
|
||||
@ -157,6 +157,7 @@ const translation = {
|
||||
workspace: '작업 공간',
|
||||
createWorkspace: '작업 공간 만들기',
|
||||
helpCenter: '도움말 센터',
|
||||
communityFeedback: '로드맵 및 피드백',
|
||||
roadmap: '로드맵',
|
||||
community: '커뮤니티',
|
||||
about: 'Dify 소개',
|
||||
@ -165,7 +166,6 @@ const translation = {
|
||||
compliance: '컴플라이언스',
|
||||
support: '지원',
|
||||
contactUs: '문의하기',
|
||||
forum: '포럼',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: '계정',
|
||||
@ -722,7 +722,6 @@ const translation = {
|
||||
fileExtensionNotSupport: '지원되지 않는 파일 확장자',
|
||||
uploadFromComputerLimit: '업로드 파일은 {{size}}를 초과할 수 없습니다.',
|
||||
uploadFromComputerUploadError: '파일 업로드에 실패했습니다. 다시 업로드하십시오.',
|
||||
fileExtensionBlocked: '보안상의 이유로 이 파일 형식은 차단되었습니다',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: '{{count}}일 후에 만료',
|
||||
|
||||
@ -277,6 +277,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Klucz Zmiennej',
|
||||
name: 'Nazwa Pola Wejściowego Użytkownika',
|
||||
optional: 'Opcjonalnie',
|
||||
type: 'Typ Wejścia',
|
||||
action: 'Akcje',
|
||||
typeString: 'String',
|
||||
|
||||
@ -166,6 +166,7 @@ const translation = {
|
||||
workspace: 'Przestrzeń robocza',
|
||||
createWorkspace: 'Utwórz przestrzeń roboczą',
|
||||
helpCenter: 'Pomoc',
|
||||
communityFeedback: 'Opinie',
|
||||
roadmap: 'Plan działania',
|
||||
community: 'Społeczność',
|
||||
about: 'O',
|
||||
@ -174,7 +175,6 @@ const translation = {
|
||||
github: 'GitHub',
|
||||
compliance: 'Zgodność',
|
||||
contactUs: 'Skontaktuj się z nami',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'KONTO',
|
||||
@ -744,7 +744,6 @@ const translation = {
|
||||
uploadFromComputerReadError: 'Odczyt pliku nie powiódł się, spróbuj ponownie.',
|
||||
fileExtensionNotSupport: 'Rozszerzenie pliku nie jest obsługiwane',
|
||||
uploadFromComputer: 'Przesyłanie lokalne',
|
||||
fileExtensionBlocked: 'Ten typ pliku jest zablokowany ze względów bezpieczeństwa',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: 'Wygasa za {{count}} dni',
|
||||
|
||||
@ -261,6 +261,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Chave da Variável',
|
||||
name: 'Nome do Campo de Entrada do Usuário',
|
||||
optional: 'Opcional',
|
||||
type: 'Tipo de Entrada',
|
||||
action: 'Ações',
|
||||
typeString: 'Texto',
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: 'Espaço de trabalho',
|
||||
createWorkspace: 'Criar Espaço de Trabalho',
|
||||
helpCenter: 'Ajuda',
|
||||
communityFeedback: 'Feedback',
|
||||
roadmap: 'Roteiro',
|
||||
community: 'Comunidade',
|
||||
about: 'Sobre',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
support: 'Suporte',
|
||||
compliance: 'Conformidade',
|
||||
contactUs: 'Contate-Nos',
|
||||
forum: 'Fórum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'CONTA',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
uploadFromComputerReadError: 'Falha na leitura do arquivo, tente novamente.',
|
||||
uploadFromComputerLimit: 'Carregar arquivo não pode exceder {{size}}',
|
||||
uploadFromComputerUploadError: 'Falha no upload do arquivo, faça o upload novamente.',
|
||||
fileExtensionBlocked: 'Este tipo de arquivo está bloqueado por razões de segurança',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Expirando em um dia',
|
||||
|
||||
@ -261,6 +261,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Cheie variabilă',
|
||||
name: 'Nume câmp de intrare utilizator',
|
||||
optional: 'Opțional',
|
||||
type: 'Tip intrare',
|
||||
action: 'Acțiuni',
|
||||
typeString: 'Șir',
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: 'Spațiu de lucru',
|
||||
createWorkspace: 'Creează Spațiu de lucru',
|
||||
helpCenter: 'Ajutor',
|
||||
communityFeedback: 'Feedback',
|
||||
roadmap: 'Plan de acțiune',
|
||||
community: 'Comunitate',
|
||||
about: 'Despre',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
support: 'Suport',
|
||||
compliance: 'Conformitate',
|
||||
contactUs: 'Contactați-ne',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'CONT',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
pasteFileLinkInvalid: 'Link fișier nevalid',
|
||||
uploadFromComputerLimit: 'Încărcarea fișierului nu poate depăși {{size}}',
|
||||
pasteFileLink: 'Lipiți linkul fișierului',
|
||||
fileExtensionBlocked: 'Acest tip de fișier este blocat din motive de securitate',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Expiră într-o zi',
|
||||
|
||||
@ -327,6 +327,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Ключ переменной',
|
||||
name: 'Имя поля пользовательского ввода',
|
||||
optional: 'Необязательно',
|
||||
type: 'Тип ввода',
|
||||
action: 'Действия',
|
||||
typeString: 'Строка',
|
||||
|
||||
@ -165,6 +165,7 @@ const translation = {
|
||||
workspace: 'Рабочее пространство',
|
||||
createWorkspace: 'Создать рабочее пространство',
|
||||
helpCenter: 'Помощь',
|
||||
communityFeedback: 'Обратная связь',
|
||||
roadmap: 'План развития',
|
||||
community: 'Сообщество',
|
||||
about: 'О нас',
|
||||
@ -173,7 +174,6 @@ const translation = {
|
||||
compliance: 'Соблюдение',
|
||||
support: 'Поддержка',
|
||||
contactUs: 'Свяжитесь с нами',
|
||||
forum: 'Форум',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'АККАУНТ',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
pasteFileLinkInvalid: 'Неверная ссылка на файл',
|
||||
uploadFromComputerLimit: 'Файл загрузки не может превышать {{size}}',
|
||||
uploadFromComputerUploadError: 'Загрузка файла не удалась, пожалуйста, загрузите еще раз.',
|
||||
fileExtensionBlocked: 'Этот тип файла заблокирован по соображениям безопасности',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Срок действия истекает за один день',
|
||||
|
||||
@ -350,6 +350,7 @@ const translation = {
|
||||
},
|
||||
variableTable: {
|
||||
action: 'Dejanja',
|
||||
optional: 'Neobvezno',
|
||||
typeString: 'Niz',
|
||||
typeSelect: 'Izbrati',
|
||||
type: 'Vrsta vnosa',
|
||||
|
||||
@ -165,6 +165,7 @@ const translation = {
|
||||
workspace: 'Delovni prostor',
|
||||
createWorkspace: 'Ustvari delovni prostor',
|
||||
helpCenter: 'Pomoč',
|
||||
communityFeedback: 'Povratne informacije',
|
||||
roadmap: 'Načrt razvoja',
|
||||
community: 'Skupnost',
|
||||
about: 'O nas',
|
||||
@ -173,7 +174,6 @@ const translation = {
|
||||
github: 'GitHub',
|
||||
compliance: 'Skladnost',
|
||||
contactUs: 'Kontaktirajte nas',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'SPLOŠNO',
|
||||
@ -792,7 +792,6 @@ const translation = {
|
||||
uploadFromComputer: 'Lokalno nalaganje',
|
||||
uploadFromComputerLimit: 'Nalaganje {{type}} ne sme presegati {{size}}',
|
||||
uploadFromComputerReadError: 'Branje datoteke ni uspelo, poskusite znova.',
|
||||
fileExtensionBlocked: 'Ta vrsta datoteke je zaradi varnostnih razlogov blokirana',
|
||||
},
|
||||
tag: {
|
||||
addTag: 'Dodajanje oznak',
|
||||
|
||||
@ -323,6 +323,7 @@ const translation = {
|
||||
timeoutExceeded: 'ผลลัพธ์จะไม่แสดงเนื่องจากหมดเวลา โปรดดูบันทึกเพื่อรวบรวมผลลัพธ์ที่สมบูรณ์',
|
||||
},
|
||||
variableTable: {
|
||||
optional: 'เสริม',
|
||||
key: 'ปุ่มตัวแปร',
|
||||
typeString: 'เชือก',
|
||||
typeSelect: 'เลือก',
|
||||
|
||||
@ -160,6 +160,7 @@ const translation = {
|
||||
workspace: 'พื้นที่',
|
||||
createWorkspace: 'สร้างพื้นที่ทํางาน',
|
||||
helpCenter: 'วิธีใช้',
|
||||
communityFeedback: 'การตอบสนอง',
|
||||
roadmap: 'แผนงาน',
|
||||
community: 'ชุมชน',
|
||||
about: 'ประมาณ',
|
||||
@ -168,7 +169,6 @@ const translation = {
|
||||
compliance: 'การปฏิบัติตามข้อกำหนด',
|
||||
support: 'การสนับสนุน',
|
||||
contactUs: 'ติดต่อเรา',
|
||||
forum: 'ฟอรั่ม',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'ทั่วไป',
|
||||
@ -706,7 +706,6 @@ const translation = {
|
||||
uploadFromComputerLimit: 'อัปโหลด {{type}} ต้องไม่เกิน {{size}}',
|
||||
pasteFileLinkInvalid: 'ลิงก์ไฟล์ไม่ถูกต้อง',
|
||||
fileExtensionNotSupport: 'ไม่รองรับนามสกุลไฟล์',
|
||||
fileExtensionBlocked: 'ประเภทไฟล์นี้ถูกบล็อกด้วยเหตุผลด้านความปลอดภัย',
|
||||
},
|
||||
tag: {
|
||||
placeholder: 'แท็กทั้งหมด',
|
||||
|
||||
@ -327,6 +327,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Değişken Anahtarı',
|
||||
name: 'Kullanıcı Giriş Alanı Adı',
|
||||
optional: 'İsteğe Bağlı',
|
||||
type: 'Giriş Tipi',
|
||||
action: 'Aksiyonlar',
|
||||
typeString: 'Metin',
|
||||
|
||||
@ -165,6 +165,7 @@ const translation = {
|
||||
workspace: 'Çalışma Alanı',
|
||||
createWorkspace: 'Çalışma Alanı Oluştur',
|
||||
helpCenter: 'Yardım',
|
||||
communityFeedback: 'Geri Bildirim',
|
||||
roadmap: 'Yol haritası',
|
||||
community: 'Topluluk',
|
||||
about: 'Hakkında',
|
||||
@ -173,7 +174,6 @@ const translation = {
|
||||
compliance: 'Uygunluk',
|
||||
github: 'GitHub',
|
||||
contactUs: 'Bize Ulaşın',
|
||||
forum: 'Forum',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'HESAP',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
pasteFileLinkInputPlaceholder: 'URL\'yi giriniz...',
|
||||
pasteFileLinkInvalid: 'Geçersiz dosya bağlantısı',
|
||||
fileExtensionNotSupport: 'Dosya uzantısı desteklenmiyor',
|
||||
fileExtensionBlocked: 'Bu dosya türü güvenlik nedenleriyle engellenmiştir',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: '{{count}} gün içinde sona eriyor',
|
||||
|
||||
@ -273,6 +273,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Ключ змінної', // Variable Key
|
||||
name: 'Назва поля для введення користувача', // User Input Field Name
|
||||
optional: 'Додатково', // Optional
|
||||
type: 'Тип введення', // Input Type
|
||||
action: 'Дії', // Actions
|
||||
typeString: 'Рядок', // String
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: 'Робочий простір',
|
||||
createWorkspace: 'Створити робочий простір',
|
||||
helpCenter: 'Довідковий центр',
|
||||
communityFeedback: 'відгуки',
|
||||
roadmap: 'Дорожня карта',
|
||||
community: 'Спільнота',
|
||||
about: 'Про нас',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
support: 'Підтримка',
|
||||
github: 'Гітхаб',
|
||||
contactUs: 'Зв’яжіться з нами',
|
||||
forum: 'Форум',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'ОБЛІКОВИЙ ЗАПИС',
|
||||
@ -727,7 +727,6 @@ const translation = {
|
||||
fileExtensionNotSupport: 'Розширення файлу не підтримується',
|
||||
uploadFromComputerReadError: 'Не вдалося прочитати файл, будь ласка, спробуйте ще раз.',
|
||||
uploadFromComputerUploadError: 'Не вдалося завантажити файл, будь ласка, завантажте ще раз.',
|
||||
fileExtensionBlocked: 'Цей тип файлу заблоковано з міркувань безпеки',
|
||||
},
|
||||
license: {
|
||||
expiring: 'Термін дії закінчується за один день',
|
||||
|
||||
@ -255,6 +255,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: 'Khóa biến',
|
||||
name: 'Tên trường nhập liệu người dùng',
|
||||
optional: 'Tùy chọn',
|
||||
type: 'Loại nhập liệu',
|
||||
action: 'Hành động',
|
||||
typeString: 'Chuỗi',
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: 'Không gian làm việc',
|
||||
createWorkspace: 'Tạo Không gian làm việc',
|
||||
helpCenter: 'Trung tâm trợ giúp',
|
||||
communityFeedback: 'Phản hồi',
|
||||
roadmap: 'Lộ trình',
|
||||
community: 'Cộng đồng',
|
||||
about: 'Về chúng tôi',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
github: 'GitHub',
|
||||
support: 'Hỗ trợ',
|
||||
contactUs: 'Liên hệ với chúng tôi',
|
||||
forum: 'Diễn đàn',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: 'TÀI KHOẢN',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
pasteFileLinkInvalid: 'Liên kết tệp không hợp lệ',
|
||||
uploadFromComputerUploadError: 'Tải lên tệp không thành công, vui lòng tải lên lại.',
|
||||
uploadFromComputerReadError: 'Đọc tệp không thành công, vui lòng thử lại.',
|
||||
fileExtensionBlocked: 'Loại tệp này bị chặn vì lý do bảo mật',
|
||||
},
|
||||
license: {
|
||||
expiring_plural: 'Hết hạn sau {{count}} ngày',
|
||||
|
||||
@ -342,6 +342,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: '变量 Key',
|
||||
name: '字段名称',
|
||||
optional: '可选',
|
||||
type: '类型',
|
||||
action: '操作',
|
||||
typeString: '文本',
|
||||
|
||||
@ -176,7 +176,7 @@ const translation = {
|
||||
helpCenter: '帮助文档',
|
||||
support: '支持',
|
||||
compliance: '合规',
|
||||
forum: '论坛',
|
||||
communityFeedback: '用户反馈',
|
||||
roadmap: '路线图',
|
||||
github: 'GitHub',
|
||||
community: '社区',
|
||||
|
||||
@ -255,6 +255,7 @@ const translation = {
|
||||
variableTable: {
|
||||
key: '變數 Key',
|
||||
name: '欄位名稱',
|
||||
optional: '可選',
|
||||
type: '型別',
|
||||
action: '操作',
|
||||
typeString: '文字',
|
||||
|
||||
@ -161,6 +161,7 @@ const translation = {
|
||||
workspace: '工作空間',
|
||||
createWorkspace: '建立工作空間',
|
||||
helpCenter: '幫助文件',
|
||||
communityFeedback: '使用者反饋',
|
||||
roadmap: '路線圖',
|
||||
community: '社群',
|
||||
about: '關於',
|
||||
@ -169,7 +170,6 @@ const translation = {
|
||||
github: 'GitHub',
|
||||
compliance: '合規',
|
||||
contactUs: '聯絡我們',
|
||||
forum: '論壇',
|
||||
},
|
||||
settings: {
|
||||
accountGroup: '賬戶',
|
||||
@ -726,7 +726,6 @@ const translation = {
|
||||
uploadFromComputer: '本地上傳',
|
||||
fileExtensionNotSupport: '不支援檔擴展名',
|
||||
uploadFromComputerLimit: '上傳文件不能超過 {{size}}',
|
||||
fileExtensionBlocked: '出於安全原因,此檔案類型被阻止',
|
||||
},
|
||||
license: {
|
||||
expiring: '將在 1 天內過期',
|
||||
|
||||
@ -208,7 +208,7 @@
|
||||
"canvas": "^3.2.0",
|
||||
"esbuild": "~0.25.0",
|
||||
"pbkdf2": "~3.1.3",
|
||||
"vite": "~6.4.1",
|
||||
"vite": "~6.2",
|
||||
"prismjs": "~1.30",
|
||||
"brace-expansion": "~2.0"
|
||||
},
|
||||
@ -231,7 +231,7 @@
|
||||
"esbuild@<0.25.0": "0.25.0",
|
||||
"pbkdf2@<3.1.3": "3.1.3",
|
||||
"prismjs@<1.30.0": "1.30.0",
|
||||
"vite@<6.4.1": "6.4.1",
|
||||
"vite@<6.2.7": "6.2.7",
|
||||
"array-includes": "npm:@nolyfill/array-includes@^1",
|
||||
"array.prototype.findlast": "npm:@nolyfill/array.prototype.findlast@^1",
|
||||
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@^1",
|
||||
|
||||
633
web/pnpm-lock.yaml
generated
633
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Test suite for app redirection utility functions
|
||||
* Tests navigation path generation based on user permissions and app modes
|
||||
*/
|
||||
import { getRedirection, getRedirectionPath } from './app-redirection'
|
||||
|
||||
describe('app-redirection', () => {
|
||||
/**
|
||||
* Tests getRedirectionPath which determines the correct path based on:
|
||||
* - User's editor permissions
|
||||
* - App mode (workflow, advanced-chat, chat, completion, agent-chat)
|
||||
*/
|
||||
describe('getRedirectionPath', () => {
|
||||
test('returns overview path when user is not editor', () => {
|
||||
const app = { id: 'app-123', mode: 'chat' as const }
|
||||
const result = getRedirectionPath(false, app)
|
||||
expect(result).toBe('/app/app-123/overview')
|
||||
})
|
||||
|
||||
test('returns workflow path for workflow mode when user is editor', () => {
|
||||
const app = { id: 'app-123', mode: 'workflow' as const }
|
||||
const result = getRedirectionPath(true, app)
|
||||
expect(result).toBe('/app/app-123/workflow')
|
||||
})
|
||||
|
||||
test('returns workflow path for advanced-chat mode when user is editor', () => {
|
||||
const app = { id: 'app-123', mode: 'advanced-chat' as const }
|
||||
const result = getRedirectionPath(true, app)
|
||||
expect(result).toBe('/app/app-123/workflow')
|
||||
})
|
||||
|
||||
test('returns configuration path for chat mode when user is editor', () => {
|
||||
const app = { id: 'app-123', mode: 'chat' as const }
|
||||
const result = getRedirectionPath(true, app)
|
||||
expect(result).toBe('/app/app-123/configuration')
|
||||
})
|
||||
|
||||
test('returns configuration path for completion mode when user is editor', () => {
|
||||
const app = { id: 'app-123', mode: 'completion' as const }
|
||||
const result = getRedirectionPath(true, app)
|
||||
expect(result).toBe('/app/app-123/configuration')
|
||||
})
|
||||
|
||||
test('returns configuration path for agent-chat mode when user is editor', () => {
|
||||
const app = { id: 'app-456', mode: 'agent-chat' as const }
|
||||
const result = getRedirectionPath(true, app)
|
||||
expect(result).toBe('/app/app-456/configuration')
|
||||
})
|
||||
|
||||
test('handles different app IDs', () => {
|
||||
const app1 = { id: 'abc-123', mode: 'chat' as const }
|
||||
const app2 = { id: 'xyz-789', mode: 'workflow' as const }
|
||||
|
||||
expect(getRedirectionPath(false, app1)).toBe('/app/abc-123/overview')
|
||||
expect(getRedirectionPath(true, app2)).toBe('/app/xyz-789/workflow')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests getRedirection which combines path generation with a redirect callback
|
||||
*/
|
||||
describe('getRedirection', () => {
|
||||
/**
|
||||
* Tests that the redirection function is called with the correct path
|
||||
*/
|
||||
test('calls redirection function with correct path for non-editor', () => {
|
||||
const app = { id: 'app-123', mode: 'chat' as const }
|
||||
const mockRedirect = jest.fn()
|
||||
|
||||
getRedirection(false, app, mockRedirect)
|
||||
|
||||
expect(mockRedirect).toHaveBeenCalledWith('/app/app-123/overview')
|
||||
expect(mockRedirect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('calls redirection function with workflow path for editor', () => {
|
||||
const app = { id: 'app-123', mode: 'workflow' as const }
|
||||
const mockRedirect = jest.fn()
|
||||
|
||||
getRedirection(true, app, mockRedirect)
|
||||
|
||||
expect(mockRedirect).toHaveBeenCalledWith('/app/app-123/workflow')
|
||||
expect(mockRedirect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('calls redirection function with configuration path for chat mode editor', () => {
|
||||
const app = { id: 'app-123', mode: 'chat' as const }
|
||||
const mockRedirect = jest.fn()
|
||||
|
||||
getRedirection(true, app, mockRedirect)
|
||||
|
||||
expect(mockRedirect).toHaveBeenCalledWith('/app/app-123/configuration')
|
||||
expect(mockRedirect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('works with different redirection functions', () => {
|
||||
const app = { id: 'app-123', mode: 'workflow' as const }
|
||||
const paths: string[] = []
|
||||
const customRedirect = (path: string) => paths.push(path)
|
||||
|
||||
getRedirection(true, app, customRedirect)
|
||||
|
||||
expect(paths).toEqual(['/app/app-123/workflow'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,18 +1,6 @@
|
||||
/**
|
||||
* Test suite for the classnames utility function
|
||||
* This utility combines the classnames library with tailwind-merge
|
||||
* to handle conditional CSS classes and merge conflicting Tailwind classes
|
||||
*/
|
||||
import cn from './classnames'
|
||||
|
||||
describe('classnames', () => {
|
||||
/**
|
||||
* Tests basic classnames library features:
|
||||
* - String concatenation
|
||||
* - Array handling
|
||||
* - Falsy value filtering
|
||||
* - Object-based conditional classes
|
||||
*/
|
||||
test('classnames libs feature', () => {
|
||||
expect(cn('foo')).toBe('foo')
|
||||
expect(cn('foo', 'bar')).toBe('foo bar')
|
||||
@ -29,14 +17,6 @@ describe('classnames', () => {
|
||||
})).toBe('foo baz')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests tailwind-merge functionality:
|
||||
* - Conflicting class resolution (last one wins)
|
||||
* - Modifier handling (hover, focus, etc.)
|
||||
* - Important prefix (!)
|
||||
* - Custom color classes
|
||||
* - Arbitrary values
|
||||
*/
|
||||
test('tailwind-merge', () => {
|
||||
/* eslint-disable tailwindcss/classnames-order */
|
||||
expect(cn('p-0')).toBe('p-0')
|
||||
@ -64,10 +44,6 @@ describe('classnames', () => {
|
||||
expect(cn('text-3.5xl text-black')).toBe('text-3.5xl text-black')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests the integration of classnames and tailwind-merge:
|
||||
* - Object-based conditional classes with Tailwind conflict resolution
|
||||
*/
|
||||
test('classnames combined with tailwind-merge', () => {
|
||||
expect(cn('text-right', {
|
||||
'text-center': true,
|
||||
@ -77,81 +53,4 @@ describe('classnames', () => {
|
||||
'text-center': false,
|
||||
})).toBe('text-right')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests handling of multiple mixed argument types:
|
||||
* - Strings, arrays, and objects in a single call
|
||||
* - Tailwind merge working across different argument types
|
||||
*/
|
||||
test('multiple mixed argument types', () => {
|
||||
expect(cn('foo', ['bar', 'baz'], { qux: true, quux: false })).toBe('foo bar baz qux')
|
||||
expect(cn('p-4', ['p-2', 'm-4'], { 'text-left': true, 'text-right': true })).toBe('p-2 m-4 text-right')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests nested array handling:
|
||||
* - Deep array flattening
|
||||
* - Tailwind merge with nested structures
|
||||
*/
|
||||
test('nested arrays', () => {
|
||||
expect(cn(['foo', ['bar', 'baz']])).toBe('foo bar baz')
|
||||
expect(cn(['p-4', ['p-2', 'text-center']])).toBe('p-2 text-center')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests empty input handling:
|
||||
* - Empty strings, arrays, and objects
|
||||
* - Mixed empty and non-empty values
|
||||
*/
|
||||
test('empty inputs', () => {
|
||||
expect(cn('')).toBe('')
|
||||
expect(cn([])).toBe('')
|
||||
expect(cn({})).toBe('')
|
||||
expect(cn('', [], {})).toBe('')
|
||||
expect(cn('foo', '', 'bar')).toBe('foo bar')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests number input handling:
|
||||
* - Truthy numbers converted to strings
|
||||
* - Zero treated as falsy
|
||||
*/
|
||||
test('numbers as inputs', () => {
|
||||
expect(cn(1)).toBe('1')
|
||||
expect(cn(0)).toBe('')
|
||||
expect(cn('foo', 1, 'bar')).toBe('foo 1 bar')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests multiple object arguments:
|
||||
* - Object merging
|
||||
* - Tailwind conflict resolution across objects
|
||||
*/
|
||||
test('multiple objects', () => {
|
||||
expect(cn({ foo: true }, { bar: true })).toBe('foo bar')
|
||||
expect(cn({ foo: true, bar: false }, { bar: true, baz: true })).toBe('foo bar baz')
|
||||
expect(cn({ 'p-4': true }, { 'p-2': true })).toBe('p-2')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests complex edge cases:
|
||||
* - Mixed falsy values
|
||||
* - Nested arrays with falsy values
|
||||
* - Multiple conflicting Tailwind classes
|
||||
*/
|
||||
test('complex edge cases', () => {
|
||||
expect(cn('foo', null, undefined, false, 'bar', 0, 1, '')).toBe('foo bar 1')
|
||||
expect(cn(['foo', null, ['bar', undefined, 'baz']])).toBe('foo bar baz')
|
||||
expect(cn('text-sm', { 'text-lg': false, 'text-xl': true }, 'text-2xl')).toBe('text-2xl')
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests important (!) modifier behavior:
|
||||
* - Important modifiers in objects
|
||||
* - Conflict resolution with important prefix
|
||||
*/
|
||||
test('important modifier with objects', () => {
|
||||
expect(cn({ '!font-medium': true }, { '!font-bold': true })).toBe('!font-bold')
|
||||
expect(cn('font-normal', { '!font-bold': true })).toBe('font-normal !font-bold')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,230 +0,0 @@
|
||||
import { mergeValidCompletionParams } from './completion-params'
|
||||
import type { FormValue, ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
describe('completion-params', () => {
|
||||
describe('mergeValidCompletionParams', () => {
|
||||
test('returns empty params and removedDetails for undefined oldParams', () => {
|
||||
const rules: ModelParameterRule[] = []
|
||||
const result = mergeValidCompletionParams(undefined, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('returns empty params and removedDetails for empty oldParams', () => {
|
||||
const rules: ModelParameterRule[] = []
|
||||
const result = mergeValidCompletionParams({}, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('validates int type parameter within range', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'max_tokens', type: 'int', min: 1, max: 4096, label: { en_US: 'Max Tokens', zh_Hans: '最大标记' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { max_tokens: 100 }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ max_tokens: 100 })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('removes int parameter below minimum', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'max_tokens', type: 'int', min: 1, max: 4096, label: { en_US: 'Max Tokens', zh_Hans: '最大标记' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { max_tokens: 0 }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({ max_tokens: 'out of range (1-4096)' })
|
||||
})
|
||||
|
||||
test('removes int parameter above maximum', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'max_tokens', type: 'int', min: 1, max: 4096, label: { en_US: 'Max Tokens', zh_Hans: '最大标记' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { max_tokens: 5000 }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({ max_tokens: 'out of range (1-4096)' })
|
||||
})
|
||||
|
||||
test('removes int parameter with invalid type', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'max_tokens', type: 'int', min: 1, max: 4096, label: { en_US: 'Max Tokens', zh_Hans: '最大标记' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { max_tokens: 'not a number' as any }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({ max_tokens: 'invalid type' })
|
||||
})
|
||||
|
||||
test('validates float type parameter', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'temperature', type: 'float', min: 0, max: 2, label: { en_US: 'Temperature', zh_Hans: '温度' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { temperature: 0.7 }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ temperature: 0.7 })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('validates float at boundary values', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'temperature', type: 'float', min: 0, max: 2, label: { en_US: 'Temperature', zh_Hans: '温度' }, required: false },
|
||||
]
|
||||
|
||||
const result1 = mergeValidCompletionParams({ temperature: 0 }, rules)
|
||||
expect(result1.params).toEqual({ temperature: 0 })
|
||||
|
||||
const result2 = mergeValidCompletionParams({ temperature: 2 }, rules)
|
||||
expect(result2.params).toEqual({ temperature: 2 })
|
||||
})
|
||||
|
||||
test('validates boolean type parameter', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'stream', type: 'boolean', label: { en_US: 'Stream', zh_Hans: '流' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { stream: true }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ stream: true })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('removes boolean parameter with invalid type', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'stream', type: 'boolean', label: { en_US: 'Stream', zh_Hans: '流' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { stream: 'yes' as any }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({ stream: 'invalid type' })
|
||||
})
|
||||
|
||||
test('validates string type parameter', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'model', type: 'string', label: { en_US: 'Model', zh_Hans: '模型' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { model: 'gpt-4' }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ model: 'gpt-4' })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('validates string parameter with options', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'model', type: 'string', options: ['gpt-3.5-turbo', 'gpt-4'], label: { en_US: 'Model', zh_Hans: '模型' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { model: 'gpt-4' }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ model: 'gpt-4' })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('removes string parameter with invalid option', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'model', type: 'string', options: ['gpt-3.5-turbo', 'gpt-4'], label: { en_US: 'Model', zh_Hans: '模型' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { model: 'invalid-model' }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({ model: 'unsupported option' })
|
||||
})
|
||||
|
||||
test('validates text type parameter', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'prompt', type: 'text', label: { en_US: 'Prompt', zh_Hans: '提示' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { prompt: 'Hello world' }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ prompt: 'Hello world' })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('removes unsupported parameters', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'temperature', type: 'float', min: 0, max: 2, label: { en_US: 'Temperature', zh_Hans: '温度' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { temperature: 0.7, unsupported_param: 'value' }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ temperature: 0.7 })
|
||||
expect(result.removedDetails).toEqual({ unsupported_param: 'unsupported' })
|
||||
})
|
||||
|
||||
test('keeps stop parameter in advanced mode even without rule', () => {
|
||||
const rules: ModelParameterRule[] = []
|
||||
const oldParams: FormValue = { stop: ['END'] }
|
||||
const result = mergeValidCompletionParams(oldParams, rules, true)
|
||||
|
||||
expect(result.params).toEqual({ stop: ['END'] })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('removes stop parameter in normal mode without rule', () => {
|
||||
const rules: ModelParameterRule[] = []
|
||||
const oldParams: FormValue = { stop: ['END'] }
|
||||
const result = mergeValidCompletionParams(oldParams, rules, false)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails).toEqual({ stop: 'unsupported' })
|
||||
})
|
||||
|
||||
test('handles multiple parameters with mixed validity', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'temperature', type: 'float', min: 0, max: 2, label: { en_US: 'Temperature', zh_Hans: '温度' }, required: false },
|
||||
{ name: 'max_tokens', type: 'int', min: 1, max: 4096, label: { en_US: 'Max Tokens', zh_Hans: '最大标记' }, required: false },
|
||||
{ name: 'model', type: 'string', options: ['gpt-4'], label: { en_US: 'Model', zh_Hans: '模型' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = {
|
||||
temperature: 0.7,
|
||||
max_tokens: 5000,
|
||||
model: 'gpt-4',
|
||||
unsupported: 'value',
|
||||
}
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({
|
||||
temperature: 0.7,
|
||||
model: 'gpt-4',
|
||||
})
|
||||
expect(result.removedDetails).toEqual({
|
||||
max_tokens: 'out of range (1-4096)',
|
||||
unsupported: 'unsupported',
|
||||
})
|
||||
})
|
||||
|
||||
test('handles parameters without min/max constraints', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'value', type: 'int', label: { en_US: 'Value', zh_Hans: '值' }, required: false },
|
||||
]
|
||||
const oldParams: FormValue = { value: 999999 }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({ value: 999999 })
|
||||
expect(result.removedDetails).toEqual({})
|
||||
})
|
||||
|
||||
test('removes parameter with unsupported rule type', () => {
|
||||
const rules: ModelParameterRule[] = [
|
||||
{ name: 'custom', type: 'unknown_type', label: { en_US: 'Custom', zh_Hans: '自定义' }, required: false } as any,
|
||||
]
|
||||
const oldParams: FormValue = { custom: 'value' }
|
||||
const result = mergeValidCompletionParams(oldParams, rules)
|
||||
|
||||
expect(result.params).toEqual({})
|
||||
expect(result.removedDetails.custom).toContain('unsupported rule type')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Test suite for icon utility functions
|
||||
* Tests the generation of marketplace plugin icon URLs
|
||||
*/
|
||||
import { getIconFromMarketPlace } from './get-icon'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
|
||||
describe('get-icon', () => {
|
||||
describe('getIconFromMarketPlace', () => {
|
||||
/**
|
||||
* Tests basic URL generation for marketplace plugin icons
|
||||
*/
|
||||
test('returns correct marketplace icon URL', () => {
|
||||
const pluginId = 'test-plugin-123'
|
||||
const result = getIconFromMarketPlace(pluginId)
|
||||
expect(result).toBe(`${MARKETPLACE_API_PREFIX}/plugins/${pluginId}/icon`)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests URL generation with plugin IDs containing special characters
|
||||
* like dashes and underscores
|
||||
*/
|
||||
test('handles plugin ID with special characters', () => {
|
||||
const pluginId = 'plugin-with-dashes_and_underscores'
|
||||
const result = getIconFromMarketPlace(pluginId)
|
||||
expect(result).toBe(`${MARKETPLACE_API_PREFIX}/plugins/${pluginId}/icon`)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests behavior with empty plugin ID
|
||||
* Note: This creates a malformed URL but doesn't throw an error
|
||||
*/
|
||||
test('handles empty plugin ID', () => {
|
||||
const pluginId = ''
|
||||
const result = getIconFromMarketPlace(pluginId)
|
||||
expect(result).toBe(`${MARKETPLACE_API_PREFIX}/plugins//icon`)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests URL generation with plugin IDs containing spaces
|
||||
* Spaces will be URL-encoded when actually used
|
||||
*/
|
||||
test('handles plugin ID with spaces', () => {
|
||||
const pluginId = 'plugin with spaces'
|
||||
const result = getIconFromMarketPlace(pluginId)
|
||||
expect(result).toBe(`${MARKETPLACE_API_PREFIX}/plugins/${pluginId}/icon`)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,88 +0,0 @@
|
||||
/**
|
||||
* Test suite for MCP (Model Context Protocol) utility functions
|
||||
* Tests icon detection logic for MCP-related features
|
||||
*/
|
||||
import { shouldUseMcpIcon, shouldUseMcpIconForAppIcon } from './mcp'
|
||||
|
||||
describe('mcp', () => {
|
||||
/**
|
||||
* Tests shouldUseMcpIcon function which determines if the MCP icon
|
||||
* should be used based on the icon source format
|
||||
*/
|
||||
describe('shouldUseMcpIcon', () => {
|
||||
/**
|
||||
* The link emoji (🔗) is used as a special marker for MCP icons
|
||||
*/
|
||||
test('returns true for emoji object with 🔗 content', () => {
|
||||
const src = { content: '🔗', background: '#fff' }
|
||||
expect(shouldUseMcpIcon(src)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for emoji object with different content', () => {
|
||||
const src = { content: '🎉', background: '#fff' }
|
||||
expect(shouldUseMcpIcon(src)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for string URL', () => {
|
||||
const src = 'https://example.com/icon.png'
|
||||
expect(shouldUseMcpIcon(src)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for null', () => {
|
||||
expect(shouldUseMcpIcon(null)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for undefined', () => {
|
||||
expect(shouldUseMcpIcon(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for empty object', () => {
|
||||
expect(shouldUseMcpIcon({})).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for object without content property', () => {
|
||||
const src = { background: '#fff' }
|
||||
expect(shouldUseMcpIcon(src)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for object with null content', () => {
|
||||
const src = { content: null, background: '#fff' }
|
||||
expect(shouldUseMcpIcon(src)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests shouldUseMcpIconForAppIcon function which checks if an app icon
|
||||
* should use the MCP icon based on icon type and content
|
||||
*/
|
||||
describe('shouldUseMcpIconForAppIcon', () => {
|
||||
/**
|
||||
* MCP icon should only be used when both conditions are met:
|
||||
* - Icon type is 'emoji'
|
||||
* - Icon content is the link emoji (🔗)
|
||||
*/
|
||||
test('returns true when iconType is emoji and icon is 🔗', () => {
|
||||
expect(shouldUseMcpIconForAppIcon('emoji', '🔗')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when iconType is emoji but icon is different', () => {
|
||||
expect(shouldUseMcpIconForAppIcon('emoji', '🎉')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when iconType is image', () => {
|
||||
expect(shouldUseMcpIconForAppIcon('image', '🔗')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when iconType is image and icon is different', () => {
|
||||
expect(shouldUseMcpIconForAppIcon('image', 'file-id-123')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for empty strings', () => {
|
||||
expect(shouldUseMcpIconForAppIcon('', '')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when iconType is empty but icon is 🔗', () => {
|
||||
expect(shouldUseMcpIconForAppIcon('', '🔗')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,297 +0,0 @@
|
||||
/**
|
||||
* Test suite for navigation utility functions
|
||||
* Tests URL and query parameter manipulation for consistent navigation behavior
|
||||
* Includes helpers for preserving state during navigation (pagination, filters, etc.)
|
||||
*/
|
||||
import {
|
||||
createBackNavigation,
|
||||
createNavigationPath,
|
||||
createNavigationPathWithParams,
|
||||
datasetNavigation,
|
||||
extractQueryParams,
|
||||
mergeQueryParams,
|
||||
} from './navigation'
|
||||
|
||||
describe('navigation', () => {
|
||||
const originalWindow = globalThis.window
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock window.location with sample query parameters
|
||||
delete (globalThis as any).window
|
||||
globalThis.window = {
|
||||
location: {
|
||||
search: '?page=3&limit=10&keyword=test',
|
||||
},
|
||||
} as any
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.window = originalWindow
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests createNavigationPath which builds URLs with optional query parameter preservation
|
||||
*/
|
||||
describe('createNavigationPath', () => {
|
||||
test('preserves query parameters by default', () => {
|
||||
const result = createNavigationPath('/datasets/123/documents')
|
||||
expect(result).toBe('/datasets/123/documents?page=3&limit=10&keyword=test')
|
||||
})
|
||||
|
||||
test('returns clean path when preserveParams is false', () => {
|
||||
const result = createNavigationPath('/datasets/123/documents', false)
|
||||
expect(result).toBe('/datasets/123/documents')
|
||||
})
|
||||
|
||||
test('handles empty query string', () => {
|
||||
globalThis.window.location.search = ''
|
||||
const result = createNavigationPath('/datasets/123/documents')
|
||||
expect(result).toBe('/datasets/123/documents')
|
||||
})
|
||||
|
||||
test('handles path with trailing slash', () => {
|
||||
const result = createNavigationPath('/datasets/123/documents/')
|
||||
expect(result).toBe('/datasets/123/documents/?page=3&limit=10&keyword=test')
|
||||
})
|
||||
|
||||
test('handles root path', () => {
|
||||
const result = createNavigationPath('/')
|
||||
expect(result).toBe('/?page=3&limit=10&keyword=test')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests createBackNavigation which creates a navigation callback function
|
||||
*/
|
||||
describe('createBackNavigation', () => {
|
||||
/**
|
||||
* Tests that the returned function properly navigates with preserved params
|
||||
*/
|
||||
test('returns function that calls router.push with correct path', () => {
|
||||
const mockRouter = { push: jest.fn() }
|
||||
const backNav = createBackNavigation(mockRouter, '/datasets/123/documents')
|
||||
|
||||
backNav()
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/datasets/123/documents?page=3&limit=10&keyword=test')
|
||||
})
|
||||
|
||||
test('returns function that navigates without params when preserveParams is false', () => {
|
||||
const mockRouter = { push: jest.fn() }
|
||||
const backNav = createBackNavigation(mockRouter, '/datasets/123/documents', false)
|
||||
|
||||
backNav()
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/datasets/123/documents')
|
||||
})
|
||||
|
||||
test('can be called multiple times', () => {
|
||||
const mockRouter = { push: jest.fn() }
|
||||
const backNav = createBackNavigation(mockRouter, '/datasets/123/documents')
|
||||
|
||||
backNav()
|
||||
backNav()
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests extractQueryParams which extracts specific parameters from current URL
|
||||
*/
|
||||
describe('extractQueryParams', () => {
|
||||
/**
|
||||
* Tests selective parameter extraction
|
||||
*/
|
||||
test('extracts specified parameters', () => {
|
||||
const result = extractQueryParams(['page', 'limit'])
|
||||
expect(result).toEqual({ page: '3', limit: '10' })
|
||||
})
|
||||
|
||||
test('extracts all specified parameters including keyword', () => {
|
||||
const result = extractQueryParams(['page', 'limit', 'keyword'])
|
||||
expect(result).toEqual({ page: '3', limit: '10', keyword: 'test' })
|
||||
})
|
||||
|
||||
test('ignores non-existent parameters', () => {
|
||||
const result = extractQueryParams(['page', 'nonexistent'])
|
||||
expect(result).toEqual({ page: '3' })
|
||||
})
|
||||
|
||||
test('returns empty object when no parameters match', () => {
|
||||
const result = extractQueryParams(['foo', 'bar'])
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
test('returns empty object for empty array', () => {
|
||||
const result = extractQueryParams([])
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
test('handles empty query string', () => {
|
||||
globalThis.window.location.search = ''
|
||||
const result = extractQueryParams(['page', 'limit'])
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests createNavigationPathWithParams which builds URLs with specific parameters
|
||||
*/
|
||||
describe('createNavigationPathWithParams', () => {
|
||||
/**
|
||||
* Tests URL construction with custom parameters
|
||||
*/
|
||||
test('creates path with specified parameters', () => {
|
||||
const result = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: '1',
|
||||
limit: '25',
|
||||
})
|
||||
expect(result).toBe('/datasets/123/documents?page=1&limit=25')
|
||||
})
|
||||
|
||||
test('handles string and number values', () => {
|
||||
const result = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: 1,
|
||||
limit: 25,
|
||||
keyword: 'search',
|
||||
})
|
||||
expect(result).toBe('/datasets/123/documents?page=1&limit=25&keyword=search')
|
||||
})
|
||||
|
||||
test('filters out empty string values', () => {
|
||||
const result = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: '1',
|
||||
keyword: '',
|
||||
})
|
||||
expect(result).toBe('/datasets/123/documents?page=1')
|
||||
})
|
||||
|
||||
test('filters out null and undefined values', () => {
|
||||
const result = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
page: '1',
|
||||
keyword: null as any,
|
||||
filter: undefined as any,
|
||||
})
|
||||
expect(result).toBe('/datasets/123/documents?page=1')
|
||||
})
|
||||
|
||||
test('returns base path when params are empty', () => {
|
||||
const result = createNavigationPathWithParams('/datasets/123/documents', {})
|
||||
expect(result).toBe('/datasets/123/documents')
|
||||
})
|
||||
|
||||
test('encodes special characters in values', () => {
|
||||
const result = createNavigationPathWithParams('/datasets/123/documents', {
|
||||
keyword: 'search term',
|
||||
})
|
||||
expect(result).toBe('/datasets/123/documents?keyword=search+term')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests mergeQueryParams which combines new parameters with existing URL params
|
||||
*/
|
||||
describe('mergeQueryParams', () => {
|
||||
/**
|
||||
* Tests parameter merging and overriding
|
||||
*/
|
||||
test('merges new params with existing ones', () => {
|
||||
const result = mergeQueryParams({ keyword: 'new', page: '1' })
|
||||
expect(result.get('page')).toBe('1')
|
||||
expect(result.get('limit')).toBe('10')
|
||||
expect(result.get('keyword')).toBe('new')
|
||||
})
|
||||
|
||||
test('overrides existing parameters', () => {
|
||||
const result = mergeQueryParams({ page: '5' })
|
||||
expect(result.get('page')).toBe('5')
|
||||
expect(result.get('limit')).toBe('10')
|
||||
})
|
||||
|
||||
test('adds new parameters', () => {
|
||||
const result = mergeQueryParams({ filter: 'active' })
|
||||
expect(result.get('filter')).toBe('active')
|
||||
expect(result.get('page')).toBe('3')
|
||||
})
|
||||
|
||||
test('removes parameters with null value', () => {
|
||||
const result = mergeQueryParams({ page: null })
|
||||
expect(result.get('page')).toBeNull()
|
||||
expect(result.get('limit')).toBe('10')
|
||||
})
|
||||
|
||||
test('removes parameters with undefined value', () => {
|
||||
const result = mergeQueryParams({ page: undefined })
|
||||
expect(result.get('page')).toBeNull()
|
||||
expect(result.get('limit')).toBe('10')
|
||||
})
|
||||
|
||||
test('does not preserve existing when preserveExisting is false', () => {
|
||||
const result = mergeQueryParams({ filter: 'active' }, false)
|
||||
expect(result.get('filter')).toBe('active')
|
||||
expect(result.get('page')).toBeNull()
|
||||
expect(result.get('limit')).toBeNull()
|
||||
})
|
||||
|
||||
test('handles number values', () => {
|
||||
const result = mergeQueryParams({ page: 5, limit: 20 })
|
||||
expect(result.get('page')).toBe('5')
|
||||
expect(result.get('limit')).toBe('20')
|
||||
})
|
||||
|
||||
test('does not add empty string values', () => {
|
||||
const result = mergeQueryParams({ newParam: '' })
|
||||
expect(result.get('newParam')).toBeNull()
|
||||
// Existing params are preserved
|
||||
expect(result.get('keyword')).toBe('test')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests datasetNavigation helper object with common dataset navigation patterns
|
||||
*/
|
||||
describe('datasetNavigation', () => {
|
||||
/**
|
||||
* Tests navigation back to dataset documents list
|
||||
*/
|
||||
describe('backToDocuments', () => {
|
||||
test('creates navigation function with preserved params', () => {
|
||||
const mockRouter = { push: jest.fn() }
|
||||
const backNav = datasetNavigation.backToDocuments(mockRouter, 'dataset-123')
|
||||
|
||||
backNav()
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/datasets/dataset-123/documents?page=3&limit=10&keyword=test')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests navigation to document detail page
|
||||
*/
|
||||
describe('toDocumentDetail', () => {
|
||||
test('creates navigation function to document detail', () => {
|
||||
const mockRouter = { push: jest.fn() }
|
||||
const navFunc = datasetNavigation.toDocumentDetail(mockRouter, 'dataset-123', 'doc-456')
|
||||
|
||||
navFunc()
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/datasets/dataset-123/documents/doc-456')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests navigation to document settings page
|
||||
*/
|
||||
describe('toDocumentSettings', () => {
|
||||
test('creates navigation function to document settings', () => {
|
||||
const mockRouter = { push: jest.fn() }
|
||||
const navFunc = datasetNavigation.toDocumentSettings(mockRouter, 'dataset-123', 'doc-456')
|
||||
|
||||
navFunc()
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/datasets/dataset-123/documents/doc-456/settings')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Test suite for permission utility functions
|
||||
* Tests dataset edit permission logic based on user roles and dataset settings
|
||||
*/
|
||||
import { hasEditPermissionForDataset } from './permission'
|
||||
import { DatasetPermission } from '@/models/datasets'
|
||||
|
||||
describe('permission', () => {
|
||||
/**
|
||||
* Tests hasEditPermissionForDataset which checks if a user can edit a dataset
|
||||
* Based on three permission levels:
|
||||
* - onlyMe: Only the creator can edit
|
||||
* - allTeamMembers: All team members can edit
|
||||
* - partialMembers: Only specified members can edit
|
||||
*/
|
||||
describe('hasEditPermissionForDataset', () => {
|
||||
const userId = 'user-123'
|
||||
const creatorId = 'creator-456'
|
||||
const otherUserId = 'user-789'
|
||||
|
||||
test('returns true when permission is onlyMe and user is creator', () => {
|
||||
const config = {
|
||||
createdBy: userId,
|
||||
partialMemberList: [],
|
||||
permission: DatasetPermission.onlyMe,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(userId, config)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when permission is onlyMe and user is not creator', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [],
|
||||
permission: DatasetPermission.onlyMe,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(userId, config)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when permission is allTeamMembers for any user', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [],
|
||||
permission: DatasetPermission.allTeamMembers,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(userId, config)).toBe(true)
|
||||
expect(hasEditPermissionForDataset(otherUserId, config)).toBe(true)
|
||||
expect(hasEditPermissionForDataset(creatorId, config)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true when permission is partialMembers and user is in list', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [userId, otherUserId],
|
||||
permission: DatasetPermission.partialMembers,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(userId, config)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when permission is partialMembers and user is not in list', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [otherUserId],
|
||||
permission: DatasetPermission.partialMembers,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(userId, config)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when permission is partialMembers with empty list', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [],
|
||||
permission: DatasetPermission.partialMembers,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(userId, config)).toBe(false)
|
||||
})
|
||||
|
||||
test('creator is not automatically granted access with partialMembers permission', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [userId],
|
||||
permission: DatasetPermission.partialMembers,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(creatorId, config)).toBe(false)
|
||||
})
|
||||
|
||||
test('creator has access when included in partialMemberList', () => {
|
||||
const config = {
|
||||
createdBy: creatorId,
|
||||
partialMemberList: [creatorId, userId],
|
||||
permission: DatasetPermission.partialMembers,
|
||||
}
|
||||
expect(hasEditPermissionForDataset(creatorId, config)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Test suite for time utility functions
|
||||
* Tests date comparison and formatting using dayjs
|
||||
*/
|
||||
import { formatTime, isAfter } from './time'
|
||||
|
||||
describe('time', () => {
|
||||
/**
|
||||
* Tests isAfter function which compares two dates
|
||||
* Returns true if the first date is after the second
|
||||
*/
|
||||
describe('isAfter', () => {
|
||||
test('returns true when first date is after second date', () => {
|
||||
const date1 = '2024-01-02'
|
||||
const date2 = '2024-01-01'
|
||||
expect(isAfter(date1, date2)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when first date is before second date', () => {
|
||||
const date1 = '2024-01-01'
|
||||
const date2 = '2024-01-02'
|
||||
expect(isAfter(date1, date2)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when dates are equal', () => {
|
||||
const date = '2024-01-01'
|
||||
expect(isAfter(date, date)).toBe(false)
|
||||
})
|
||||
|
||||
test('works with Date objects', () => {
|
||||
const date1 = new Date('2024-01-02')
|
||||
const date2 = new Date('2024-01-01')
|
||||
expect(isAfter(date1, date2)).toBe(true)
|
||||
})
|
||||
|
||||
test('works with timestamps', () => {
|
||||
const date1 = 1704240000000 // 2024-01-03
|
||||
const date2 = 1704153600000 // 2024-01-02
|
||||
expect(isAfter(date1, date2)).toBe(true)
|
||||
})
|
||||
|
||||
test('handles time differences within same day', () => {
|
||||
const date1 = '2024-01-01 12:00:00'
|
||||
const date2 = '2024-01-01 11:00:00'
|
||||
expect(isAfter(date1, date2)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests formatTime function which formats dates using dayjs
|
||||
* Supports various date formats and input types
|
||||
*/
|
||||
describe('formatTime', () => {
|
||||
/**
|
||||
* Tests basic date formatting with standard format
|
||||
*/
|
||||
test('formats date with YYYY-MM-DD format', () => {
|
||||
const date = '2024-01-15'
|
||||
const result = formatTime({ date, dateFormat: 'YYYY-MM-DD' })
|
||||
expect(result).toBe('2024-01-15')
|
||||
})
|
||||
|
||||
test('formats date with custom format', () => {
|
||||
const date = '2024-01-15 14:30:00'
|
||||
const result = formatTime({ date, dateFormat: 'MMM DD, YYYY HH:mm' })
|
||||
expect(result).toBe('Jan 15, 2024 14:30')
|
||||
})
|
||||
|
||||
test('formats date with full month name', () => {
|
||||
const date = '2024-01-15'
|
||||
const result = formatTime({ date, dateFormat: 'MMMM DD, YYYY' })
|
||||
expect(result).toBe('January 15, 2024')
|
||||
})
|
||||
|
||||
test('formats date with time only', () => {
|
||||
const date = '2024-01-15 14:30:45'
|
||||
const result = formatTime({ date, dateFormat: 'HH:mm:ss' })
|
||||
expect(result).toBe('14:30:45')
|
||||
})
|
||||
|
||||
test('works with Date objects', () => {
|
||||
const date = new Date(2024, 0, 15) // Month is 0-indexed
|
||||
const result = formatTime({ date, dateFormat: 'YYYY-MM-DD' })
|
||||
expect(result).toBe('2024-01-15')
|
||||
})
|
||||
|
||||
test('works with timestamps', () => {
|
||||
const date = 1705276800000 // 2024-01-15 00:00:00 UTC
|
||||
const result = formatTime({ date, dateFormat: 'YYYY-MM-DD' })
|
||||
expect(result).toContain('2024-01-1') // Account for timezone differences
|
||||
})
|
||||
|
||||
test('handles ISO 8601 format', () => {
|
||||
const date = '2024-01-15T14:30:00Z'
|
||||
const result = formatTime({ date, dateFormat: 'YYYY-MM-DD HH:mm' })
|
||||
expect(result).toContain('2024-01-15')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Test suite for tool call utility functions
|
||||
* Tests detection of function/tool call support in AI models
|
||||
*/
|
||||
import { supportFunctionCall } from './tool-call'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
describe('tool-call', () => {
|
||||
/**
|
||||
* Tests supportFunctionCall which checks if a model supports any form of
|
||||
* function calling (toolCall, multiToolCall, or streamToolCall)
|
||||
*/
|
||||
describe('supportFunctionCall', () => {
|
||||
/**
|
||||
* Tests detection of basic tool call support
|
||||
*/
|
||||
test('returns true when features include toolCall', () => {
|
||||
const features = [ModelFeatureEnum.toolCall]
|
||||
expect(supportFunctionCall(features)).toBe(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests detection of multi-tool call support (calling multiple tools in one request)
|
||||
*/
|
||||
test('returns true when features include multiToolCall', () => {
|
||||
const features = [ModelFeatureEnum.multiToolCall]
|
||||
expect(supportFunctionCall(features)).toBe(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests detection of streaming tool call support
|
||||
*/
|
||||
test('returns true when features include streamToolCall', () => {
|
||||
const features = [ModelFeatureEnum.streamToolCall]
|
||||
expect(supportFunctionCall(features)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true when features include multiple tool call types', () => {
|
||||
const features = [
|
||||
ModelFeatureEnum.toolCall,
|
||||
ModelFeatureEnum.multiToolCall,
|
||||
ModelFeatureEnum.streamToolCall,
|
||||
]
|
||||
expect(supportFunctionCall(features)).toBe(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests that tool call support is detected even when mixed with other features
|
||||
*/
|
||||
test('returns true when features include tool call among other features', () => {
|
||||
const features = [
|
||||
ModelFeatureEnum.agentThought,
|
||||
ModelFeatureEnum.toolCall,
|
||||
ModelFeatureEnum.vision,
|
||||
]
|
||||
expect(supportFunctionCall(features)).toBe(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests that false is returned when no tool call features are present
|
||||
*/
|
||||
test('returns false when features do not include any tool call type', () => {
|
||||
const features = [ModelFeatureEnum.agentThought, ModelFeatureEnum.vision]
|
||||
expect(supportFunctionCall(features)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for empty array', () => {
|
||||
expect(supportFunctionCall([])).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for undefined', () => {
|
||||
expect(supportFunctionCall(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for null', () => {
|
||||
expect(supportFunctionCall(null as any)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user