mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 10:38:32 +08:00
feat(web): add customizable input placeholder for Agent/Chatflow/Chatbot web app (#37790)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
e22fd9efd6
commit
051f4b32e3
@ -183,6 +183,7 @@ class Site(BaseModel):
|
||||
description: str | None = None
|
||||
copyright: str | None = None
|
||||
privacy_policy: str | None = None
|
||||
input_placeholder: str | None = None
|
||||
custom_disclaimer: str | None = None
|
||||
default_language: str
|
||||
show_workflow_steps: bool
|
||||
|
||||
@ -345,6 +345,7 @@ class Site(ResponseModel):
|
||||
customize_domain: str | None = None
|
||||
copyright: str | None = None
|
||||
privacy_policy: str | None = None
|
||||
input_placeholder: str | None = None
|
||||
custom_disclaimer: str | None = None
|
||||
customize_token_strategy: str | None = None
|
||||
prompt_public: bool | None = None
|
||||
|
||||
@ -40,6 +40,7 @@ class AppSiteUpdatePayload(BaseModel):
|
||||
customize_domain: str | None = Field(default=None)
|
||||
copyright: str | None = Field(default=None)
|
||||
privacy_policy: str | None = Field(default=None)
|
||||
input_placeholder: str | None = Field(default=None)
|
||||
custom_disclaimer: str | None = Field(default=None)
|
||||
customize_token_strategy: Literal["must", "allow", "not_allow"] | None = Field(default=None)
|
||||
prompt_public: bool | None = Field(default=None)
|
||||
@ -66,6 +67,7 @@ class AppSiteResponse(ResponseModel):
|
||||
customize_domain: str | None = None
|
||||
copyright: str | None = None
|
||||
privacy_policy: str | None = None
|
||||
input_placeholder: str | None = None
|
||||
custom_disclaimer: str | None = None
|
||||
customize_token_strategy: str
|
||||
prompt_public: bool
|
||||
@ -110,6 +112,7 @@ class AppSite(Resource):
|
||||
"customize_domain",
|
||||
"copyright",
|
||||
"privacy_policy",
|
||||
"input_placeholder",
|
||||
"custom_disclaimer",
|
||||
"customize_token_strategy",
|
||||
"prompt_public",
|
||||
|
||||
@ -14,7 +14,7 @@ from fields.base import ResponseModel
|
||||
from libs.helper import AppIconUrlField
|
||||
from models.account import TenantStatus
|
||||
from models.model import App, EndUser, Site
|
||||
from services.feature_service import FeatureService
|
||||
from services.feature_service import FeatureModel, FeatureService
|
||||
|
||||
|
||||
class AppSiteModelConfigResponse(ResponseModel):
|
||||
@ -38,6 +38,7 @@ class AppSiteResponse(ResponseModel):
|
||||
description: str | None = None
|
||||
copyright: str | None = None
|
||||
privacy_policy: str | None = None
|
||||
input_placeholder: str | None = None
|
||||
custom_disclaimer: str | None = None
|
||||
default_language: str | None = None
|
||||
prompt_public: bool | None = None
|
||||
@ -84,6 +85,7 @@ class AppSiteApi(WebApiResource):
|
||||
"description": fields.String,
|
||||
"copyright": fields.String,
|
||||
"privacy_policy": fields.String,
|
||||
"input_placeholder": fields.String,
|
||||
"custom_disclaimer": fields.String,
|
||||
"default_language": fields.String,
|
||||
"prompt_public": fields.Boolean,
|
||||
@ -127,9 +129,15 @@ class AppSiteApi(WebApiResource):
|
||||
if app_model.tenant and app_model.tenant.status == TenantStatus.ARCHIVE:
|
||||
raise Forbidden()
|
||||
|
||||
can_replace_logo = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True).can_replace_logo
|
||||
features = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True)
|
||||
|
||||
return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, can_replace_logo)
|
||||
return AppSiteInfo(
|
||||
app_model.tenant,
|
||||
app_model,
|
||||
serialize_runtime_site(site, features),
|
||||
end_user.id,
|
||||
features.can_replace_logo,
|
||||
)
|
||||
|
||||
|
||||
class AppSiteInfo:
|
||||
@ -164,7 +172,23 @@ def serialize_site(site: Site) -> dict[str, Any]:
|
||||
return cast(dict[str, Any], marshal(site, AppSiteApi.site_fields))
|
||||
|
||||
|
||||
def serialize_runtime_site(site: Site, features: FeatureModel) -> dict[str, Any]:
|
||||
site_payload = serialize_site(site)
|
||||
if not features.billing.enabled or features.webapp_copyright_enabled:
|
||||
return site_payload
|
||||
|
||||
site_payload["copyright"] = None
|
||||
site_payload["input_placeholder"] = None
|
||||
return site_payload
|
||||
|
||||
|
||||
def serialize_app_site_payload(app_model: App, site: Site, end_user_id: str | None) -> dict[str, Any]:
|
||||
can_replace_logo = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True).can_replace_logo
|
||||
app_site_info = AppSiteInfo(app_model.tenant, app_model, site, end_user_id, can_replace_logo)
|
||||
features = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True)
|
||||
app_site_info = AppSiteInfo(
|
||||
app_model.tenant,
|
||||
app_model,
|
||||
serialize_runtime_site(site, features),
|
||||
end_user_id,
|
||||
features.can_replace_logo,
|
||||
)
|
||||
return cast(dict[str, Any], marshal(app_site_info, AppSiteApi.app_fields))
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
"""add input placeholder to sites
|
||||
|
||||
Revision ID: a6f1c9d2e8b4
|
||||
Revises: d9e8f7a6b5c4
|
||||
Create Date: 2026-06-24 19:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a6f1c9d2e8b4"
|
||||
down_revision = "d9e8f7a6b5c4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("sites", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("input_placeholder", sa.String(length=255), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("sites", schema=None) as batch_op:
|
||||
batch_op.drop_column("input_placeholder")
|
||||
@ -2183,6 +2183,7 @@ class Site(Base):
|
||||
chat_color_theme_inverted: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
|
||||
copyright = mapped_column(String(255))
|
||||
privacy_policy = mapped_column(String(255))
|
||||
input_placeholder = mapped_column(String(255))
|
||||
show_workflow_steps: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
|
||||
use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
|
||||
_custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", LongText, default="")
|
||||
|
||||
@ -13925,6 +13925,7 @@ AppMCPServer Status Enum
|
||||
| description | string | | No |
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| prompt_public | boolean | | Yes |
|
||||
| show_workflow_steps | boolean | | Yes |
|
||||
@ -13952,6 +13953,7 @@ AppMCPServer Status Enum
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| prompt_public | boolean | | No |
|
||||
| show_workflow_steps | boolean | | No |
|
||||
@ -19265,6 +19267,7 @@ Simple provider entity response.
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | Yes |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| show_workflow_steps | boolean | | Yes |
|
||||
| title | string | | Yes |
|
||||
|
||||
@ -3999,6 +3999,7 @@ Model class for provider with models response.
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | Yes |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| show_workflow_steps | boolean | | Yes |
|
||||
| title | string | | Yes |
|
||||
|
||||
@ -1006,6 +1006,7 @@ Returns Server-Sent Events stream.
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | No |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| prompt_public | boolean | | No |
|
||||
| show_workflow_steps | boolean | | No |
|
||||
|
||||
@ -343,8 +343,13 @@ class TestSiteEndpoints:
|
||||
return flask_app_with_containers
|
||||
|
||||
def test_site_response_structure(self):
|
||||
payload = AppSiteUpdatePayload(title="My Site", description="Test site")
|
||||
payload = AppSiteUpdatePayload(
|
||||
title="My Site",
|
||||
description="Test site",
|
||||
input_placeholder="Ask me anything",
|
||||
)
|
||||
assert payload.title == "My Site"
|
||||
assert payload.input_placeholder == "Ask me anything"
|
||||
|
||||
def test_site_default_language_validation(self):
|
||||
payload = AppSiteUpdatePayload(default_language="en-US")
|
||||
@ -365,6 +370,7 @@ class TestSiteEndpoints:
|
||||
site.customize_domain = None
|
||||
site.copyright = None
|
||||
site.privacy_policy = None
|
||||
site.input_placeholder = None
|
||||
site.custom_disclaimer = ""
|
||||
site.customize_token_strategy = "not_allow"
|
||||
site.prompt_public = False
|
||||
@ -377,11 +383,13 @@ class TestSiteEndpoints:
|
||||
)
|
||||
monkeypatch.setattr(site_module, "naive_utc_now", lambda: "now")
|
||||
|
||||
with app.test_request_context("/", json={"title": "My Site"}):
|
||||
with app.test_request_context("/", json={"title": "My Site", "input_placeholder": "Ask me anything"}):
|
||||
result = method(api, SimpleNamespace(id="u1"), app_model=SimpleNamespace(id="app-1"))
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["title"] == "My Site"
|
||||
assert result["input_placeholder"] == "Ask me anything"
|
||||
assert site.input_placeholder == "Ask me anything"
|
||||
|
||||
def test_app_site_access_token_reset(self, app: Flask, monkeypatch: pytest.MonkeyPatch):
|
||||
api = site_module.AppSiteAccessTokenReset()
|
||||
@ -398,6 +406,7 @@ class TestSiteEndpoints:
|
||||
site.customize_domain = None
|
||||
site.copyright = None
|
||||
site.privacy_policy = None
|
||||
site.input_placeholder = None
|
||||
site.custom_disclaimer = ""
|
||||
site.customize_token_strategy = "not_allow"
|
||||
site.prompt_public = False
|
||||
|
||||
@ -222,7 +222,7 @@ def test_get_human_input_form_resolves_runtime_select_options(
|
||||
)
|
||||
|
||||
def mock_get_features(tenant_id: str, exclude_vector_space: bool = False) -> FeatureModel:
|
||||
features = FeatureModel(can_replace_logo=True)
|
||||
features = FeatureModel(can_replace_logo=True, webapp_copyright_enabled=True)
|
||||
return features
|
||||
|
||||
monkeypatch.setattr(
|
||||
|
||||
@ -13,6 +13,7 @@ from werkzeug.exceptions import Forbidden
|
||||
from controllers.web.site import AppSiteApi, AppSiteInfo
|
||||
from models import Tenant, TenantStatus
|
||||
from models.model import App, AppMode, CustomizeTokenStrategy, Site
|
||||
from services.feature_service import FeatureModel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -51,6 +52,7 @@ def _create_site(db_session: Session, app_id: str) -> Site:
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
input_placeholder="Ask the app",
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
code=f"code-{app_id[-6:]}",
|
||||
prompt_public=False,
|
||||
@ -70,7 +72,7 @@ class TestAppSiteApi:
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
_create_site(db_session_with_containers, app_model.id)
|
||||
end_user = SimpleNamespace(id="eu-1")
|
||||
mock_features.return_value = SimpleNamespace(can_replace_logo=False)
|
||||
mock_features.return_value = FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True)
|
||||
|
||||
with app.test_request_context("/site"):
|
||||
result = AppSiteApi().get(app_model, end_user)
|
||||
@ -78,6 +80,7 @@ class TestAppSiteApi:
|
||||
assert result["app_id"] == app_model.id
|
||||
assert result["plan"] == "basic"
|
||||
assert result["enable_site"] is True
|
||||
assert result["site"]["input_placeholder"] == "Ask the app"
|
||||
|
||||
def test_missing_site_raises_forbidden(self, app: Flask, db_session_with_containers: Session) -> None:
|
||||
app.config["RESTX_MASK_HEADER"] = "X-Fields"
|
||||
@ -98,7 +101,7 @@ class TestAppSiteApi:
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
_create_site(db_session_with_containers, app_model.id)
|
||||
end_user = SimpleNamespace(id="eu-1")
|
||||
mock_features.return_value = SimpleNamespace(can_replace_logo=False)
|
||||
mock_features.return_value = FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True)
|
||||
|
||||
with app.test_request_context("/site"):
|
||||
with pytest.raises(Forbidden):
|
||||
|
||||
@ -379,6 +379,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
title="Public Site",
|
||||
icon_type="image",
|
||||
icon="site-icon",
|
||||
input_placeholder="Ask anything",
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
@ -421,6 +422,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models):
|
||||
assert serialized["model_config"]["retriever_resource"] == {"enabled": True}
|
||||
assert serialized["deleted_tools"][0]["tool_name"] == "search"
|
||||
assert serialized["site"]["icon_url"] == "signed:site-icon"
|
||||
assert serialized["site"]["input_placeholder"] == "Ask anything"
|
||||
assert serialized["site"]["created_at"] == int(timestamp.timestamp())
|
||||
assert serialized["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
|
||||
assert serialized["bound_agent_id"] == "agent-1"
|
||||
|
||||
@ -112,6 +112,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
chat_color_theme_inverted=False,
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
@ -132,7 +133,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
monkeypatch.setattr(
|
||||
site_module.FeatureService,
|
||||
"get_features",
|
||||
lambda tenant_id, **_kwargs: SimpleNamespace(can_replace_logo=True),
|
||||
lambda tenant_id, **_kwargs: FeatureModel(can_replace_logo=True, webapp_copyright_enabled=True),
|
||||
)
|
||||
|
||||
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
|
||||
@ -167,6 +168,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
"description": "desc",
|
||||
"copyright": None,
|
||||
"privacy_policy": None,
|
||||
"input_placeholder": "Ask me anything",
|
||||
"custom_disclaimer": None,
|
||||
"default_language": "en",
|
||||
"prompt_public": False,
|
||||
@ -186,6 +188,72 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
limiter_mock.increment_rate_limit.assert_called_once_with("203.0.113.10")
|
||||
|
||||
|
||||
def test_serialize_app_site_payload_masks_paid_webapp_fields_when_feature_disabled(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Runtime site payload hides paid-only fields for plans that cannot use them."""
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", plan="sandbox", custom_config_dict={})
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant, enable_site=True)
|
||||
site_model = SimpleNamespace(
|
||||
title="My Site",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright="Dify",
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
)
|
||||
features = FeatureModel(can_replace_logo=False, webapp_copyright_enabled=False)
|
||||
features.billing.enabled = True
|
||||
monkeypatch.setattr(site_module.FeatureService, "get_features", lambda tenant_id, **_kwargs: features)
|
||||
|
||||
payload = site_module.serialize_app_site_payload(app_model, site_model, end_user_id=None)
|
||||
|
||||
assert payload["site"]["copyright"] is None
|
||||
assert payload["site"]["input_placeholder"] is None
|
||||
|
||||
|
||||
def test_serialize_app_site_payload_keeps_paid_webapp_fields_when_billing_disabled(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Self-hosted community runtimes keep site fields because billing is not enforcing the paid gate."""
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", plan="basic", custom_config_dict={})
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant, enable_site=True)
|
||||
site_model = SimpleNamespace(
|
||||
title="My Site",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright="Dify",
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
site_module.FeatureService,
|
||||
"get_features",
|
||||
lambda tenant_id, **_kwargs: FeatureModel(can_replace_logo=False, webapp_copyright_enabled=False),
|
||||
)
|
||||
|
||||
payload = site_module.serialize_app_site_payload(app_model, site_model, end_user_id=None)
|
||||
|
||||
assert payload["site"]["copyright"] == "Dify"
|
||||
assert payload["site"]["input_placeholder"] == "Ask me anything"
|
||||
|
||||
|
||||
def test_get_form_uses_runtime_select_options(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
"""GET returns variable-backed select options resolved from runtime state."""
|
||||
|
||||
@ -256,6 +324,7 @@ def test_get_form_uses_runtime_select_options(monkeypatch: pytest.MonkeyPatch, a
|
||||
chat_color_theme_inverted=False,
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
@ -380,6 +449,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
|
||||
chat_color_theme_inverted=False,
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
@ -397,7 +467,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
|
||||
monkeypatch.setattr(
|
||||
site_module.FeatureService,
|
||||
"get_features",
|
||||
lambda tenant_id, **_kwargs: SimpleNamespace(can_replace_logo=True),
|
||||
lambda tenant_id, **_kwargs: FeatureModel(can_replace_logo=True, webapp_copyright_enabled=True),
|
||||
)
|
||||
|
||||
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
|
||||
@ -432,6 +502,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
|
||||
"description": "desc",
|
||||
"copyright": None,
|
||||
"privacy_policy": None,
|
||||
"input_placeholder": "Ask me anything",
|
||||
"custom_disclaimer": None,
|
||||
"default_language": "en",
|
||||
"prompt_public": False,
|
||||
|
||||
@ -891,14 +891,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/settings/index.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/workflow-hidden-input-fields.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -424,6 +424,7 @@ export type Site = {
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
readonly icon_url: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
@ -1560,6 +1561,7 @@ export type SiteWritable = {
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
|
||||
@ -203,6 +203,7 @@ export const zSite = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullable(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
@ -2126,6 +2127,7 @@ export const zSiteWritable = z.object({
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
|
||||
@ -580,6 +580,7 @@ export type AppSiteUpdatePayload = {
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
prompt_public?: boolean | null
|
||||
show_workflow_steps?: boolean | null
|
||||
@ -598,6 +599,7 @@ export type AppSiteResponse = {
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
prompt_public: boolean
|
||||
show_workflow_steps: boolean
|
||||
@ -1242,6 +1244,7 @@ export type Site = {
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
readonly icon_url: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
@ -2691,6 +2694,7 @@ export type SiteWritable = {
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
|
||||
@ -358,6 +358,7 @@ export const zAppSiteUpdatePayload = z.object({
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
prompt_public: z.boolean().nullish(),
|
||||
show_workflow_steps: z.boolean().nullish(),
|
||||
@ -379,6 +380,7 @@ export const zAppSiteResponse = z.object({
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
prompt_public: z.boolean(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
@ -858,6 +860,7 @@ export const zSite = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullable(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
@ -3546,6 +3549,7 @@ export const zSiteWritable = z.object({
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
|
||||
@ -96,6 +96,7 @@ export type Site = {
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
readonly icon_url: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
@ -346,6 +347,7 @@ export type SiteWritable = {
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
|
||||
@ -53,6 +53,7 @@ export const zSite = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullable(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
@ -430,6 +431,7 @@ export const zSiteWritable = z.object({
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
|
||||
@ -1463,6 +1463,7 @@ export type Site = {
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
readonly icon_url: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
@ -1689,6 +1690,7 @@ export type SiteWritable = {
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
show_workflow_steps: boolean
|
||||
title: string
|
||||
|
||||
@ -1752,6 +1752,7 @@ export const zSite = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullable(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
@ -2311,6 +2312,7 @@ export const zSiteWritable = z.object({
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
show_workflow_steps: z.boolean(),
|
||||
title: z.string(),
|
||||
|
||||
@ -80,6 +80,7 @@ export type AppSiteResponse = {
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
icon_url?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
prompt_public?: boolean | null
|
||||
show_workflow_steps?: boolean | null
|
||||
|
||||
@ -73,6 +73,7 @@ export const zAppSiteResponse = z.object({
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
prompt_public: z.boolean().nullish(),
|
||||
show_workflow_steps: z.boolean().nullish(),
|
||||
|
||||
@ -53,14 +53,6 @@ const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockUseProviderContext = vi.fn<() => ProviderContextState>()
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
const buildModalContext = (): ModalContextState => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
@ -109,6 +101,7 @@ const mockAppInfo = {
|
||||
copyright: '© Dify',
|
||||
privacy_policy: '',
|
||||
custom_disclaimer: 'Disclaimer',
|
||||
input_placeholder: 'Ask me anything',
|
||||
default_language: 'en-US',
|
||||
show_workflow_steps: true,
|
||||
use_icon_as_answer_icon: true,
|
||||
@ -127,6 +120,8 @@ const renderSettingsModal = (appInfo = mockAppInfo) => render(
|
||||
/>,
|
||||
)
|
||||
|
||||
const inputPlaceholderName = 'appOverview.overview.appInfo.settings.more.inputPlaceholder'
|
||||
|
||||
describe('SettingsModal', () => {
|
||||
beforeEach(() => {
|
||||
toastMocks.call.mockClear()
|
||||
@ -139,7 +134,7 @@ describe('SettingsModal', () => {
|
||||
enableBilling: true,
|
||||
plan: {
|
||||
...baseProviderContextValue.plan,
|
||||
type: Plan.sandbox,
|
||||
type: Plan.professional,
|
||||
},
|
||||
webappCopyrightEnabled: true,
|
||||
})
|
||||
@ -157,6 +152,7 @@ describe('SettingsModal', () => {
|
||||
fireEvent.click(showMoreEntry)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.copyRightPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
@ -221,6 +217,7 @@ describe('SettingsModal', () => {
|
||||
copyright: mockAppInfo.site.copyright,
|
||||
privacy_policy: mockAppInfo.site.privacy_policy,
|
||||
custom_disclaimer: mockAppInfo.site.custom_disclaimer,
|
||||
input_placeholder: mockAppInfo.site.input_placeholder,
|
||||
icon_type: 'emoji',
|
||||
icon: mockAppInfo.site.icon,
|
||||
icon_background: mockAppInfo.site.icon_background,
|
||||
@ -278,11 +275,115 @@ describe('SettingsModal', () => {
|
||||
expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open the pricing modal from the copyright upgrade badge for sandbox plans', async () => {
|
||||
it('should reset the input placeholder when app info changes while open', () => {
|
||||
const { rerender } = render(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={true}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toHaveValue('Ask me anything')
|
||||
|
||||
rerender(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={true}
|
||||
appInfo={{
|
||||
...mockAppInfo,
|
||||
site: {
|
||||
...mockAppInfo.site,
|
||||
input_placeholder: 'Updated prompt',
|
||||
},
|
||||
} as typeof mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toHaveValue('Updated prompt')
|
||||
})
|
||||
|
||||
it('should display paid webapp settings as defaults for Cloud sandbox plans', async () => {
|
||||
mockOnSave.mockResolvedValueOnce(undefined)
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
...baseProviderContextValue,
|
||||
enableBilling: true,
|
||||
plan: {
|
||||
...baseProviderContextValue.plan,
|
||||
type: Plan.sandbox,
|
||||
},
|
||||
webappCopyrightEnabled: true,
|
||||
})
|
||||
|
||||
renderSettingsModal()
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
fireEvent.click(await screen.findByText('billing.upgradeBtn.encourageShort'))
|
||||
|
||||
const inputPlaceholder = screen.getByRole('textbox', { name: inputPlaceholderName })
|
||||
expect(inputPlaceholder).toBeDisabled()
|
||||
expect(inputPlaceholder).toHaveValue('')
|
||||
expect(screen.queryByPlaceholderText('appOverview.overview.appInfo.settings.more.copyRightPlaceholder')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
copyright: '',
|
||||
input_placeholder: '',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep the input placeholder editable when billing is disabled', async () => {
|
||||
mockOnSave.mockResolvedValueOnce(undefined)
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
...baseProviderContextValue,
|
||||
enableBilling: false,
|
||||
plan: {
|
||||
...baseProviderContextValue.plan,
|
||||
type: Plan.sandbox,
|
||||
},
|
||||
webappCopyrightEnabled: false,
|
||||
})
|
||||
|
||||
renderSettingsModal()
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
const inputPlaceholder = screen.getByRole('textbox', { name: inputPlaceholderName })
|
||||
fireEvent.change(inputPlaceholder, { target: { value: 'Self-hosted prompt' } })
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
expect(inputPlaceholder).toBeEnabled()
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
copyright: '',
|
||||
input_placeholder: 'Self-hosted prompt',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should open the pricing modal from the copyright upgrade badge for sandbox plans', async () => {
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
...baseProviderContextValue,
|
||||
enableBilling: true,
|
||||
plan: {
|
||||
...baseProviderContextValue.plan,
|
||||
type: Plan.sandbox,
|
||||
},
|
||||
webappCopyrightEnabled: false,
|
||||
})
|
||||
|
||||
renderSettingsModal()
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
fireEvent.click((await screen.findAllByText('billing.upgradeBtn.encourageShort'))[0]!)
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalled()
|
||||
|
||||
@ -19,8 +19,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { PremiumBadgeButton } from '@/app/components/base/premium-badge'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
@ -46,6 +45,7 @@ export type ConfigParams = {
|
||||
copyright: string
|
||||
privacy_policy: string
|
||||
custom_disclaimer: string
|
||||
input_placeholder: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string
|
||||
@ -54,6 +54,13 @@ export type ConfigParams = {
|
||||
enable_sso?: boolean
|
||||
}
|
||||
|
||||
const INPUT_PLACEHOLDER_MAX_LENGTH = 64
|
||||
const INPUT_PLACEHOLDER_SUPPORTED_MODES: ReadonlyArray<AppModeEnum> = [
|
||||
AppModeEnum.CHAT,
|
||||
AppModeEnum.AGENT_CHAT,
|
||||
AppModeEnum.ADVANCED_CHAT,
|
||||
]
|
||||
|
||||
const prefixSettings = 'overview.appInfo.settings'
|
||||
type SelectOption = {
|
||||
value: Language
|
||||
@ -71,6 +78,7 @@ const createInputInfo = (appInfo: ISettingsModalProps['appInfo']) => {
|
||||
copyright,
|
||||
privacy_policy,
|
||||
custom_disclaimer,
|
||||
input_placeholder,
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
} = appInfo.site
|
||||
@ -84,6 +92,7 @@ const createInputInfo = (appInfo: ISettingsModalProps['appInfo']) => {
|
||||
copyrightSwitchValue: !!copyright,
|
||||
privacyPolicy: privacy_policy,
|
||||
customDisclaimer: custom_disclaimer,
|
||||
inputPlaceholder: input_placeholder ?? '',
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
enable_sso: appInfo.enable_sso,
|
||||
@ -108,6 +117,7 @@ const getSettingsResetKey = (appInfo: ISettingsModalProps['appInfo']) => JSON.st
|
||||
appInfo.site.copyright,
|
||||
appInfo.site.privacy_policy,
|
||||
appInfo.site.custom_disclaimer,
|
||||
appInfo.site.input_placeholder,
|
||||
appInfo.site.default_language,
|
||||
appInfo.site.icon_type,
|
||||
appInfo.site.icon,
|
||||
@ -125,6 +135,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const [inputPlaceholderFocused, setInputPlaceholderFocused] = useState(false)
|
||||
const { default_language } = appInfo.site
|
||||
const nextInputInfo = createInputInfo(appInfo)
|
||||
const nextAppIcon = createAppIcon(appInfo)
|
||||
@ -140,10 +151,51 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
const [previousSettingsResetKey, setPreviousSettingsResetKey] = useState(settingsResetKey)
|
||||
|
||||
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
|
||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||
const isFreePlan = plan.type === 'sandbox'
|
||||
const showUpgradeAction = IS_CLOUD_EDITION && enableBilling && isFreePlan
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
const isCloudSandboxPlan = enableBilling && plan.type === Plan.sandbox
|
||||
const selectedLanguage = LANGUAGE_OPTIONS.find(item => item.value === language)
|
||||
const inputPlaceholderLabelId = React.useId()
|
||||
const inputPlaceholderDescriptionId = React.useId()
|
||||
|
||||
const inputPlaceholderValue = isCloudSandboxPlan ? '' : (inputInfo.inputPlaceholder ?? '')
|
||||
const copyrightSwitchValue = isCloudSandboxPlan ? false : inputInfo.copyrightSwitchValue
|
||||
// Editable + has value + blurred -> preview as gray placeholder text (matches how it'll render in chat).
|
||||
const showInputPlaceholderPreview = !isCloudSandboxPlan && inputPlaceholderValue.trim().length > 0 && !inputPlaceholderFocused
|
||||
const inputPlaceholderField = (
|
||||
<div
|
||||
className={cn(
|
||||
'mt-2 flex h-10 items-center gap-2 rounded-lg border border-components-input-border-hover bg-components-input-bg-normal pr-1 pl-3 transition-colors',
|
||||
!isCloudSandboxPlan && inputPlaceholderFocused && 'border-components-input-border-active bg-components-input-bg-active',
|
||||
isCloudSandboxPlan && 'cursor-not-allowed opacity-60',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="input_placeholder"
|
||||
value={inputPlaceholderValue}
|
||||
onChange={e => setInputInfo(item => ({ ...item, inputPlaceholder: e.target.value }))}
|
||||
onFocus={() => setInputPlaceholderFocused(true)}
|
||||
onBlur={() => setInputPlaceholderFocused(false)}
|
||||
disabled={isCloudSandboxPlan}
|
||||
maxLength={INPUT_PLACEHOLDER_MAX_LENGTH}
|
||||
autoComplete="off"
|
||||
aria-labelledby={inputPlaceholderLabelId}
|
||||
aria-describedby={inputPlaceholderDescriptionId}
|
||||
placeholder={t(`${prefixSettings}.more.inputPlaceholderPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
className={cn(
|
||||
'flex-1 bg-transparent body-md-regular outline-hidden',
|
||||
showInputPlaceholderPreview ? 'text-text-placeholder' : 'text-text-primary',
|
||||
isCloudSandboxPlan && 'cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="grid h-7 w-7 shrink-0 cursor-not-allowed place-items-center rounded-md bg-components-button-primary-bg opacity-50"
|
||||
>
|
||||
<span className="i-custom-vender-solid-communication-send-03 h-4 w-4 text-components-button-primary-text" />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const handleLanguageChange = (nextValue: string | null) => {
|
||||
const nextLanguage = LANGUAGE_OPTIONS.find(item => item.value === nextValue)
|
||||
@ -151,11 +203,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
setLanguage(nextLanguage.value)
|
||||
}
|
||||
const handlePlanClick = useCallback(() => {
|
||||
if (isFreePlan)
|
||||
setShowPricingModal()
|
||||
else
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
}, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
|
||||
setShowPricingModal()
|
||||
}, [setShowPricingModal])
|
||||
|
||||
const shouldResetForm = isShow && (!previousIsShow || settingsResetKey !== previousSettingsResetKey)
|
||||
if (isShow !== previousIsShow || shouldResetForm) {
|
||||
@ -215,13 +264,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
chat_color_theme: inputInfo.chatColorTheme,
|
||||
chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
|
||||
prompt_public: false,
|
||||
copyright: !webappCopyrightEnabled
|
||||
copyright: (!webappCopyrightEnabled || isCloudSandboxPlan)
|
||||
? ''
|
||||
: inputInfo.copyrightSwitchValue
|
||||
: copyrightSwitchValue
|
||||
? inputInfo.copyright
|
||||
: '',
|
||||
privacy_policy: inputInfo.privacyPolicy,
|
||||
custom_disclaimer: inputInfo.customDisclaimer,
|
||||
input_placeholder: (isCloudSandboxPlan || !INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode))
|
||||
? ''
|
||||
: (inputInfo.inputPlaceholder ?? '').slice(0, INPUT_PLACEHOLDER_MAX_LENGTH),
|
||||
icon_type: appIcon.type,
|
||||
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
|
||||
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
||||
@ -373,7 +425,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
{/* more settings switch */}
|
||||
<Divider className="my-0 h-px" />
|
||||
{!isShowMore && (
|
||||
<div className="flex cursor-pointer items-center" onClick={() => setIsShowMore(true)}>
|
||||
<button type="button" className="flex w-full cursor-pointer items-center text-left" onClick={() => setIsShowMore(true)}>
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
@ -385,18 +437,56 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden="true" className="ml-1 i-ri-arrow-right-s-line size-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{/* more settings */}
|
||||
{isShowMore && (
|
||||
<>
|
||||
{/* input placeholder — Chatbot / Agent / Chatflow only */}
|
||||
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div id={inputPlaceholderLabelId} className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.inputPlaceholder`, { ns: 'appOverview' })}</div>
|
||||
{isCloudSandboxPlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
|
||||
<span aria-hidden="true" className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadgeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p id={inputPlaceholderDescriptionId} className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.inputPlaceholderTip`, { ns: 'appOverview' })}</p>
|
||||
{isCloudSandboxPlan
|
||||
? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={inputPlaceholderField} />
|
||||
<TooltipContent className="w-[180px]">
|
||||
{t(`${prefixSettings}.more.inputPlaceholderTooltip`, { ns: 'appOverview' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
: inputPlaceholderField}
|
||||
{!isCloudSandboxPlan && (
|
||||
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
|
||||
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
{/* upgrade button */}
|
||||
{showUpgradeAction && (
|
||||
{isCloudSandboxPlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
|
||||
<span aria-hidden="true" className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
@ -412,7 +502,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
{webappCopyrightEnabled
|
||||
? (
|
||||
<Switch
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
checked={copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
)
|
||||
@ -423,7 +513,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<div>
|
||||
<Switch
|
||||
disabled
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
checked={copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
</div>
|
||||
@ -436,7 +526,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
{inputInfo.copyrightSwitchValue && (
|
||||
{copyrightSwitchValue && (
|
||||
<Input
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
|
||||
@ -42,9 +42,18 @@ vi.mock('../question', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../chat-input-area', () => ({
|
||||
default: ({ disabled, readonly }: { disabled?: boolean, readonly?: boolean }) => (
|
||||
default: ({
|
||||
customPlaceholder,
|
||||
disabled,
|
||||
readonly,
|
||||
}: {
|
||||
customPlaceholder?: string
|
||||
disabled?: boolean
|
||||
readonly?: boolean
|
||||
}) => (
|
||||
<div
|
||||
data-testid="chat-input-area"
|
||||
data-custom-placeholder={customPlaceholder}
|
||||
data-disabled={String(!!disabled)}
|
||||
data-readonly={String(!!readonly)}
|
||||
/>
|
||||
@ -828,6 +837,14 @@ describe('Chat', () => {
|
||||
expect(screen.getByTestId('chat-input-area')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass appData.site.input_placeholder as customPlaceholder to ChatInputArea', () => {
|
||||
renderChat({
|
||||
appData: { site: { input_placeholder: 'Ask the assistant' } } as unknown as ChatProps['appData'],
|
||||
noChatInput: false,
|
||||
})
|
||||
expect(screen.getByTestId('chat-input-area')).toHaveAttribute('data-custom-placeholder', 'Ask the assistant')
|
||||
})
|
||||
|
||||
it('should pass Bot as default botName when appData.site.title is missing', () => {
|
||||
renderChat({
|
||||
appData: {} as unknown as ChatProps['appData'],
|
||||
|
||||
@ -299,6 +299,21 @@ describe('ChatInputArea', () => {
|
||||
expect(getTextarea()!).toHaveAttribute('placeholder', expect.stringContaining('botName'))
|
||||
})
|
||||
|
||||
it('should render the custom placeholder when provided', () => {
|
||||
render(<ChatInputArea visionConfig={mockVisionConfig} customPlaceholder="Ask the assistant" />)
|
||||
expect(screen.getByPlaceholderText('Ask the assistant')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should fall back to the readonly placeholder when readonly has a custom placeholder', () => {
|
||||
render(<ChatInputArea visionConfig={mockVisionConfig} customPlaceholder="Ask the assistant" readonly />)
|
||||
expect(screen.getByPlaceholderText(/inputDisabledPlaceholder/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should fall back to the default placeholder when custom placeholder is blank', () => {
|
||||
render(<ChatInputArea visionConfig={mockVisionConfig} customPlaceholder=" " />)
|
||||
expect(getTextarea()!).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply disabled styles when the disabled prop is true', () => {
|
||||
const { container } = render(<ChatInputArea visionConfig={mockVisionConfig} disabled />)
|
||||
expect(container.firstChild).toHaveClass('opacity-50')
|
||||
|
||||
@ -23,6 +23,7 @@ import Operation from './operation'
|
||||
type ChatInputAreaProps = {
|
||||
readonly?: boolean
|
||||
botName?: string
|
||||
customPlaceholder?: string
|
||||
showFeatureBar?: boolean
|
||||
showFileUpload?: boolean
|
||||
featureBarReadonly?: boolean
|
||||
@ -44,7 +45,7 @@ type ChatInputAreaProps = {
|
||||
*/
|
||||
sendOnEnter?: boolean
|
||||
}
|
||||
const ChatInputArea = ({ readonly, botName, showFeatureBar, showFileUpload, featureBarReadonly = readonly, featureBarDisabled, onFeatureBarClick, visionConfig, speechToTextConfig = { enabled: true }, onSend, inputs = {}, inputsForm = [], theme, isResponding, disabled, sendOnEnter = true }: ChatInputAreaProps) => {
|
||||
const ChatInputArea = ({ readonly, botName, customPlaceholder, showFeatureBar, showFileUpload, featureBarReadonly = readonly, featureBarDisabled, onFeatureBarClick, visionConfig, speechToTextConfig = { enabled: true }, onSend, inputs = {}, inputsForm = [], theme, isResponding, disabled, sendOnEnter = true }: ChatInputAreaProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { wrapperRef, textareaRef, textValueRef, holdSpaceRef, handleTextareaResize, isMultipleLine } = useTextAreaHeight()
|
||||
const [query, setQuery] = useState('')
|
||||
@ -147,7 +148,7 @@ const ChatInputArea = ({ readonly, botName, showFeatureBar, showFileUpload, feat
|
||||
<div ref={textValueRef} className="pointer-events-none invisible absolute size-auto p-1 body-lg-regular leading-6 whitespace-pre">
|
||||
{query}
|
||||
</div>
|
||||
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-text-primary outline-hidden')} placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
|
||||
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-text-primary outline-hidden')} placeholder={!readonly && customPlaceholder?.trim() ? customPlaceholder : decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
|
||||
</div>
|
||||
{!isMultipleLine && operation}
|
||||
</div>
|
||||
|
||||
@ -241,6 +241,7 @@ const Chat: FC<ChatProps> = ({
|
||||
!noChatInput && (
|
||||
<ChatInputArea
|
||||
botName={appData?.site?.title || 'Bot'}
|
||||
customPlaceholder={appData?.site?.input_placeholder}
|
||||
disabled={inputDisabled}
|
||||
showFeatureBar={showFeatureBar}
|
||||
showFileUpload={showFileUpload}
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "أدخل نص إخلاء المسؤولية المخصص",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "سيتم عرض نص إخلاء المسؤولية المخصص على جانب العميل، مما يوفر معلومات إضافية حول التطبيق",
|
||||
"overview.appInfo.settings.more.entry": "عرض المزيد من الإعدادات",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "نص العنصر النائب للإدخال",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "تحدث إلى الروبوت",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "خصص نص العنصر النائب لصندوق إدخال الدردشة. اتركه فارغًا لاستخدام القيمة الافتراضية.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "قم بالترقية لتخصيص نص العنصر النائب",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "سياسة الخصوصية",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "أدخل رابط سياسة الخصوصية",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "يساعد الزوار على فهم البيانات التي يجمعها التطبيق، راجع <privacyPolicyLink>سياسة الخصوصية</privacyPolicyLink> لـ Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Geben Sie den benutzerdefinierten Haftungsausschluss-Text ein",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Der ben userdefinierte Haftungsausschluss-Text wird auf der Clientseite angezeigt und bietet zusätzliche Informationen über die Anwendung",
|
||||
"overview.appInfo.settings.more.entry": "Mehr Einstellungen anzeigen",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Eingabe-Platzhalter",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Mit dem Bot sprechen",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Passen Sie den Platzhaltertext für das Chat-Eingabefeld an. Leer lassen, um den Standard zu verwenden.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Upgrade durchführen, um den Platzhalter anzupassen",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Datenschutzrichtlinie",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Geben Sie den Link zur Datenschutzrichtlinie ein",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Hilft Besuchern zu verstehen, welche Daten die Anwendung sammelt, siehe Difys <privacyPolicyLink>Datenschutzrichtlinie</privacyPolicyLink>.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Enter the custom disclaimer text",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Custom disclaimer text will be displayed on the client side, providing additional information about the application",
|
||||
"overview.appInfo.settings.more.entry": "Show more settings",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Input placeholder",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Talk to bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Customize your own placeholder text for the chat input box. Leave empty to use the default.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Upgrade to customize the placeholder",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Privacy Policy",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Enter the privacy policy link",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Helps visitors understand the data the application collects, see Dify's <privacyPolicyLink>Privacy Policy</privacyPolicyLink>.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Ingresa el texto de descargo de responsabilidad personalizado",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "El texto de descargo de responsabilidad personalizado se mostrará en el lado del cliente, proporcionando información adicional sobre la aplicación",
|
||||
"overview.appInfo.settings.more.entry": "Mostrar más configuraciones",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Marcador de posición de entrada",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Habla con el bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Personaliza el texto del marcador de posición del cuadro de entrada del chat. Déjalo vacío para usar el valor predeterminado.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Actualiza para personalizar el marcador de posición",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Política de privacidad",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Ingresa el enlace de la política de privacidad",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Ayuda a los visitantes a comprender los datos que recopila la aplicación, consulta la <privacyPolicyLink>Política de privacidad</privacyPolicyLink> de Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "متن سلب مسئولیت سفارشی را وارد کنید",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "متن سلب مسئولیت سفارشی در سمت مشتری نمایش داده میشود و اطلاعات بیشتری درباره برنامه ارائه میدهد",
|
||||
"overview.appInfo.settings.more.entry": "نمایش تنظیمات بیشتر",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "متن راهنمای ورودی",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "با ربات گفتگو کنید",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "متن راهنمای کادر ورودی چت را سفارشی کنید. برای استفاده از مقدار پیشفرض، آن را خالی بگذارید.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "برای سفارشیسازی متن راهنما ارتقا دهید",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "سیاست حفظ حریم خصوصی",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "لینک سیاست حفظ حریم خصوصی را وارد کنید",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "به بازدیدکنندگان کمک میکند تا بفهمند برنامه چه دادههایی را جمعآوری میکند، به سیاست حفظ حریم خصوصی Dify نگاه کنید <privacyPolicyLink>Privacy Policy</privacyPolicyLink>.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Entrez le texte de la clause de non-responsabilité personnalisée",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Le texte de la clause de non-responsabilité personnalisée sera affiché côté client, fournissant des informations supplémentaires sur l'application",
|
||||
"overview.appInfo.settings.more.entry": "Afficher plus de paramètres",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Texte indicatif de saisie",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Discuter avec le bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Personnalisez le texte indicatif du champ de saisie du chat. Laissez vide pour utiliser la valeur par défaut.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Passez à une offre supérieure pour personnaliser le texte indicatif",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Politique de confidentialité",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Entrez le lien de la politique de confidentialité",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Aide les visiteurs à comprendre les données collectées par l'application, voir la <privacyPolicyLink>Politique de confidentialité</privacyPolicyLink> de Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "कस्टम अस्वीकरण टेक्स्ट दर्ज करें",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "कस्टम अस्वीकरण टेक्स्ट क्लाइंट साइड पर प्रदर्शित होगा, जो एप्लिकेशन के बारे में अतिरिक्त जानकारी प्रदान करेगा",
|
||||
"overview.appInfo.settings.more.entry": "अधिक सेटिंग्स दिखाएं",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "इनपुट प्लेसहोल्डर",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "बॉट से बात करें",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "चैट इनपुट बॉक्स के लिए अपना प्लेसहोल्डर टेक्स्ट कस्टमाइज़ करें। डिफ़ॉल्ट का उपयोग करने के लिए इसे खाली छोड़ें।",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "प्लेसहोल्डर कस्टमाइज़ करने के लिए अपग्रेड करें",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "गोपनीयता नीति",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "गोपनीयता नीति लिंक दर्ज करें",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "आगंतुकों को यह समझने में मदद करता है कि एप्लिकेशन कौन सा डेटा एकत्र करता है, देखें Dify की <privacyPolicyLink>गोपनीयता नीति</privacyPolicyLink>।",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Masukkan teks penafian khusus",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Teks penafian khusus akan ditampilkan di sisi klien, memberikan informasi tambahan tentang aplikasi",
|
||||
"overview.appInfo.settings.more.entry": "Tampilkan setelan lainnya",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Placeholder input",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Bicara dengan bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Sesuaikan teks placeholder untuk kotak input chat. Kosongkan untuk menggunakan bawaan.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Upgrade untuk menyesuaikan placeholder",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Kebijakan Privasi",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Masukkan tautan kebijakan privasi",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Membantu pengunjung memahami data yang dikumpulkan aplikasi, lihat <privacyPolicyLink>Kebijakan Privasi Dify</privacyPolicyLink>.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Inserisci il testo del disclaimer personalizzato",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Il testo del disclaimer personalizzato verrà visualizzato sul lato client, fornendo informazioni aggiuntive sull'applicazione",
|
||||
"overview.appInfo.settings.more.entry": "Mostra più impostazioni",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Segnaposto di input",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Parla con il bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Personalizza il testo segnaposto del campo di input della chat. Lascia vuoto per usare il valore predefinito.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Esegui l'upgrade per personalizzare il segnaposto",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Privacy Policy",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Inserisci il link alla privacy policy",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Aiuta i visitatori a capire i dati raccolti dall'applicazione, vedi la <privacyPolicyLink>Privacy Policy</privacyPolicyLink> di Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "免責事項を入力してください",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "アプリケーションの使用に関する免責事項を提供します。",
|
||||
"overview.appInfo.settings.more.entry": "その他の設定を表示",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "入力欄のプレースホルダー",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "ボットと話す",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "チャット入力欄のプレースホルダーをカスタマイズできます。空欄の場合はデフォルトを使用します。",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "アップグレードするとプレースホルダーをカスタマイズできます",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "プライバシーポリシー",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "プライバシーポリシーリンクを入力してください",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "訪問者がアプリケーションが収集するデータを理解し、Dify の<privacyPolicyLink>プライバシーポリシー</privacyPolicyLink>を参照できるようにします。",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "사용자 지정 면책 조항 텍스트를 입력합니다.",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "사용자 지정 고지 사항 텍스트는 클라이언트 쪽에 표시되어 응용 프로그램에 대한 추가 정보를 제공합니다",
|
||||
"overview.appInfo.settings.more.entry": "추가 설정 보기",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "입력 플레이스홀더",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "봇과 대화하기",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "채팅 입력창의 플레이스홀더 텍스트를 사용자 지정합니다. 비워 두면 기본값을 사용합니다.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "플레이스홀더를 사용자 지정하려면 업그레이드하세요",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "개인정보 처리방침",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "개인정보 처리방침 링크를 입력하세요",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "방문자가 애플리케이션이 수집하는 데이터를 이해하고, Dify 의 <privacyPolicyLink>개인정보 처리방침</privacyPolicyLink>을 참조할 수 있도록 합니다.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Enter the custom disclaimer text",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Custom disclaimer text will be displayed on the client side, providing additional information about the application",
|
||||
"overview.appInfo.settings.more.entry": "Show more settings",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Invoerplaceholder",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Praat met de bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Pas de placeholdertekst voor het chatinvoerveld aan. Laat leeg om de standaardwaarde te gebruiken.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Upgrade om de placeholder aan te passen",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Privacy Policy",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Enter the privacy policy link",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Helps visitors understand the data the application collects, see Dify's <privacyPolicyLink>Privacy Policy</privacyPolicyLink>.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Wprowadź oświadczenie o ochronie danych",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Niestandardowy tekst oświadczenia będzie wyświetlany po stronie klienta, dostarczając dodatkowych informacji o aplikacji.",
|
||||
"overview.appInfo.settings.more.entry": "Pokaż więcej ustawień",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Symbol zastępczy pola wejściowego",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Porozmawiaj z botem",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Dostosuj tekst zastępczy pola wprowadzania czatu. Pozostaw puste, aby użyć wartości domyślnej.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Uaktualnij, aby dostosować tekst zastępczy",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Polityka prywatności",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Wprowadź link do polityki prywatności",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Pomaga odwiedzającym zrozumieć, jakie dane zbiera aplikacja, zobacz <privacyPolicyLink>Politykę prywatności Dify</privacyPolicyLink>.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Insira o texto do aviso legal",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "O texto do aviso legal personalizado será exibido no lado do cliente, fornecendo informações adicionais sobre o aplicativo",
|
||||
"overview.appInfo.settings.more.entry": "Mostrar mais configurações",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Placeholder de entrada",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Fale com o bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Personalize o texto de placeholder da caixa de entrada do chat. Deixe em branco para usar o padrão.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Faça upgrade para personalizar o placeholder",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Política de Privacidade",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Insira o link da política de privacidade",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Ajuda os visitantes a entender os dados coletados pelo aplicativo, consulte a <privacyPolicyLink>Política de Privacidade</privacyPolicyLink> do Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Introduceți textul personalizat de declinare a responsabilității",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Textul personalizat de declinare a responsabilității va fi afișat pe partea clientului, oferind informații suplimentare despre aplicație",
|
||||
"overview.appInfo.settings.more.entry": "Afișați mai multe setări",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Substituent pentru introducere",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Vorbește cu botul",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Personalizează textul substituent pentru caseta de introducere a chatului. Lasă gol pentru a folosi valoarea implicită.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Fă upgrade pentru a personaliza substituentul",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Politica de confidențialitate",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Introduceți link-ul politicii de confidențialitate",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Ajută vizitatorii să înțeleagă datele pe care le colectează aplicația, consultați <privacyPolicyLink>Politica de confidențialitate</privacyPolicyLink> a Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Введите текст пользовательского отказа от ответственности",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Текст пользовательского отказа от ответственности будет отображаться на стороне клиента, предоставляя дополнительную информацию о приложении",
|
||||
"overview.appInfo.settings.more.entry": "Показать больше настроек",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Подсказка поля ввода",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Поговорить с ботом",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Настройте текст подсказки для поля ввода чата. Оставьте пустым, чтобы использовать значение по умолчанию.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Обновите тариф, чтобы настроить подсказку",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Политика конфиденциальности",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Введите ссылку на политику конфиденциальности",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Помогает посетителям понять, какие данные собирает приложение, см. <privacyPolicyLink>Политику конфиденциальности</privacyPolicyLink> Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Vnesite prilagojeno izjavo o omejitvi odgovornosti",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Prilagojeno izjavo o omejitvi odgovornosti bo prikazano na strani za stranke, ki bo zagotavljala dodatne informacije o aplikaciji",
|
||||
"overview.appInfo.settings.more.entry": "Prikaži več nastavitev",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Nadomestno besedilo vnosa",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Pogovorite se z botom",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Prilagodite nadomestno besedilo za vnosno polje klepeta. Pustite prazno za privzeto vrednost.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Nadgradite, da prilagodite nadomestno besedilo",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Politika zasebnosti",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Vnesite povezavo do politike zasebnosti",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Pomaga obiskovalcem razumeti, katere podatke aplikacija zbira, glejte <privacyPolicyLink>politiko zasebnosti</privacyPolicyLink> Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "ป้อนข้อความข้อจํากัดความรับผิดชอบที่กําหนดเอง",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "ข้อความปฏิเสธความรับผิดชอบที่กําหนดเองจะแสดงที่ฝั่งไคลเอ็นต์ โดยให้ข้อมูลเพิ่มเติมเกี่ยวกับแอปพลิเคชัน",
|
||||
"overview.appInfo.settings.more.entry": "แสดงการตั้งค่าเพิ่มเติม",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "ข้อความตัวอย่างในช่องป้อนข้อมูล",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "คุยกับบอต",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "ปรับแต่งข้อความตัวอย่างสำหรับช่องป้อนข้อความแชท เว้นว่างไว้เพื่อใช้ค่าเริ่มต้น",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "อัปเกรดเพื่อปรับแต่งข้อความตัวอย่าง",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "นโยบายความเป็นส่วนตัว",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "ป้อนลิงก์นโยบายความเป็นส่วนตัว",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "ช่วยให้ผู้เยี่ยมชมเข้าใจข้อมูลที่แอปพลิเคชันรวบรวม โปรดดูนโยบาย<privacyPolicyLink>ความเป็นส่วนตัว</privacyPolicyLink>ของ Dify",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Özel ifşa metnini girin",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Özel ifşa metni istemci tarafında görüntülenecek ve uygulama hakkında ek bilgiler sağlayacak",
|
||||
"overview.appInfo.settings.more.entry": "Daha fazla ayarı göster",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Giriş yer tutucusu",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Botla konuş",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Sohbet giriş kutusu için yer tutucu metni özelleştirin. Varsayılanı kullanmak için boş bırakın.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Yer tutucuyu özelleştirmek için yükseltin",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Gizlilik Politikası",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Gizlilik politikası bağlantısını girin",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Ziyaretçilerin uygulamanın topladığı verileri anlamalarına yardımcı olur, Dify'nin <privacyPolicyLink>Gizlilik Politikası</privacyPolicyLink>'na bakın.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Введіть відмову від відповідальності",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Відображається на клієнтському боці, щоб визначити відповідальність за використання додатка",
|
||||
"overview.appInfo.settings.more.entry": "Показати додаткові налаштування",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Підказка поля введення",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Поговорити з ботом",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Налаштуйте текст підказки для поля введення чату. Залиште порожнім, щоб використовувати значення за замовчуванням.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Оновіть тариф, щоб налаштувати підказку",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Політика конфіденційності",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Введіть посилання на політику конфіденційності",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Допомагає відвідувачам зрозуміти дані, зібрані додатком, див. <privacyPolicyLink>Політику конфіденційності</privacyPolicyLink> Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "Nhập liên kết tuyên bố từ chối trách nhiệm",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "Liên kết này sẽ hiển thị ở phía người dùng, cung cấp thông tin về trách nhiệm của ứng dụng",
|
||||
"overview.appInfo.settings.more.entry": "Hiển thị thêm cài đặt",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "Văn bản gợi ý nhập liệu",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "Trò chuyện với bot",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "Tùy chỉnh văn bản gợi ý cho ô nhập chat. Để trống để dùng giá trị mặc định.",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "Nâng cấp để tùy chỉnh văn bản gợi ý",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "Chính sách bảo mật",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "Nhập liên kết chính sách bảo mật",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "Giúp người dùng hiểu dữ liệu mà ứng dụng thu thập, xem <privacyPolicyLink>Chính sách bảo mật</privacyPolicyLink> của Dify.",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "请输入免责声明",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "在应用中展示免责声明,可用于告知用户 AI 的局限性。",
|
||||
"overview.appInfo.settings.more.entry": "展示更多设置",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "输入框提示语",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "与机器人对话",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "自定义聊天输入框的提示语。留空则使用默认值。",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "升级后可自定义提示语",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "隐私政策",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "请输入隐私政策链接",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "帮助访问者了解该应用收集的数据,可参考 Dify 的<privacyPolicyLink>隐私政策</privacyPolicyLink>。",
|
||||
|
||||
@ -88,6 +88,10 @@
|
||||
"overview.appInfo.settings.more.customDisclaimerPlaceholder": "請輸入免責聲明",
|
||||
"overview.appInfo.settings.more.customDisclaimerTip": "客製化的免責聲明文字將在客戶端顯示,提供有關應用程式的額外資訊。",
|
||||
"overview.appInfo.settings.more.entry": "展示更多設定",
|
||||
"overview.appInfo.settings.more.inputPlaceholder": "輸入框提示語",
|
||||
"overview.appInfo.settings.more.inputPlaceholderPlaceholder": "與機器人對話",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTip": "自訂聊天輸入框的提示語。留空則使用預設值。",
|
||||
"overview.appInfo.settings.more.inputPlaceholderTooltip": "升級後可自訂提示語",
|
||||
"overview.appInfo.settings.more.privacyPolicy": "隱私政策",
|
||||
"overview.appInfo.settings.more.privacyPolicyPlaceholder": "請輸入隱私政策連結",
|
||||
"overview.appInfo.settings.more.privacyPolicyTip": "幫助訪問者瞭解該應用收集的資料,可參考 Dify 的<privacyPolicyLink>隱私政策</privacyPolicyLink>。",
|
||||
|
||||
@ -22,6 +22,7 @@ export type SiteInfo = {
|
||||
copyright?: string
|
||||
privacy_policy?: string
|
||||
custom_disclaimer?: string
|
||||
input_placeholder?: string
|
||||
show_workflow_steps?: boolean
|
||||
use_icon_as_answer_icon?: boolean
|
||||
}
|
||||
|
||||
@ -302,6 +302,8 @@ export type SiteConfig = {
|
||||
privacy_policy: string
|
||||
/** Custom Disclaimer */
|
||||
custom_disclaimer: string
|
||||
/** Custom placeholder text for the chat input box. Empty means fall back to the default. */
|
||||
input_placeholder: string
|
||||
|
||||
icon_type: AppIconType | null
|
||||
icon: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user