Kaynağa Gözat

Feat: copyright modification (#12707)

KVOJJJin 3 ay önce
ebeveyn
işleme
435eddd867

+ 243 - 129
web/app/components/app/overview/settings/index.tsx

@@ -1,26 +1,33 @@
 'use client'
 import type { FC } from 'react'
-import React, { useEffect, useState } from 'react'
-import { ChevronRightIcon } from '@heroicons/react/20/solid'
+import React, { useCallback, useEffect, useState } from 'react'
+import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
 import Link from 'next/link'
 import { Trans, useTranslation } from 'react-i18next'
-import { useContextSelector } from 'use-context-selector'
-import s from './style.module.css'
+import { useContext, useContextSelector } from 'use-context-selector'
+import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
 import Modal from '@/app/components/base/modal'
+import ActionButton from '@/app/components/base/action-button'
 import Button from '@/app/components/base/button'
+import Divider from '@/app/components/base/divider'
 import Input from '@/app/components/base/input'
 import Textarea from '@/app/components/base/textarea'
 import AppIcon from '@/app/components/base/app-icon'
 import Switch from '@/app/components/base/switch'
+import PremiumBadge from '@/app/components/base/premium-badge'
 import { SimpleSelect } from '@/app/components/base/select'
 import type { AppDetailResponse } from '@/models/app'
 import type { AppIconType, AppSSO, Language } from '@/types/app'
 import { useToastContext } from '@/app/components/base/toast'
-import { languages } from '@/i18n/language'
+import { LanguagesSupported, languages } from '@/i18n/language'
 import Tooltip from '@/app/components/base/tooltip'
 import AppContext, { useAppContext } from '@/context/app-context'
+import { useProviderContext } from '@/context/provider-context'
+import { useModalContext } from '@/context/modal-context'
 import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
 import AppIconPicker from '@/app/components/base/app-icon-picker'
+import I18n from '@/context/i18n'
+import cn from '@/utils/classnames'
 
 export type ISettingsModalProps = {
   isChat: boolean
@@ -84,6 +91,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
     chatColorTheme: chat_color_theme,
     chatColorThemeInverted: chat_color_theme_inverted,
     copyright,
+    copyrightSwitchValue: !!copyright,
     privacyPolicy: privacy_policy,
     customDisclaimer: custom_disclaimer,
     show_workflow_steps,
@@ -93,6 +101,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
   const [language, setLanguage] = useState(default_language)
   const [saveLoading, setSaveLoading] = useState(false)
   const { t } = useTranslation()
+  const { locale } = useContext(I18n)
 
   const [showAppIconPicker, setShowAppIconPicker] = useState(false)
   const [appIcon, setAppIcon] = useState<AppIconSelection>(
@@ -100,7 +109,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       ? { type: 'image', url: icon_url!, fileId: icon }
       : { type: 'emoji', icon, background: icon_background! },
   )
-  const isChatBot = appInfo.mode === 'chat' || appInfo.mode === 'advanced-chat' || appInfo.mode === 'agent-chat'
+
+  const { enableBilling, plan } = useProviderContext()
+  const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
+  const isFreePlan = plan.type === 'sandbox'
+  const handlePlanClick = useCallback(() => {
+    if (isFreePlan)
+      setShowPricingModal()
+    else
+      setShowAccountSettingModal({ payload: 'billing' })
+  }, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
 
   useEffect(() => {
     setInputInfo({
@@ -109,6 +127,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       chatColorTheme: chat_color_theme,
       chatColorThemeInverted: chat_color_theme_inverted,
       copyright,
+      copyrightSwitchValue: !!copyright,
       privacyPolicy: privacy_policy,
       customDisclaimer: custom_disclaimer,
       show_workflow_steps,
@@ -158,7 +177,11 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       chat_color_theme: inputInfo.chatColorTheme,
       chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
       prompt_public: false,
-      copyright: inputInfo.copyright,
+      copyright: isFreePlan
+        ? ''
+        : inputInfo.copyrightSwitchValue
+          ? inputInfo.copyright
+          : '',
       privacy_policy: inputInfo.privacyPolicy,
       custom_disclaimer: inputInfo.customDisclaimer,
       icon_type: appIcon.type,
@@ -192,141 +215,232 @@ const SettingsModal: FC<ISettingsModalProps> = ({
   return (
     <>
       <Modal
-        title={t(`${prefixSettings}.title`)}
         isShow={isShow}
+        closable={false}
         onClose={onHide}
-        className={`${s.settingsModal}`}
+        className='max-w-[520px] p-0'
       >
-        <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div>
-        <div className='flex mt-2'>
-          <AppIcon size='large'
-            onClick={() => { setShowAppIconPicker(true) }}
-            className='cursor-pointer !mr-3 self-center'
-            iconType={appIcon.type}
-            icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
-            background={appIcon.type === 'image' ? undefined : appIcon.background}
-            imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
-          />
-          <Input
-            className='grow h-10'
-            value={inputInfo.title}
-            onChange={onChange('title')}
-            placeholder={t('app.appNamePlaceholder') || ''}
-          />
+        {/* header */}
+        <div className='pl-6 pt-5 pr-5 pb-3'>
+          <div className='flex items-center gap-1'>
+            <div className='grow text-text-primary title-2xl-semi-bold'>{t(`${prefixSettings}.title`)}</div>
+            <ActionButton className='shrink-0' onClick={onHide}>
+              <RiCloseLine className='w-4 h-4' />
+            </ActionButton>
+          </div>
+          <div className='mt-0.5 text-text-tertiary system-xs-regular'>
+            <span>{t(`${prefixSettings}.modalTip`)}</span>
+            <Link href={`${locale === LanguagesSupported[1] ? 'https://docs.dify.ai/zh-hans/guides/application-publishing/launch-your-webapp-quickly#she-zhi-ni-de-ai-zhan-dian' : 'https://docs.dify.ai/guides/application-publishing/launch-your-webapp-quickly#setting-up-your-ai-site'}`} target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link>
+          </div>
         </div>
-        <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
-        <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
-        <Textarea
-          className='mt-2'
-          value={inputInfo.desc}
-          onChange={e => onDesChange(e.target.value)}
-          placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
-        />
-        {isChatBot && (
-          <div className='w-full mt-4'>
-            <div className='flex justify-between items-center'>
-              <div className={`font-medium ${s.settingTitle} text-gray-900 `}>{t('app.answerIcon.title')}</div>
-              <Switch
-                defaultValue={inputInfo.use_icon_as_answer_icon}
-                onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
+        {/* form body */}
+        <div className='px-6 py-3 space-y-5'>
+          {/* name & icon */}
+          <div className='flex gap-4'>
+            <div className='grow'>
+              <div className={cn('mb-1 py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.webName`)}</div>
+              <Input
+                className='w-full'
+                value={inputInfo.title}
+                onChange={onChange('title')}
+                placeholder={t('app.appNamePlaceholder') || ''}
               />
             </div>
-            <p className='body-xs-regular text-gray-500'>{t('app.answerIcon.description')}</p>
-          </div>
-        )}
-        <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
-        <SimpleSelect
-          items={languages.filter(item => item.supported)}
-          defaultValue={language}
-          onSelect={item => setLanguage(item.value as Language)}
-        />
-        <div className='w-full mt-8'>
-          <p className='system-xs-medium text-gray-500'>{t(`${prefixSettings}.workflow.title`)}</p>
-          <div className='flex justify-between items-center'>
-            <div className='font-medium system-sm-semibold flex-grow text-gray-900'>{t(`${prefixSettings}.workflow.subTitle`)}</div>
-            <Switch
-              disabled={!(appInfo.mode === 'workflow' || appInfo.mode === 'advanced-chat')}
-              defaultValue={inputInfo.show_workflow_steps}
-              onChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
+            <AppIcon
+              size='xxl'
+              onClick={() => { setShowAppIconPicker(true) }}
+              className='mt-2 cursor-pointer'
+              iconType={appIcon.type}
+              icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
+              background={appIcon.type === 'image' ? undefined : appIcon.background}
+              imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
             />
           </div>
-          <p className='body-xs-regular text-gray-500'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
-        </div>
-
-        {isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div>
-          <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p>
-          <Input
-            className='mt-2 h-10'
-            value={inputInfo.chatColorTheme ?? ''}
-            onChange={onChange('chatColorTheme')}
-            placeholder='E.g #A020F0'
-          />
-          <div className="mt-1 flex justify-between items-center">
-            <p className={`ml-2 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeInverted`)}</p>
-            <Switch defaultValue={inputInfo.chatColorThemeInverted} onChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
+          {/* description */}
+          <div className='relative'>
+            <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.webDesc`)}</div>
+            <Textarea
+              className='mt-1'
+              value={inputInfo.desc}
+              onChange={e => onDesChange(e.target.value)}
+              placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
+            />
+            <p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.webDescTip`)}</p>
           </div>
-        </>}
-        {systemFeatures.enable_web_sso_switch_component && <div className='w-full mt-8'>
-          <p className='system-xs-medium text-gray-500'>{t(`${prefixSettings}.sso.label`)}</p>
-          <div className='flex justify-between items-center'>
-            <div className='font-medium system-sm-semibold flex-grow text-gray-900'>{t(`${prefixSettings}.sso.title`)}</div>
-            <Tooltip
-              disabled={systemFeatures.sso_enforced_for_web}
-              popupContent={
-                <div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>
-              }
-              asChild={false}
-            >
-              <Switch disabled={!systemFeatures.sso_enforced_for_web || !isCurrentWorkspaceEditor} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
-            </Tooltip>
+          <Divider className="h-px my-0" />
+          {/* answer icon */}
+          {isChat && (
+            <div className='w-full'>
+              <div className='flex justify-between items-center'>
+                <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t('app.answerIcon.title')}</div>
+                <Switch
+                  defaultValue={inputInfo.use_icon_as_answer_icon}
+                  onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
+                />
+              </div>
+              <p className='pb-0.5 text-text-tertiary body-xs-regular'>{t('app.answerIcon.description')}</p>
+            </div>
+          )}
+          {/* language */}
+          <div className='flex items-center'>
+            <div className={cn('grow py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.language`)}</div>
+            <SimpleSelect
+              wrapperClassName='w-[200px]'
+              items={languages.filter(item => item.supported)}
+              defaultValue={language}
+              onSelect={item => setLanguage(item.value as Language)}
+            />
           </div>
-          <p className='body-xs-regular text-gray-500'>{t(`${prefixSettings}.sso.description`)}</p>
-        </div>}
-        {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
-          <div className='flex justify-between'>
-            <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
-            <div className='flex-shrink-0 w-4 h-4 text-gray-500'>
-              <ChevronRightIcon />
+          {/* theme color */}
+          {isChat && (
+            <div className='flex items-center'>
+              <div className='grow'>
+                <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.chatColorTheme`)}</div>
+                <div className='pb-0.5 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.chatColorThemeDesc`)}</div>
+              </div>
+              <div className='shrink-0'>
+                <Input
+                  className='mb-1 w-[200px]'
+                  value={inputInfo.chatColorTheme ?? ''}
+                  onChange={onChange('chatColorTheme')}
+                  placeholder='E.g #A020F0'
+                />
+                <div className='flex justify-between items-center'>
+                  <p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`)}</p>
+                  <Switch defaultValue={inputInfo.chatColorThemeInverted} onChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
+                </div>
+              </div>
+            </div>
+          )}
+          {/* workflow detail */}
+          <div className='w-full'>
+            <div className='flex justify-between items-center'>
+              <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.workflow.subTitle`)}</div>
+              <Switch
+                disabled={!(appInfo.mode === 'workflow' || appInfo.mode === 'advanced-chat')}
+                defaultValue={inputInfo.show_workflow_steps}
+                onChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
+              />
             </div>
+            <p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
           </div>
-          <p className={`mt-1 ${s.policy} text-gray-500`}>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
-        </div>}
-        {isShowMore && <>
-          <hr className='w-full mt-6' />
-          <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
-          <Input
-            className='mt-2 h-10'
-            value={inputInfo.copyright}
-            onChange={onChange('copyright')}
-            placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
-          />
-          <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
-          <p className={`mt-1 ${s.settingsTip} text-gray-500`}>
-            <Trans
-              i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
-              components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
-            />
-          </p>
-          <Input
-            className='mt-2 h-10'
-            value={inputInfo.privacyPolicy}
-            onChange={onChange('privacyPolicy')}
-            placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
-          />
-          <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
-          <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
-          <Input
-            className='mt-2 h-10'
-            value={inputInfo.customDisclaimer}
-            onChange={onChange('customDisclaimer')}
-            placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string}
-          />
-        </>}
-        <div className='mt-10 flex justify-end'>
+          {/* SSO */}
+          {systemFeatures.enable_web_sso_switch_component && (
+            <>
+              <Divider className="h-px my-0" />
+              <div className='w-full'>
+                <p className='mb-1 system-xs-medium-uppercase text-text-tertiary'>{t(`${prefixSettings}.sso.label`)}</p>
+                <div className='flex justify-between items-center'>
+                  <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.sso.title`)}</div>
+                  <Tooltip
+                    disabled={systemFeatures.sso_enforced_for_web}
+                    popupContent={
+                      <div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>
+                    }
+                    asChild={false}
+                  >
+                    <Switch disabled={!systemFeatures.sso_enforced_for_web || !isCurrentWorkspaceEditor} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
+                  </Tooltip>
+                </div>
+                <p className='pb-0.5 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.sso.description`)}</p>
+              </div>
+            </>
+          )}
+          {/* more settings switch */}
+          <Divider className="h-px my-0" />
+          {!isShowMore && (
+            <div className='flex items-center cursor-pointer' onClick={() => setIsShowMore(true)}>
+              <div className='grow'>
+                <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.entry`)}</div>
+                <p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.more.copyRightPlaceholder`)} & {t(`${prefixSettings}.more.privacyPolicyPlaceholder`)}</p>
+              </div>
+              <RiArrowRightSLine className='shrink-0 ml-1 w-4 h-4 text-text-secondary'/>
+            </div>
+          )}
+          {/* more settings */}
+          {isShowMore && (
+            <>
+              {/* copyright */}
+              <div className='w-full'>
+                <div className='flex items-center'>
+                  <div className='grow flex items-center'>
+                    <div className={cn('mr-1 py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.copyright`)}</div>
+                    {/* upgrade button */}
+                    {enableBilling && isFreePlan && (
+                      <div className='select-none h-[18px]'>
+                        <PremiumBadge size='s' color='blue' allowHover={true} onClick={handlePlanClick}>
+                          <SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
+                          <div className='system-xs-medium'>
+                            <span className='p-1'>
+                              {t('billing.upgradeBtn.encourageShort')}
+                            </span>
+                          </div>
+                        </PremiumBadge>
+                      </div>
+                    )}
+                  </div>
+                  <Tooltip
+                    disabled={!isFreePlan}
+                    popupContent={
+                      <div className='w-[260px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
+                    }
+                    asChild={false}
+                  >
+                    <Switch
+                      disabled={isFreePlan}
+                      defaultValue={inputInfo.copyrightSwitchValue}
+                      onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
+                    />
+                  </Tooltip>
+                </div>
+                <p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.more.copyrightTip`)}</p>
+                {inputInfo.copyrightSwitchValue && (
+                  <Input
+                    className='mt-2 h-10'
+                    value={inputInfo.copyright}
+                    onChange={onChange('copyright')}
+                    placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
+                  />
+                )}
+              </div>
+              {/* privacy policy */}
+              <div className='w-full'>
+                <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
+                <p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
+                  <Trans
+                    i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
+                    components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} 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`) as string}
+                />
+              </div>
+              {/* custom disclaimer */}
+              <div className='w-full'>
+                <div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
+                <p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
+                <Textarea
+                  className='mt-1'
+                  value={inputInfo.customDisclaimer}
+                  onChange={onChange('customDisclaimer')}
+                  placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string}
+                />
+              </div>
+            </>
+          )}
+        </div>
+        {/* footer */}
+        <div className='p-6 pt-5 flex justify-end'>
           <Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button>
           <Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
         </div>
-        {showAppIconPicker && <AppIconPicker
+      </Modal >
+      {showAppIconPicker && (
+        <AppIconPicker
           onSelect={(payload) => {
             setAppIcon(payload)
             setShowAppIconPicker(false)
@@ -337,8 +451,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
               : { type: 'emoji', icon, background: icon_background! })
             setShowAppIconPicker(false)
           }}
-        />}
-      </Modal >
+        />
+      )}
     </>
 
   )

+ 0 - 18
web/app/components/app/overview/settings/style.module.css

@@ -1,18 +0,0 @@
-.settingsModal {
-	max-width: 32.5rem !important;
-}
-
-.settingTitle {
-	line-height: 21px;
-	font-size: 0.875rem;
-}
-
-.settingsTip {
-	line-height: 1.125rem;
-	font-size: 0.75rem;
-}
-
-.policy {
-	font-size: 0.75rem;
-	line-height: 1.125rem;
-}

+ 5 - 3
web/app/components/base/chat/chat-with-history/sidebar/index.tsx

@@ -115,9 +115,11 @@ const Sidebar = () => {
           )
         }
       </div>
-      <div className='px-4 pb-4 text-xs text-gray-400'>
-        © {appData?.site.copyright || appData?.site.title} {(new Date()).getFullYear()}
-      </div>
+      {appData?.site.copyright && (
+        <div className='px-4 pb-4 text-xs text-gray-400'>
+          © {(new Date()).getFullYear()} {appData?.site.copyright}
+        </div>
+      )}
       {!!showConfirm && (
         <Confirm
           title={t('share.chat.deleteConversation.title')}

+ 9 - 0
web/app/components/base/icons/assets/public/common/highlight.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="46" height="24" viewBox="0 0 46 24" fill="none">
+  <path opacity="0.5" d="M-6.5 8C-6.5 3.58172 -2.91828 0 1.5 0H45.5L33.0248 24H1.49999C-2.91829 24 -6.5 20.4183 -6.5 16V8Z" fill="url(#paint0_linear_6333_42118)"/>
+  <defs>
+    <linearGradient id="paint0_linear_6333_42118" x1="1.81679" y1="5.47784e-07" x2="101.257" y2="30.3866" gradientUnits="userSpaceOnUse">
+      <stop stop-color="white" stop-opacity="0.12"/>
+      <stop offset="1" stop-color="white" stop-opacity="0.3"/>
+    </linearGradient>
+  </defs>
+</svg>

+ 6 - 0
web/app/components/base/icons/assets/public/common/sparkles-soft.svg

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="sparkles-soft">
+<path id="Vector" opacity="0.5" d="M10.9963 1.36798C10.9839 1.25339 10.8909 1.16677 10.7802 1.16666C10.6695 1.16654 10.5763 1.25295 10.5636 1.36752C10.5045 1.90085 10.3525 2.26673 10.1143 2.5149C9.87599 2.76307 9.52476 2.92145 9.01275 2.98296C8.90277 2.99618 8.81983 3.09324 8.81995 3.20856C8.82006 3.32388 8.90322 3.42076 9.0132 3.43373C9.51653 3.49312 9.87583 3.65148 10.1201 3.90135C10.3631 4.14986 10.518 4.51523 10.563 5.04321C10.573 5.16035 10.6673 5.25012 10.7802 5.24999C10.8931 5.24986 10.9872 5.15987 10.9969 5.0427C11.0401 4.52364 11.1949 4.15004 11.4394 3.89528C11.684 3.64052 12.0426 3.47926 12.5409 3.43433C12.6534 3.42419 12.7398 3.32619 12.7399 3.20858C12.7401 3.09097 12.6539 2.99277 12.5414 2.98236C12.0346 2.93546 11.6838 2.77407 11.4452 2.52098C11.2054 2.2665 11.0533 1.89229 10.9963 1.36798Z" fill="#F5F8FF"/>
+<path id="Vector_2" d="M7.13646 2.85102C7.10442 2.55638 6.8653 2.33365 6.5806 2.33334C6.29595 2.33304 6.05633 2.55526 6.02374 2.84984C5.87186 4.22127 5.48089 5.1621 4.86827 5.80025C4.25565 6.43838 3.35245 6.84566 2.03587 7.00386C1.75307 7.03781 1.53975 7.28742 1.54004 7.58393C1.54033 7.88049 1.75415 8.12958 2.03701 8.16294C3.33132 8.31566 4.25509 8.72289 4.88328 9.36543C5.50807 10.0045 5.90647 10.9439 6.02222 12.3016C6.04793 12.6029 6.29035 12.8337 6.58066 12.8333C6.87102 12.833 7.11294 12.6016 7.13797 12.3003C7.24885 10.9656 7.64695 10.0049 8.27583 9.34979C8.90477 8.69471 9.82698 8.28002 11.1083 8.16452C11.3976 8.13844 11.6197 7.88644 11.62 7.58399C11.6204 7.28159 11.3988 7.02906 11.1096 7.00229C9.8062 6.88171 8.90432 6.46673 8.29084 5.81589C7.674 5.16152 7.28306 4.19926 7.13646 2.85102Z" fill="#F5F8FF"/>
+</g>
+</svg>

+ 67 - 0
web/app/components/base/icons/src/public/common/Highlight.json

@@ -0,0 +1,67 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"xmlns": "http://www.w3.org/2000/svg",
+			"width": "46",
+			"height": "24",
+			"viewBox": "0 0 46 24",
+			"fill": "none"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"opacity": "0.5",
+					"d": "M-6.5 8C-6.5 3.58172 -2.91828 0 1.5 0H45.5L33.0248 24H1.49999C-2.91829 24 -6.5 20.4183 -6.5 16V8Z",
+					"fill": "url(#paint0_linear_6333_42118)"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "linearGradient",
+						"attributes": {
+							"id": "paint0_linear_6333_42118",
+							"x1": "1.81679",
+							"y1": "5.47784e-07",
+							"x2": "101.257",
+							"y2": "30.3866",
+							"gradientUnits": "userSpaceOnUse"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"stop-color": "white",
+									"stop-opacity": "0.12"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"offset": "1",
+									"stop-color": "white",
+									"stop-opacity": "0.3"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Highlight"
+}

+ 16 - 0
web/app/components/base/icons/src/public/common/Highlight.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Highlight.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Highlight'
+
+export default Icon

+ 47 - 0
web/app/components/base/icons/src/public/common/SparklesSoft.json

@@ -0,0 +1,47 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "14",
+			"height": "14",
+			"viewBox": "0 0 14 14",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "sparkles-soft"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector",
+							"opacity": "0.5",
+							"d": "M10.9963 1.36798C10.9839 1.25339 10.8909 1.16677 10.7802 1.16666C10.6695 1.16654 10.5763 1.25295 10.5636 1.36752C10.5045 1.90085 10.3525 2.26673 10.1143 2.5149C9.87599 2.76307 9.52476 2.92145 9.01275 2.98296C8.90277 2.99618 8.81983 3.09324 8.81995 3.20856C8.82006 3.32388 8.90322 3.42076 9.0132 3.43373C9.51653 3.49312 9.87583 3.65148 10.1201 3.90135C10.3631 4.14986 10.518 4.51523 10.563 5.04321C10.573 5.16035 10.6673 5.25012 10.7802 5.24999C10.8931 5.24986 10.9872 5.15987 10.9969 5.0427C11.0401 4.52364 11.1949 4.15004 11.4394 3.89528C11.684 3.64052 12.0426 3.47926 12.5409 3.43433C12.6534 3.42419 12.7398 3.32619 12.7399 3.20858C12.7401 3.09097 12.6539 2.99277 12.5414 2.98236C12.0346 2.93546 11.6838 2.77407 11.4452 2.52098C11.2054 2.2665 11.0533 1.89229 10.9963 1.36798Z",
+							"fill": "#F5F8FF"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_2",
+							"d": "M7.13646 2.85102C7.10442 2.55638 6.8653 2.33365 6.5806 2.33334C6.29595 2.33304 6.05633 2.55526 6.02374 2.84984C5.87186 4.22127 5.48089 5.1621 4.86827 5.80025C4.25565 6.43838 3.35245 6.84566 2.03587 7.00386C1.75307 7.03781 1.53975 7.28742 1.54004 7.58393C1.54033 7.88049 1.75415 8.12958 2.03701 8.16294C3.33132 8.31566 4.25509 8.72289 4.88328 9.36543C5.50807 10.0045 5.90647 10.9439 6.02222 12.3016C6.04793 12.6029 6.29035 12.8337 6.58066 12.8333C6.87102 12.833 7.11294 12.6016 7.13797 12.3003C7.24885 10.9656 7.64695 10.0049 8.27583 9.34979C8.90477 8.69471 9.82698 8.28002 11.1083 8.16452C11.3976 8.13844 11.6197 7.88644 11.62 7.58399C11.6204 7.28159 11.3988 7.02906 11.1096 7.00229C9.8062 6.88171 8.90432 6.46673 8.29084 5.81589C7.674 5.16152 7.28306 4.19926 7.13646 2.85102Z",
+							"fill": "#F5F8FF"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "SparklesSoft"
+}

+ 16 - 0
web/app/components/base/icons/src/public/common/SparklesSoft.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './SparklesSoft.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'SparklesSoft'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/public/common/index.ts

@@ -2,9 +2,11 @@ export { default as D } from './D'
 export { default as DiagonalDividingLine } from './DiagonalDividingLine'
 export { default as Dify } from './Dify'
 export { default as Github } from './Github'
+export { default as Highlight } from './Highlight'
 export { default as Line3 } from './Line3'
 export { default as Lock } from './Lock'
 export { default as MessageChatSquare } from './MessageChatSquare'
 export { default as MultiPathRetrieval } from './MultiPathRetrieval'
 export { default as NTo1Retrieval } from './NTo1Retrieval'
 export { default as Notion } from './Notion'
+export { default as SparklesSoft } from './SparklesSoft'

+ 48 - 0
web/app/components/base/premium-badge/index.css

@@ -0,0 +1,48 @@
+@tailwind components;
+
+@layer components {
+  .premium-badge {
+    @apply inline-flex justify-center items-center rounded-full border box-border border-[rgba(255,255,255,0.8)] text-white
+  }
+
+  /* m is for the regular button */
+  .premium-badge-m {
+    @apply border shadow-lg !p-1 h-6 w-auto
+  }
+
+  .premium-badge-s {
+    @apply border-[0.5px] shadow-xs !px-1 !py-[3px] h-[18px] w-auto
+  }
+
+  .premium-badge-blue {
+    @apply bg-gradient-to-r from-[#5289ffe6] to-[#155aefe6] bg-util-colors-blue-blue-200
+  }
+
+  .premium-badge-indigo {
+    @apply bg-gradient-to-r from-[#8098f9e6] to-[#444ce7e6] bg-util-colors-indigo-indigo-200
+  }
+
+  .premium-badge-gray {
+    @apply bg-gradient-to-r from-[#98a2b2e6] to-[#676f83e6] bg-util-colors-gray-gray-200
+  }
+
+  .premium-badge-orange {
+    @apply bg-gradient-to-r from-[#ff692ee6] to-[#e04f16e6] bg-util-colors-orange-orange-200
+  }
+
+  .premium-badge-blue.allowHover:hover {
+    @apply bg-gradient-to-r from-[#296dffe6] to-[#004aebe6] bg-util-colors-blue-blue-300 cursor-pointer
+  }
+
+  .premium-badge-indigo.allowHover:hover {
+    @apply bg-gradient-to-r from-[#6172f3e6] to-[#2d31a6e6] bg-util-colors-indigo-indigo-300 cursor-pointer
+  }
+
+  .premium-badge-gray.allowHover:hover {
+    @apply bg-gradient-to-r from-[#676f83e6] to-[#354052e6] bg-util-colors-gray-gray-300 cursor-pointer
+  }
+
+  .premium-badge-orange.allowHover:hover {
+    @apply bg-gradient-to-r from-[#ff4405e6] to-[#b93815e6] bg-util-colors-orange-orange-300 cursor-pointer
+  }
+}

+ 78 - 0
web/app/components/base/premium-badge/index.tsx

@@ -0,0 +1,78 @@
+import type { CSSProperties, ReactNode } from 'react'
+import React from 'react'
+import { type VariantProps, cva } from 'class-variance-authority'
+import { Highlight } from '@/app/components/base/icons/src/public/common'
+import classNames from '@/utils/classnames'
+import './index.css'
+
+const PremiumBadgeVariants = cva(
+  'premium-badge',
+  {
+    variants: {
+      size: {
+        s: 'premium-badge-s',
+        m: 'premium-badge-m',
+      },
+      color: {
+        blue: 'premium-badge-blue',
+        indigo: 'premium-badge-indigo',
+        gray: 'premium-badge-gray',
+        orange: 'premium-badge-orange',
+      },
+      allowHover: {
+        true: 'allowHover',
+        false: '',
+      },
+    },
+    defaultVariants: {
+      size: 'm',
+      color: 'blue',
+      allowHover: false,
+    },
+  },
+)
+
+type PremiumBadgeProps = {
+  size?: 's' | 'm'
+  color?: 'blue' | 'indigo' | 'gray' | 'orange'
+  allowHover?: boolean
+  styleCss?: CSSProperties
+  children?: ReactNode
+} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof PremiumBadgeVariants>
+
+const PremiumBadge: React.FC<PremiumBadgeProps> = ({
+  className,
+  size,
+  color,
+  allowHover,
+  styleCss,
+  children,
+  ...props
+}) => {
+  return (
+    <div
+      className={classNames(
+        PremiumBadgeVariants({ size, color, allowHover, className }),
+        'relative text-nowrap',
+      )}
+      style={styleCss}
+      {...props}
+    >
+      {children}
+      <Highlight
+        className={classNames(
+          'absolute top-0 opacity-50 hover:opacity-80',
+          size === 's' ? 'h-4.5 w-12' : 'h-6 w-12',
+        )}
+        style={{
+          right: '50%',
+          transform: 'translateX(10%)',
+        }}
+      />
+    </div>
+  )
+}
+PremiumBadge.displayName = 'PremiumBadge'
+
+export default PremiumBadge
+export { PremiumBadge, PremiumBadgeVariants }

+ 4 - 2
web/app/components/share/text-generation/index.tsx

@@ -644,10 +644,12 @@ const TextGeneration: FC<IMainProps> = ({
             isInstalledApp ? 'left-[248px]' : 'left-8',
             'fixed  bottom-4  flex space-x-2 text-gray-400 font-normal text-xs',
           )}>
-            <div className="">© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}</div>
+            {siteInfo.copyright && (
+              <div className="">© {(new Date()).getFullYear()} {siteInfo.copyright}</div>
+            )}
             {siteInfo.privacy_policy && (
               <>
-                <div>·</div>
+                {siteInfo.copyright && <div>·</div>}
                 <div>{t('share.chat.privacyPolicyLeft')}
                   <a
                     className='text-gray-500 px-1'

+ 5 - 2
web/i18n/en-US/app-overview.ts

@@ -38,7 +38,8 @@ const translation = {
       preUseReminder: 'Please enable WebApp before continuing.',
       settings: {
         entry: 'Settings',
-        title: 'WebApp Settings',
+        title: 'Web App Settings',
+        modalTip: 'Client-side web app settings. ',
         webName: 'WebApp Name',
         webDesc: 'WebApp Description',
         webDescTip: 'This text will be displayed on the client side, providing basic guidance on how to use the application',
@@ -56,7 +57,7 @@ const translation = {
         chatColorThemeInverted: 'Inverted',
         invalidHexMessage: 'Invalid hex value',
         sso: {
-          label: 'SSO Authentication',
+          label: 'SSO Enforcement',
           title: 'WebApp SSO',
           description: 'All users are required to login with SSO before using WebApp',
           tooltip: 'Contact the administrator to enable WebApp SSO',
@@ -64,6 +65,8 @@ const translation = {
         more: {
           entry: 'Show more settings',
           copyright: 'Copyright',
+          copyrightTip: 'Display copyright information in the webapp',
+          copyrightTooltip: 'Please upgrade to Professional plan or above',
           copyRightPlaceholder: 'Enter the name of the author or organization',
           privacyPolicy: 'Privacy Policy',
           privacyPolicyPlaceholder: 'Enter the privacy policy link',

+ 3 - 0
web/i18n/zh-Hans/app-overview.ts

@@ -39,6 +39,7 @@ const translation = {
       settings: {
         entry: '设置',
         title: 'WebApp 设置',
+        modalTip: '客户端 WebApp 设置。',
         webName: 'WebApp 名称',
         webDesc: 'WebApp 描述',
         webDescTip: '以下文字将展示在客户端中,对应用进行说明和使用上的基本引导',
@@ -64,6 +65,8 @@ const translation = {
         more: {
           entry: '展示更多设置',
           copyright: '版权',
+          copyrightTip: '在 WebApp 中展示版权信息',
+          copyrightTooltip: '请升级到专业版或者更高',
           copyRightPlaceholder: '请输入作者或组织名称',
           privacyPolicy: '隐私政策',
           privacyPolicyPlaceholder: '请输入隐私政策链接',