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:
Cyber 2026-06-26 17:57:31 +08:00 committed by GitHub
parent e22fd9efd6
commit 051f4b32e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 532 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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))

View File

@ -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")

View File

@ -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="")

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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

View File

@ -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(

View File

@ -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):

View File

@ -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"

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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(),

View File

@ -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()

View File

@ -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}

View File

@ -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'],

View File

@ -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')

View File

@ -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>

View File

@ -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}

View File

@ -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.",

View File

@ -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>.",

View File

@ -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>.",

View File

@ -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.",

View File

@ -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>.",

View File

@ -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.",

View File

@ -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>।",

View File

@ -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>.",

View File

@ -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.",

View File

@ -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>を参照できるようにします。",

View File

@ -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>을 참조할 수 있도록 합니다.",

View File

@ -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>.",

View File

@ -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>.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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>。",

View File

@ -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>。",

View File

@ -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
}

View File

@ -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