mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
feat(web): refine web app customization options (#38650)
This commit is contained in:
parent
3c8e0e2113
commit
775ed70bac
@ -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()
|
||||
})
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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' },
|
||||
})
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 有効",
|
||||
|
||||
@ -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": "このテキストはクライアント側に表示され、アプリケーションの使用方法の基本的なガイダンスを提供します。",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "以下文字将展示在客户端中,对应用进行说明和使用上的基本引导",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user