feat(web): refine web app customization options (#38650)

This commit is contained in:
无能的迪拜巧克力 2026-07-10 16:35:26 +08:00 committed by GitHub
parent 3c8e0e2113
commit 775ed70bac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 230 additions and 214 deletions

View File

@ -155,26 +155,26 @@ Then('I should see the Agent v2 Embedded configuration dialog', async function (
})
When('I open Agent v2 Web app customization', async function (this: DifyWorld) {
await getWebAppCard(this).getByRole('button', { name: 'Customize' }).click()
await getWebAppCard(this).getByRole('button', { name: 'Custom Frontend' }).click()
})
Then('I should see the Agent v2 Web app customization dialog', async function (this: DifyWorld) {
const dialog = getDialog(this, 'Customize AI web app')
const dialog = getDialog(this, 'Custom Frontend')
await expect(dialog).toBeVisible()
await expect(dialog.getByText('Customize AI web app')).toBeVisible()
await expect(dialog.getByText('Custom Frontend')).toBeVisible()
await expect(dialog.getByText(/NEXT_PUBLIC_APP_ID|NEXT_PUBLIC_API_URL/)).toBeVisible()
})
When('I open Agent v2 Web app settings', async function (this: DifyWorld) {
await getWebAppCard(this).getByRole('button', { name: 'Settings' }).click()
await getWebAppCard(this).getByRole('button', { name: 'Branding' }).click()
})
Then('I should see the Agent v2 Web app settings dialog', async function (this: DifyWorld) {
const dialog = getDialog(this, 'Web App Settings')
const dialog = getDialog(this, 'Branding')
await expect(dialog).toBeVisible()
await expect(dialog.getByRole('heading', { name: 'Web App Settings' })).toBeVisible()
await expect(dialog.getByRole('heading', { name: 'Branding' })).toBeVisible()
await expect(dialog.getByText('web app Name')).toBeVisible()
await expect(dialog.getByText('web app Description')).toBeVisible()
})

View File

@ -75,8 +75,8 @@ Then('I should see the Agent v2 Access Point overview', async function (this: Di
await expect(accessRegion.getByLabel('Toggle Web app access')).toBeVisible()
await expect(accessRegion.getByRole('link', { name: 'Launch' })).toBeVisible()
await expect(accessRegion.getByRole('button', { name: 'Embedded' })).toBeVisible()
await expect(accessRegion.getByRole('button', { name: 'Customize' })).toBeVisible()
await expect(accessRegion.getByRole('button', { name: 'Settings' })).toBeVisible()
await expect(accessRegion.getByRole('button', { name: 'Custom Frontend' })).toBeVisible()
await expect(accessRegion.getByRole('button', { name: 'Branding' })).toBeVisible()
await expect(accessRegion.getByRole('heading', { name: 'Backend service API' })).toBeVisible()
await expect(accessRegion.getByText('Service API Endpoint')).toBeVisible()
await expect(accessRegion.getByLabel('Copy service API endpoint')).toBeVisible()

View File

@ -117,9 +117,45 @@ describe('app-card-sections', () => {
expect(onLaunch).toHaveBeenCalledTimes(1)
expect(onLaunchConfig).toHaveBeenCalledTimes(1)
expect(screen.getByText('overview.appInfo.launch')).not.toHaveAttribute('title')
expect(screen.getByRole('button', { name: /overview\.appInfo\.embedded\.entry/i })).toBeInTheDocument()
})
it('should expose native titles only for truncated operation labels', () => {
const operations = createAppCardOperations({
operationKeys: ['launch', 'embedded', 'customize', 'settings', 'develop'],
t: t as never,
runningStatus: true,
triggerModeDisabled: false,
onLaunch: vi.fn(),
onEmbedded: vi.fn(),
onCustomize: vi.fn(),
onSettings: vi.fn(),
onDevelop: vi.fn(),
})
render(<AppCardOperations t={t as never} operations={operations} />)
const untruncatedLabels = [
'overview.appInfo.launch',
'overview.appInfo.embedded.entry',
'overview.apiInfo.doc',
]
untruncatedLabels.forEach((label) => {
expect(screen.getByText(label)).toBeInTheDocument()
expect(screen.getByText(label)).not.toHaveAttribute('title')
})
const truncatedLabels = [
'overview.appInfo.customize.entry',
'overview.appInfo.settings.entry',
]
truncatedLabels.forEach((label) => {
expect(screen.getByText(label)).toBeInTheDocument()
expect(screen.getByRole('button', { name: label })).toHaveAttribute('title', label)
})
})
it('should keep customize available for web app cards that are not completion or workflow apps', () => {
const operations = createAppCardOperations({
operationKeys: ['customize'],
@ -140,7 +176,7 @@ describe('app-card-sections', () => {
/>,
)
expect(screen.getByText('overview.appInfo.customize.entry')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'overview.appInfo.customize.entry' })).toHaveAttribute('title', 'overview.appInfo.customize.entry')
expect(AppModeEnum.CHAT).toBe('chat')
})

View File

@ -19,6 +19,7 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
Dialog,
DialogContent,
@ -30,7 +31,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@langgenius/dify-ui/tooltip'
import { RiArrowRightSLine, RiBookOpenLine, RiBuildingLine, RiEqualizer2Line, RiExternalLinkLine, RiGlobalLine, RiLockLine, RiPaintBrushLine, RiSettings2Line, RiVerifiedBadgeLine, RiWindowLine } from '@remixicon/react'
import { RiArrowRightSLine, RiBookOpenLine, RiBuildingLine, RiExternalLinkLine, RiGlobalLine, RiLockLine, RiPaintBrushLine, RiPaletteLine, RiSettings2Line, RiVerifiedBadgeLine, RiWindowLine } from '@remixicon/react'
import { Trans } from 'react-i18next'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Divider from '@/app/components/base/divider'
@ -72,7 +73,7 @@ const OPERATION_ICON_MAP: Record<OverviewOperationKey, OperationIcon> = {
launch: RiExternalLinkLine,
embedded: RiWindowLine,
customize: RiPaintBrushLine,
settings: RiEqualizer2Line,
settings: RiPaletteLine,
develop: RiBookOpenLine,
}
@ -336,22 +337,25 @@ export const AppCardOperations = ({
}) => (
<>
{operations.map(({ key, label, Icon, disabled, onClick }) => {
const shouldTruncate = key === 'customize' || key === 'settings'
const buttonContent = (
<MaybeTooltip
content={t('overview.appInfo.preUseReminder', { ns: 'appOverview' }) ?? ''}
tooltipClassName="mt-[-8px]"
show={disabled}
>
<div className="flex items-center justify-center gap-px">
<Icon className="size-3.5" />
<div className={`${disabled ? 'text-components-button-ghost-text-disabled' : 'text-text-tertiary'} px-[3px] system-xs-medium`}>{label}</div>
<div className={cn('flex items-center justify-center gap-px', shouldTruncate && 'max-w-full min-w-0')}>
<Icon className="size-3.5 shrink-0" />
<div
className={cn(
disabled ? 'text-components-button-ghost-text-disabled' : 'text-text-tertiary',
'px-[3px] system-xs-medium',
shouldTruncate && 'min-w-0 truncate',
)}
>
{label}
</div>
</MaybeTooltip>
</div>
)
if (key === 'launch' && launchConfigAction) {
return (
<div key={key} className="mr-1 inline-flex">
<div key={key} className="mr-1 inline-flex shrink-0">
<MaybeTooltip
content={t('overview.appInfo.preUseReminder', { ns: 'appOverview' }) ?? ''}
tooltipClassName="mt-[-8px]"
@ -394,18 +398,36 @@ export const AppCardOperations = ({
)
}
return (
const actionButton = (
<Button
className="mr-1 min-w-[88px]"
className={cn(
'mr-1 max-w-full min-w-[88px] overflow-hidden [&>*]:max-w-full [&>*]:min-w-0',
!shouldTruncate && 'shrink-0',
)}
size="small"
variant="ghost"
key={key}
title={shouldTruncate ? label : undefined}
onClick={onClick}
disabled={disabled}
>
{buttonContent}
</Button>
)
if (disabled) {
return (
<MaybeTooltip
key={key}
content={t('overview.appInfo.preUseReminder', { ns: 'appOverview' }) ?? ''}
tooltipClassName="mt-[-8px]"
>
{actionButton}
</MaybeTooltip>
)
}
return actionButton
})}
</>
)

View File

@ -144,18 +144,14 @@ describe('SettingsModal', () => {
vi.useRealTimers()
})
it('should render the modal and expose the expanded settings section', async () => {
it('should render the modal with all settings exposed by default', async () => {
renderSettingsModal()
expect(screen.getByText('appOverview.overview.appInfo.settings.title')).toBeInTheDocument()
const showMoreEntry = screen.getByText('appOverview.overview.appInfo.settings.more.entry')
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()
})
expect(screen.queryByText('appOverview.overview.appInfo.settings.more.entry')).not.toBeInTheDocument()
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()
})
it('should notify the user when the name is empty', async () => {
@ -184,9 +180,8 @@ describe('SettingsModal', () => {
expect(mockOnSave).not.toHaveBeenCalled()
})
it('should validate the privacy policy URL when advanced settings are open', async () => {
it('should validate the privacy policy URL', async () => {
renderSettingsModal()
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
const privacyInput = screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')
fireEvent.change(privacyInput, { target: { value: 'ftp://invalid-url' } })
@ -228,14 +223,10 @@ describe('SettingsModal', () => {
expect(mockOnClose).toHaveBeenCalled()
})
it('should keep one show-more trigger while toggling the advanced section', () => {
it('should not render a show-more trigger', () => {
renderSettingsModal()
expect(screen.getAllByText('appOverview.overview.appInfo.settings.more.entry')).toHaveLength(1)
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
expect(screen.getAllByText('appOverview.overview.appInfo.settings.more.entry')).toHaveLength(1)
expect(screen.queryByText('appOverview.overview.appInfo.settings.more.entry')).not.toBeInTheDocument()
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
})
@ -249,8 +240,6 @@ describe('SettingsModal', () => {
onSave={mockOnSave}
/>,
)
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
rerender(
@ -272,8 +261,8 @@ describe('SettingsModal', () => {
/>,
)
expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument()
expect(screen.queryByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).not.toBeInTheDocument()
expect(screen.queryByText('appOverview.overview.appInfo.settings.more.entry')).not.toBeInTheDocument()
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
})
it('should reset the input placeholder when app info changes while open', () => {
@ -286,8 +275,6 @@ describe('SettingsModal', () => {
onSave={mockOnSave}
/>,
)
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toHaveValue('Ask me anything')
rerender(
@ -305,8 +292,6 @@ describe('SettingsModal', () => {
onSave={mockOnSave}
/>,
)
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toHaveValue('Updated prompt')
})
@ -324,8 +309,6 @@ describe('SettingsModal', () => {
renderSettingsModal()
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
const inputPlaceholder = screen.getByRole('textbox', { name: inputPlaceholderName })
expect(inputPlaceholder).toBeDisabled()
expect(inputPlaceholder).toHaveValue('')
@ -354,8 +337,6 @@ describe('SettingsModal', () => {
})
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'))
@ -382,8 +363,6 @@ describe('SettingsModal', () => {
})
renderSettingsModal()
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
fireEvent.click((await screen.findAllByText('billing.upgradeBtn.encourageShort'))[0]!)
expect(mockSetShowPricingModal).toHaveBeenCalled()
@ -402,8 +381,6 @@ describe('SettingsModal', () => {
})
renderSettingsModal()
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
await waitFor(() => {
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
})
@ -424,8 +401,6 @@ describe('SettingsModal', () => {
renderSettingsModal(imageAppInfo)
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
fireEvent.change(screen.getByDisplayValue('A description'), {
target: { value: 'Updated description' },
})

View File

@ -4,7 +4,6 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { AppIconType, Language, SiteConfig } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { CollapsiblePanel, CollapsibleRoot, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { FieldControl, FieldDescription, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
@ -173,7 +172,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose,
onSave,
}) => {
const [isShowMore, setIsShowMore] = useState(false)
const [inputPlaceholderFocused, setInputPlaceholderFocused] = useState(false)
const { default_language } = appInfo.site
const nextInputInfo = createInputInfo(appInfo)
@ -250,13 +248,11 @@ const SettingsModal: FC<ISettingsModalProps> = ({
setInputInfo(nextInputInfo)
setLanguage(default_language)
setAppIcon(nextAppIcon)
setIsShowMore(false)
setPreviousSettingsResetKey(settingsResetKey)
}
}
const handleClose = () => {
setIsShowMore(false)
onClose()
}
@ -463,147 +459,134 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
<FieldDescription>{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</FieldDescription>
</FieldRoot>
<CollapsibleRoot open={isShowMore} onOpenChange={setIsShowMore}>
<Divider className="my-0 h-px" />
<CollapsibleTrigger className="-mx-2 mt-2 min-h-11 px-2 py-1.5 hover:not-data-disabled:bg-components-panel-on-panel-item-bg-hover focus-visible:ring-inset data-panel-open:text-text-secondary">
<div className="min-w-0 grow">
<div className="system-sm-semibold text-text-secondary">{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
<p className="truncate body-xs-regular text-text-tertiary">
{t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })}
{' '}
&
{' '}
{t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' })}
</p>
</div>
<span aria-hidden="true" className="i-ri-arrow-right-s-line size-4 shrink-0 text-text-secondary transition-transform group-data-panel-open:rotate-90" />
</CollapsibleTrigger>
<CollapsiblePanel>
<div className="space-y-5 pt-5">
{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>
<Divider className="my-0 h-px" />
<div className="space-y-5">
{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>
)}
</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}`}
</PremiumBadgeButton>
</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 */}
{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>
{webappCopyrightEnabled
? (
<Switch
checked={copyrightSwitchValue}
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
)
: (
<Tooltip>
<TooltipTrigger
render={(
<div>
<Switch
disabled
checked={copyrightSwitchValue}
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
</div>
)}
/>
<TooltipContent className="w-[180px]">
{t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })}
</TooltipContent>
</Tooltip>
)}
</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>
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
{copyrightSwitchValue && (
<Input
className="mt-2 h-10"
value={inputInfo.copyright}
onChange={onChange('copyright')}
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' }) as string}
/>
)}
</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 */}
{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>
{/* privacy policy */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
<Trans
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
ns="appOverview"
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
/>
</p>
<Input
className="mt-1"
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' }) as string}
/>
</div>
{/* custom disclaimer */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
<Textarea
aria-label={t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}
className="mt-1"
value={inputInfo.customDisclaimer}
onValueChange={value => setInputInfo(item => ({ ...item, customDisclaimer: value }))}
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
/>
</div>
{webappCopyrightEnabled
? (
<Switch
aria-label={t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}
checked={copyrightSwitchValue}
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
)
: (
<Tooltip>
<TooltipTrigger
render={(
<div>
<Switch
aria-label={t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}
disabled
checked={copyrightSwitchValue}
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
</div>
)}
/>
<TooltipContent className="w-[180px]">
{t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })}
</TooltipContent>
</Tooltip>
)}
</div>
</CollapsiblePanel>
</CollapsibleRoot>
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
{copyrightSwitchValue && (
<Input
aria-label={t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}
className="mt-2 h-10"
value={inputInfo.copyright}
onChange={onChange('copyright')}
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' }) as string}
/>
)}
</div>
{/* privacy policy */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
<Trans
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
ns="appOverview"
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
/>
</p>
<Input
aria-label={t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}
className="mt-1"
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' }) as string}
/>
</div>
{/* custom disclaimer */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
<Textarea
aria-label={t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}
className="mt-1"
value={inputInfo.customDisclaimer}
onValueChange={value => setInputInfo(item => ({ ...item, customDisclaimer: value }))}
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
/>
</div>
</div>
</ScrollArea>
{/* footer */}
<div className="flex shrink-0 justify-end p-6 pt-5">

View File

@ -248,7 +248,7 @@ export function WebAppAccessCard({
disabled={!settingsAppInfo || updateSiteMutation.isPending}
onClick={() => setShowSettingsModal(true)}
>
<span aria-hidden className="i-ri-equalizer-2-line size-4" />
<span aria-hidden className="i-ri-palette-line size-4" />
{t('agentDetail.access.webApp.actions.settings')}
</Button>
{settingsAppInfo && (

View File

@ -14,10 +14,10 @@
"agentDetail.access.title": "Access Point",
"agentDetail.access.toggleSurface": "Toggle {{name}} access",
"agentDetail.access.webApp.accessUrl": "Access URL",
"agentDetail.access.webApp.actions.customize": "Customize",
"agentDetail.access.webApp.actions.customize": "Custom Frontend",
"agentDetail.access.webApp.actions.embedded": "Embedded",
"agentDetail.access.webApp.actions.launch": "Launch",
"agentDetail.access.webApp.actions.settings": "Settings",
"agentDetail.access.webApp.actions.settings": "Branding",
"agentDetail.access.webApp.refreshUrl": "Refresh access URL",
"agentDetail.access.webApp.showQrCode": "Show QR code",
"agentDetail.access.webApp.ssoEnabled": "SSO Enabled",

View File

@ -36,9 +36,9 @@
"overview.apiInfo.explanation": "Easily integrated into your application",
"overview.apiInfo.title": "Backend Service API",
"overview.appInfo.accessibleAddress": "Public URL",
"overview.appInfo.customize.entry": "Customize",
"overview.appInfo.customize.entry": "Custom Frontend",
"overview.appInfo.customize.explanation": "You can customize the frontend of the Web App to fit your scenario and style needs.",
"overview.appInfo.customize.title": "Customize AI web app",
"overview.appInfo.customize.title": "Custom Frontend",
"overview.appInfo.customize.way": "way",
"overview.appInfo.customize.way1.name": "Fork the client code, modify it and deploy to Vercel (recommended)",
"overview.appInfo.customize.way1.step1": "Fork the client code and modify it",
@ -75,7 +75,7 @@
"overview.appInfo.settings.chatColorTheme": "Chat color theme",
"overview.appInfo.settings.chatColorThemeDesc": "Set the color theme of the chatbot",
"overview.appInfo.settings.chatColorThemeInverted": "Inverted",
"overview.appInfo.settings.entry": "Settings",
"overview.appInfo.settings.entry": "Branding",
"overview.appInfo.settings.invalidHexMessage": "Invalid hex value",
"overview.appInfo.settings.invalidPrivacyPolicy": "Invalid privacy policy link. Please use a valid link that starts with http or https",
"overview.appInfo.settings.language": "Language",
@ -99,7 +99,7 @@
"overview.appInfo.settings.sso.label": "SSO Enforcement",
"overview.appInfo.settings.sso.title": "web app SSO",
"overview.appInfo.settings.sso.tooltip": "Contact the administrator to enable web app SSO",
"overview.appInfo.settings.title": "Web App Settings",
"overview.appInfo.settings.title": "Branding",
"overview.appInfo.settings.webDesc": "web app Description",
"overview.appInfo.settings.webDescPlaceholder": "Enter the description of the web app",
"overview.appInfo.settings.webDescTip": "This text will be displayed on the client side, providing basic guidance on how to use the application",

View File

@ -14,10 +14,10 @@
"agentDetail.access.title": "アクセスポイント",
"agentDetail.access.toggleSurface": "{{name}} のアクセスを切り替え",
"agentDetail.access.webApp.accessUrl": "アクセス URL",
"agentDetail.access.webApp.actions.customize": "カスタマイズ",
"agentDetail.access.webApp.actions.customize": "カスタムフロントエンド",
"agentDetail.access.webApp.actions.embedded": "埋め込み",
"agentDetail.access.webApp.actions.launch": "起動",
"agentDetail.access.webApp.actions.settings": "設定",
"agentDetail.access.webApp.actions.settings": "ブランディング",
"agentDetail.access.webApp.refreshUrl": "アクセス URL を更新",
"agentDetail.access.webApp.showQrCode": "QR コードを表示",
"agentDetail.access.webApp.ssoEnabled": "SSO 有効",

View File

@ -36,9 +36,9 @@
"overview.apiInfo.explanation": "あなたのアプリケーションに簡単に統合できます",
"overview.apiInfo.title": "バックエンドサービス API",
"overview.appInfo.accessibleAddress": "公開 URL",
"overview.appInfo.customize.entry": "カスタマイズ",
"overview.appInfo.customize.entry": "カスタムフロントエンド",
"overview.appInfo.customize.explanation": "シナリオとスタイルのニーズに合わせて Web アプリのフロントエンドをカスタマイズできます。",
"overview.appInfo.customize.title": "AI Web アプリのカスタマイズ",
"overview.appInfo.customize.title": "カスタムフロントエンド",
"overview.appInfo.customize.way": "方法",
"overview.appInfo.customize.way1.name": "クライアントコードをフォークして修正し、Vercel にデプロイします(推奨)",
"overview.appInfo.customize.way1.step1": "クライアントコードをフォークして修正します",
@ -75,7 +75,7 @@
"overview.appInfo.settings.chatColorTheme": "チャットボットのカラーテーマ",
"overview.appInfo.settings.chatColorThemeDesc": "チャットボットのカラーテーマを設定します",
"overview.appInfo.settings.chatColorThemeInverted": "反転",
"overview.appInfo.settings.entry": "設定",
"overview.appInfo.settings.entry": "ブランディング",
"overview.appInfo.settings.invalidHexMessage": "無効な 16 進数値",
"overview.appInfo.settings.invalidPrivacyPolicy": "無効なプライバシーポリシーのリンクです。http または https で始まる有効なリンクを使用してください",
"overview.appInfo.settings.language": "言語",
@ -99,7 +99,7 @@
"overview.appInfo.settings.sso.label": "SSO 認証",
"overview.appInfo.settings.sso.title": "Web アプリの SSO",
"overview.appInfo.settings.sso.tooltip": "管理者に問い合わせて、Web アプリの SSO を有効にします",
"overview.appInfo.settings.title": "Web アプリの設定",
"overview.appInfo.settings.title": "ブランディング",
"overview.appInfo.settings.webDesc": "Web アプリの説明",
"overview.appInfo.settings.webDescPlaceholder": "Web アプリの説明を入力してください",
"overview.appInfo.settings.webDescTip": "このテキストはクライアント側に表示され、アプリケーションの使用方法の基本的なガイダンスを提供します。",

View File

@ -14,10 +14,10 @@
"agentDetail.access.title": "访问点",
"agentDetail.access.toggleSurface": "切换 {{name}} 访问状态",
"agentDetail.access.webApp.accessUrl": "访问 URL",
"agentDetail.access.webApp.actions.customize": "自定义",
"agentDetail.access.webApp.actions.customize": "自定义前端",
"agentDetail.access.webApp.actions.embedded": "嵌入",
"agentDetail.access.webApp.actions.launch": "启动",
"agentDetail.access.webApp.actions.settings": "设置",
"agentDetail.access.webApp.actions.settings": "品牌设置",
"agentDetail.access.webApp.refreshUrl": "刷新访问 URL",
"agentDetail.access.webApp.showQrCode": "显示二维码",
"agentDetail.access.webApp.ssoEnabled": "已启用 SSO",

View File

@ -36,9 +36,9 @@
"overview.apiInfo.explanation": "可集成至你的应用的后端即服务",
"overview.apiInfo.title": "后端服务 API",
"overview.appInfo.accessibleAddress": "公开访问 URL",
"overview.appInfo.customize.entry": "定制化",
"overview.appInfo.customize.entry": "自定义前端",
"overview.appInfo.customize.explanation": "你可以定制化 Web App 前端以符合你的情景与风格需求",
"overview.appInfo.customize.title": "定制化 AI web app",
"overview.appInfo.customize.title": "自定义前端",
"overview.appInfo.customize.way": "方法",
"overview.appInfo.customize.way1.name": "Fork 客户端代码修改后部署到 Vercel推荐",
"overview.appInfo.customize.way1.step1": "Fork 客户端代码并修改",
@ -75,7 +75,7 @@
"overview.appInfo.settings.chatColorTheme": "聊天颜色主题",
"overview.appInfo.settings.chatColorThemeDesc": "设置聊天机器人的颜色主题",
"overview.appInfo.settings.chatColorThemeInverted": "反转",
"overview.appInfo.settings.entry": "设置",
"overview.appInfo.settings.entry": "品牌设置",
"overview.appInfo.settings.invalidHexMessage": "无效的十六进制值",
"overview.appInfo.settings.invalidPrivacyPolicy": "无效的隐私政策链接,请使用以 http 或 https 开头的有效链接",
"overview.appInfo.settings.language": "语言",
@ -99,7 +99,7 @@
"overview.appInfo.settings.sso.label": "单点登录认证",
"overview.appInfo.settings.sso.title": "web app SSO 认证",
"overview.appInfo.settings.sso.tooltip": "联系管理员以开启 web app SSO 认证",
"overview.appInfo.settings.title": "web app 设置",
"overview.appInfo.settings.title": "品牌设置",
"overview.appInfo.settings.webDesc": "web app 描述",
"overview.appInfo.settings.webDescPlaceholder": "请输入 web app 的描述",
"overview.appInfo.settings.webDescTip": "以下文字将展示在客户端中,对应用进行说明和使用上的基本引导",