feat: custom webapp logo (#1766)

This commit is contained in:
zxhlyh
2023-12-18 16:25:37 +08:00
committed by GitHub
parent 65fd4b39ce
commit 5bb841935e
40 changed files with 888 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="colors">
<path id="Icon" d="M12 20.4722C13.0615 21.4223 14.4633 22 16 22C19.3137 22 22 19.3137 22 16C22 13.2331 20.1271 10.9036 17.5798 10.2102M6.42018 10.2102C3.87293 10.9036 2 13.2331 2 16C2 19.3137 4.68629 22 8 22C11.3137 22 14 19.3137 14 16C14 15.2195 13.851 14.4738 13.5798 13.7898M18 8C18 11.3137 15.3137 14 12 14C8.68629 14 6 11.3137 6 8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="message-dots-circle">
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 13.3283 2.25952 14.5985 2.73156 15.7608C2.77419 15.8658 2.79872 15.9264 2.81552 15.9711L2.82063 15.9849L2.82 15.9897C2.815 16.0266 2.80672 16.0769 2.79071 16.173L2.19294 19.7596C2.16612 19.9202 2.13611 20.0999 2.12433 20.256C2.11148 20.4261 2.10701 20.6969 2.22973 20.983C2.38144 21.3367 2.6633 21.6186 3.017 21.7703C3.30312 21.893 3.57386 21.8885 3.74404 21.8757C3.90013 21.8639 4.07985 21.8339 4.24049 21.8071L7.82705 21.2093C7.92309 21.1933 7.97339 21.185 8.0103 21.18L8.01505 21.1794L8.02887 21.1845C8.07362 21.2013 8.13423 21.2258 8.23921 21.2684C9.4015 21.7405 10.6717 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM6 12C6 11.1716 6.67157 10.5 7.5 10.5C8.32843 10.5 9 11.1716 9 12C9 12.8284 8.32843 13.5 7.5 13.5C6.67157 13.5 6 12.8284 6 12ZM10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5C11.1716 13.5 10.5 12.8284 10.5 12ZM16.5 10.5C15.6716 10.5 15 11.1716 15 12C15 12.8284 15.6716 13.5 16.5 13.5C17.3284 13.5 18 12.8284 18 12C18 11.1716 17.3284 10.5 16.5 10.5Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="colors">
<g id="Solid">
<path d="M13.4494 13.2298C12.9854 13.3409 12.5002 13.3999 12 13.3999C10.2804 13.3999 8.72326 12.6997 7.59953 11.5677C6.4872 10.4471 5.8 8.90382 5.8 7.20007C5.8 3.77586 8.57584 1 12 1C15.4241 1 18.2 3.77586 18.2 7.20007C18.2 8.44569 17.8327 9.60551 17.2005 10.5771C16.3665 11.8588 15.0715 12.8131 13.5506 13.2047C13.517 13.2133 13.4833 13.2217 13.4494 13.2298Z" fill="black"/>
<path d="M15.1476 14.7743C16.6646 14.1431 17.9513 13.0695 18.8465 11.7146C19.0004 11.4817 19.0773 11.3652 19.1762 11.3066C19.2615 11.2561 19.3659 11.2312 19.4648 11.2379C19.5795 11.2457 19.6773 11.3015 19.8728 11.4133C21.7413 12.4817 23 14.4946 23 16.7999C23 20.2241 20.2242 23 16.8 23C15.9123 23 15.0689 22.8139 14.3059 22.4782C14.0549 22.3678 13.9294 22.3126 13.8502 22.2049C13.7822 22.1126 13.7468 21.9922 13.7539 21.8777C13.7622 21.7444 13.8565 21.6018 14.045 21.3167C14.8373 20.1184 15.3234 18.6997 15.3917 17.1723C15.3969 17.0566 15.3996 16.9402 15.4 16.8233L15.4 16.7999C15.4 16.1888 15.333 15.5926 15.2057 15.0185C15.1876 14.9366 15.1682 14.8552 15.1476 14.7743Z" fill="black"/>
<path d="M4.12723 11.4133C4.32273 11.3015 4.42049 11.2457 4.53516 11.2379C4.63414 11.2312 4.73848 11.2561 4.82382 11.3066C4.92269 11.3652 4.99964 11.4817 5.15355 11.7146C6.62074 13.9352 9.13929 15.4001 12 15.4001C12.4146 15.4001 12.822 15.3694 13.2201 15.31L13.2263 15.3357C13.3398 15.8045 13.4 16.2947 13.4 16.7999L13.4 16.8214C13.3997 16.9056 13.3977 16.9895 13.3941 17.0728C13.2513 20.3704 10.5327 23 7.2 23C3.77584 23 1 20.2241 1 16.7999C1 14.4946 2.25869 12.4817 4.12723 11.4133Z" fill="black"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "colors"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M12 20.4722C13.0615 21.4223 14.4633 22 16 22C19.3137 22 22 19.3137 22 16C22 13.2331 20.1271 10.9036 17.5798 10.2102M6.42018 10.2102C3.87293 10.9036 2 13.2331 2 16C2 19.3137 4.68629 22 8 22C11.3137 22 14 19.3137 14 16C14 15.2195 13.851 14.4738 13.5798 13.7898M18 8C18 11.3137 15.3137 14 12 14C8.68629 14 6 11.3137 6 8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "Colors"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Colors.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 = 'Colors'
export default Icon

View File

@@ -1,2 +1,3 @@
export { default as BezierCurve03 } from './BezierCurve03'
export { default as Colors } from './Colors'
export { default as TypeSquare } from './TypeSquare'

View File

@@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "message-dots-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M12 2C6.47715 2 2 6.47715 2 12C2 13.3283 2.25952 14.5985 2.73156 15.7608C2.77419 15.8658 2.79872 15.9264 2.81552 15.9711L2.82063 15.9849L2.82 15.9897C2.815 16.0266 2.80672 16.0769 2.79071 16.173L2.19294 19.7596C2.16612 19.9202 2.13611 20.0999 2.12433 20.256C2.11148 20.4261 2.10701 20.6969 2.22973 20.983C2.38144 21.3367 2.6633 21.6186 3.017 21.7703C3.30312 21.893 3.57386 21.8885 3.74404 21.8757C3.90013 21.8639 4.07985 21.8339 4.24049 21.8071L7.82705 21.2093C7.92309 21.1933 7.97339 21.185 8.0103 21.18L8.01505 21.1794L8.02887 21.1845C8.07362 21.2013 8.13423 21.2258 8.23921 21.2684C9.4015 21.7405 10.6717 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM6 12C6 11.1716 6.67157 10.5 7.5 10.5C8.32843 10.5 9 11.1716 9 12C9 12.8284 8.32843 13.5 7.5 13.5C6.67157 13.5 6 12.8284 6 12ZM10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5C11.1716 13.5 10.5 12.8284 10.5 12ZM16.5 10.5C15.6716 10.5 15 11.1716 15 12C15 12.8284 15.6716 13.5 16.5 13.5C17.3284 13.5 18 12.8284 18 12C18 11.1716 17.3284 10.5 16.5 10.5Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "MessageDotsCircle"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './MessageDotsCircle.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 = 'MessageDotsCircle'
export default Icon

View File

@@ -1 +1,2 @@
export { default as MessageDotsCircle } from './MessageDotsCircle'
export { default as MessageFast } from './MessageFast'

View File

@@ -0,0 +1,62 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "colors"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Solid"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.4494 13.2298C12.9854 13.3409 12.5002 13.3999 12 13.3999C10.2804 13.3999 8.72326 12.6997 7.59953 11.5677C6.4872 10.4471 5.8 8.90382 5.8 7.20007C5.8 3.77586 8.57584 1 12 1C15.4241 1 18.2 3.77586 18.2 7.20007C18.2 8.44569 17.8327 9.60551 17.2005 10.5771C16.3665 11.8588 15.0715 12.8131 13.5506 13.2047C13.517 13.2133 13.4833 13.2217 13.4494 13.2298Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M15.1476 14.7743C16.6646 14.1431 17.9513 13.0695 18.8465 11.7146C19.0004 11.4817 19.0773 11.3652 19.1762 11.3066C19.2615 11.2561 19.3659 11.2312 19.4648 11.2379C19.5795 11.2457 19.6773 11.3015 19.8728 11.4133C21.7413 12.4817 23 14.4946 23 16.7999C23 20.2241 20.2242 23 16.8 23C15.9123 23 15.0689 22.8139 14.3059 22.4782C14.0549 22.3678 13.9294 22.3126 13.8502 22.2049C13.7822 22.1126 13.7468 21.9922 13.7539 21.8777C13.7622 21.7444 13.8565 21.6018 14.045 21.3167C14.8373 20.1184 15.3234 18.6997 15.3917 17.1723C15.3969 17.0566 15.3996 16.9402 15.4 16.8233L15.4 16.7999C15.4 16.1888 15.333 15.5926 15.2057 15.0185C15.1876 14.9366 15.1682 14.8552 15.1476 14.7743Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.12723 11.4133C4.32273 11.3015 4.42049 11.2457 4.53516 11.2379C4.63414 11.2312 4.73848 11.2561 4.82382 11.3066C4.92269 11.3652 4.99964 11.4817 5.15355 11.7146C6.62074 13.9352 9.13929 15.4001 12 15.4001C12.4146 15.4001 12.822 15.3694 13.2201 15.31L13.2263 15.3357C13.3398 15.8045 13.4 16.2947 13.4 16.7999L13.4 16.8214C13.3997 16.9056 13.3977 16.9895 13.3941 17.0728C13.2513 20.3704 10.5327 23 7.2 23C3.77584 23 1 20.2241 1 16.7999C1 14.4946 2.25869 12.4817 4.12723 11.4133Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Colors"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Colors.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 = 'Colors'
export default Icon

View File

@@ -1,4 +1,5 @@
export { default as Brush01 } from './Brush01'
export { default as Citations } from './Citations'
export { default as Colors } from './Colors'
export { default as Paragraph } from './Paragraph'
export { default as TypeSquare } from './TypeSquare'

View File

@@ -6,13 +6,13 @@ type ImageUploadParams = {
onSuccessCallback: (res: { id: string }) => void
onErrorCallback: () => void
}
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean) => void
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean, url?: string) => void
export const imageUpload: ImageUpload = ({
file,
onProgressCallback,
onSuccessCallback,
onErrorCallback,
}, isPublic) => {
}, isPublic, url) => {
const formData = new FormData()
formData.append('file', file)
const onProgress = (e: ProgressEvent) => {
@@ -26,7 +26,7 @@ export const imageUpload: ImageUpload = ({
xhr: new XMLHttpRequest(),
data: formData,
onprogress: onProgress,
}, isPublic)
}, isPublic, url)
.then((res: { id: string }) => {
onSuccessCallback(res)
})

View File

@@ -0,0 +1,70 @@
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import Button from '@/app/components/base/button'
import { Grid01 } from '@/app/components/base/icons/src/vender/solid/layout'
import { Container, Database01 } from '@/app/components/base/icons/src/vender/line/development'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
const CustomAppHeaderBrand = () => {
const { t } = useTranslation()
const { plan } = useProviderContext()
return (
<div className='py-3'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.app.title')}</div>
<div className='relative mb-4 rounded-xl bg-gray-100 border-[0.5px] border-black/[0.08] shadow-xs'>
<div className={`${s.mask} absolute inset-0 rounded-xl`}></div>
<div className='flex items-center pl-5 h-14 rounded-t-xl'>
<div className='relative flex items-center mr-[199px] w-[120px] h-10 bg-[rgba(217,45,32,0.12)]'>
<div className='ml-[1px] mr-[3px] w-[34px] h-[34px] border-8 border-black/[0.16] rounded-full'></div>
<div className='text-[13px] font-bold text-black/[0.24]'>YOUR LOGO</div>
<div className='absolute top-0 bottom-0 left-0.5 w-[0.5px] bg-[#F97066] opacity-50'></div>
<div className='absolute top-0 bottom-0 right-0.5 w-[0.5px] bg-[#F97066] opacity-50'></div>
<div className='absolute left-0 right-0 top-0.5 h-[0.5px] bg-[#F97066] opacity-50'></div>
<div className='absolute left-0 right-0 bottom-0.5 h-[0.5px] bg-[#F97066] opacity-50'></div>
</div>
<div className='flex items-center mr-3 px-3 h-7 rounded-xl bg-white shadow-xs'>
<Grid01 className='shrink-0 mr-2 w-4 h-4 text-[#155eef]' />
<div className='w-12 h-1.5 rounded-[5px] bg-[#155eef] opacity-80'></div>
</div>
<div className='flex items-center mr-3 px-3 h-7'>
<Container className='shrink-0 mr-2 w-4 h-4 text-gray-500' />
<div className='w-[50px] h-1.5 rounded-[5px] bg-gray-300'></div>
</div>
<div className='flex items-center px-3 h-7'>
<Database01 className='shrink-0 mr-2 w-4 h-4 text-gray-500' />
<div className='w-14 h-1.5 rounded-[5px] bg-gray-300 opacity-80'></div>
</div>
</div>
<div className='h-8 border-t border-t-gray-200 rounded-b-xl'></div>
</div>
<div className='flex items-center mb-2'>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`}
disabled={plan.type === Plan.sandbox}
>
<ImagePlus className='mr-2 w-4 h-4' />
{t('custom.upload')}
</Button>
<div className='mx-2 h-5 w-[1px] bg-black/5'></div>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
`}
disabled={plan.type === Plan.sandbox}
>
{t('custom.restore')}
</Button>
</div>
<div className='text-xs text-gray-500'>{t('custom.app.changeLogoTip')}</div>
</div>
)
}
export default CustomAppHeaderBrand

View File

@@ -0,0 +1,3 @@
.mask {
background: linear-gradient(95deg, rgba(255, 255, 255, 0.00) 43.9%, rgba(255, 255, 255, 0.80) 95.76%); ;
}

View File

@@ -0,0 +1,52 @@
import { useTranslation } from 'react-i18next'
import CustomWebAppBrand from '../custom-web-app-brand'
import CustomAppHeaderBrand from '../custom-app-header-brand'
import s from '../style.module.css'
import GridMask from '@/app/components/base/grid-mask'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import { contactSalesUrl } from '@/app/components/billing/config'
const CustomPage = () => {
const { t } = useTranslation()
const { plan } = useProviderContext()
return (
<div className='flex flex-col'>
{
plan.type === Plan.sandbox && (
<GridMask canvasClassName='!rounded-xl'>
<div className='flex justify-between mb-1 px-6 py-5 h-[88px] shadow-md rounded-xl border-[0.5px] border-gray-200'>
<div className={`${s.textGradient} leading-[24px] text-base font-semibold`}>
<div>{t('custom.upgradeTip.prefix')}</div>
<div>{t('custom.upgradeTip.suffix')}</div>
</div>
<UpgradeBtn />
</div>
</GridMask>
)
}
<CustomWebAppBrand />
{
plan.type === Plan.sandbox && (
<>
<div className='my-2 h-[0.5px] bg-gray-100'></div>
<CustomAppHeaderBrand />
</>
)
}
{
(plan.type === Plan.professional || plan.type === Plan.team) && (
<div className='absolute bottom-0 h-[50px] leading-[50px] text-xs text-gray-500'>
{t('custom.customize.prefix')}
<a className='text-[#155EEF]' href={contactSalesUrl} target='_blank'>{t('custom.customize.contactUs')}</a>
{t('custom.customize.suffix')}
</div>
)
}
</div>
)
}
export default CustomPage

View File

@@ -0,0 +1,234 @@
import type { ChangeEvent } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import LogoSite from '@/app/components/base/logo/logo-site'
import Switch from '@/app/components/base/switch'
import Button from '@/app/components/base/button'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import { imageUpload } from '@/app/components/base/image-uploader/utils'
import type {} from '@/app/components/base/image-uploader/utils'
import { useToastContext } from '@/app/components/base/toast'
import {
updateCurrentWorkspace,
} from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { API_PREFIX } from '@/config'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
const CustomWebAppBrand = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { plan } = useProviderContext()
const {
currentWorkspace,
mutateCurrentWorkspace,
isCurrentWorkspaceManager,
} = useAppContext()
const [fileId, setFileId] = useState('')
const [uploadProgress, setUploadProgress] = useState(0)
const isSandbox = plan.type === Plan.sandbox
const uploading = uploadProgress > 0 && uploadProgress < 100
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
const webappBrandRemoved = currentWorkspace.custom_config?.remove_webapp_brand
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file)
return
if (file.size > 5 * 1024 * 1024) {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: 5 }) })
return
}
imageUpload({
file,
onProgressCallback: (progress) => {
setUploadProgress(progress)
},
onSuccessCallback: (res) => {
setUploadProgress(100)
setFileId(res.id)
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
setUploadProgress(-1)
},
}, false, '/workspaces/custom-config/webapp-logo/upload')
}
const handleApply = async () => {
await updateCurrentWorkspace({
url: '/workspaces/custom-config',
body: {
remove_webapp_brand: webappBrandRemoved,
replace_webapp_logo: fileId,
},
})
mutateCurrentWorkspace()
setFileId('')
}
const handleRestore = async () => {
await updateCurrentWorkspace({
url: '/workspaces/custom-config',
body: {
remove_webapp_brand: false,
replace_webapp_logo: null,
},
})
mutateCurrentWorkspace()
}
const handleSwitch = async (checked: boolean) => {
await updateCurrentWorkspace({
url: '/workspaces/custom-config',
body: {
remove_webapp_brand: checked,
replace_webapp_logo: webappLogo,
},
})
mutateCurrentWorkspace()
}
const handleCancel = () => {
setFileId('')
setUploadProgress(0)
}
return (
<div className='py-4'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.webapp.title')}</div>
<div className='relative mb-4 pl-4 pb-6 pr-[119px] rounded-xl border-[0.5px] border-black/[0.08] shadow-xs bg-gray-50 overflow-hidden'>
<div className={`${s.mask} absolute top-0 left-0 w-full -bottom-2 z-10`}></div>
<div className='flex items-center -mt-2 mb-4 p-6 bg-white rounded-xl'>
<div className='flex items-center px-4 w-[125px] h-9 rounded-lg bg-primary-600 border-[0.5px] border-primary-700 shadow-xs'>
<MessageDotsCircle className='shrink-0 mr-2 w-4 h-4 text-white' />
<div className='grow h-2 rounded-sm bg-white opacity-50' />
</div>
</div>
<div className='flex items-center h-5 justify-between'>
<div className='w-[369px] h-1.5 rounded-sm bg-gray-200 opacity-80' />
{
!webappBrandRemoved && (
<div className='flex items-center text-[10px] font-medium text-gray-400'>
POWERED BY
{
webappLogo
? <img key={webappLogo} src={`${API_PREFIX.slice(0, -12)}/files/workspaces/${currentWorkspace.id}/webapp-logo`} alt='logo' className='ml-2 block w-auto h-5' />
: <LogoSite className='ml-2 !h-5' />
}
</div>
)
}
</div>
</div>
<div className='flex items-center justify-between mb-2 px-4 h-14 rounded-xl border-[0.5px] border-gray-200 bg-gray-50 text-sm font-medium text-gray-900'>
{t('custom.webapp.removeBrand')}
<Switch
size='l'
defaultValue={webappBrandRemoved}
disabled={isSandbox || !isCurrentWorkspaceManager}
onChange={handleSwitch}
/>
</div>
<div className={`
flex items-center justify-between px-4 py-3 rounded-xl border-[0.5px] border-gray-200 bg-gray-50
${webappBrandRemoved && 'opacity-30'}
`}>
<div>
<div className='leading-5 text-sm font-medium text-gray-900'>{t('custom.webapp.changeLogo')}</div>
<div className='leading-[18px] text-xs text-gray-500'>{t('custom.webapp.changeLogoTip')}</div>
</div>
<div className='flex items-center'>
{
!uploading && (
<Button
className={`
relative mr-2 !h-8 !px-3 bg-white !text-[13px]
${isSandbox ? 'opacity-40' : ''}
`}
disabled={isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager}
>
<ImagePlus className='mr-2 w-4 h-4' />
{
(webappLogo || fileId)
? t('custom.change')
: t('custom.upload')
}
<input
className={`
absolute block inset-0 opacity-0 text-[0] w-full
${(isSandbox || webappBrandRemoved) ? 'cursor-not-allowed' : 'cursor-pointer'}
`}
onClick={e => (e.target as HTMLInputElement).value = ''}
type='file'
accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')}
onChange={handleChange}
disabled={isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager}
/>
</Button>
)
}
{
uploading && (
<Button
className='relative mr-2 !h-8 !px-3 bg-white !text-[13px] opacity-40'
disabled={true}
>
<Loading02 className='animate-spin mr-2 w-4 h-4' />
{t('custom.uploading')}
</Button>
)
}
{
fileId && (
<>
<Button
type='primary'
className='mr-2 !h-8 !px-3 !py-0 !text-[13px]'
onClick={handleApply}
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
>
{t('custom.apply')}
</Button>
<Button
className='mr-2 !h-8 !px-3 !text-[13px] bg-white'
onClick={handleCancel}
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
>
{t('common.operation.cancel')}
</Button>
</>
)
}
<div className='mr-2 h-5 w-[1px] bg-black/5'></div>
<Button
className={`
!h-8 !px-3 bg-white !text-[13px]
${isSandbox ? 'opacity-40' : ''}
`}
disabled={isSandbox || (!webappLogo && !webappBrandRemoved) || webappBrandRemoved || !isCurrentWorkspaceManager}
onClick={handleRestore}
>
{t('custom.restore')}
</Button>
</div>
</div>
{
uploadProgress === -1 && (
<div className='mt-2 text-xs text-[#D92D20]'>{t('custom.uploadedFail')}</div>
)
}
</div>
)
}
export default CustomWebAppBrand

View File

@@ -0,0 +1,3 @@
.mask {
background: linear-gradient(273deg, rgba(255, 255, 255, 0.00) 51.75%, rgba(255, 255, 255, 0.80) 115.32%);
}

View File

@@ -0,0 +1,6 @@
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

View File

@@ -14,6 +14,7 @@ import DataSourcePage from './data-source-page'
import ModelPage from './model-page'
import s from './index.module.css'
import BillingPage from '@/app/components/billing/billing-page'
import CustomPage from '@/app/components/custom/custom-page'
import Modal from '@/app/components/base/modal'
import {
Database03,
@@ -26,8 +27,11 @@ import { User01 as User01Solid, Users01 as Users01Solid } from '@/app/components
import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
import { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { Colors } from '@/app/components/base/icons/src/vender/line/editor'
import { Colors as ColorsSolid } from '@/app/components/base/icons/src/vender/solid/editor'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config'
const iconClassName = `
w-4 h-4 ml-3 mr-2
@@ -96,6 +100,12 @@ export default function AccountSetting({
icon: <Webhooks className={iconClassName} />,
activeIcon: <Webhooks className={iconClassName} />,
},
{
key: IS_CE_EDITION ? false : 'custom',
name: t('custom.custom'),
icon: <Colors className={iconClassName} />,
activeIcon: <ColorsSolid className={iconClassName} />,
},
].filter(item => !!item.key) as GroupItem[]
})()
@@ -206,6 +216,7 @@ export default function AccountSetting({
{activeMenu === 'data-source' && <DataSourcePage />}
{activeMenu === 'plugin' && <PluginPage />}
{activeMenu === 'api-based-extension' && <ApiBasedExtensionPage /> }
{activeMenu === 'custom' && <CustomPage /> }
</div>
</div>
</div>

View File

@@ -71,6 +71,7 @@ const Main: FC<IMainProps> = ({
const [inited, setInited] = useState<boolean>(false)
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<any>(null)
// in mobile, show sidebar by click button
const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false)
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
@@ -364,10 +365,11 @@ const Main: FC<IMainProps> = ({
(async () => {
try {
const [appData, conversationData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, plan, can_replace_logo }: any = appData
const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData
setAppId(appId)
setPlan(plan)
setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
const tempIsPublicVersion = siteInfo.prompt_public
setIsPublicVersion(tempIsPublicVersion)
const prompt_template = ''
@@ -752,6 +754,7 @@ const Main: FC<IMainProps> = ({
onInputsChange={setCurrInputs}
plan={plan}
canReplaceLogo={canReplaceLogo}
customConfig={customConfig}
></ConfigSence>
{

View File

@@ -27,6 +27,10 @@ export type IWelcomeProps = {
onInputsChange: (inputs: Record<string, any>) => void
plan?: string
canReplaceLogo?: boolean
customConfig?: {
remove_webapp_brand?: boolean
replace_webapp_logo?: string
}
}
const Welcome: FC<IWelcomeProps> = ({
@@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({
hasSetInputs,
isPublicVersion,
siteInfo,
plan,
promptConfig,
onStartChat,
canEidtInpus,
savedInputs,
onInputsChange,
canReplaceLogo,
customConfig,
}) => {
const { t } = useTranslation()
const hasVar = promptConfig.prompt_variables.length > 0
@@ -352,10 +355,20 @@ const Welcome: FC<IWelcomeProps> = ({
</div>
: <div>
</div>}
{!canReplaceLogo && <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
<span className='uppercase'>{t('share.chat.powerBy')}</span>
<FootLogo />
</a>}
{
customConfig?.remove_webapp_brand
? null
: (
<a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
<span className='uppercase'>{t('share.chat.powerBy')}</span>
{
customConfig?.replace_webapp_logo
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
: <FootLogo />
}
</a>
)
}
</div>
)}
</div>

View File

@@ -55,6 +55,7 @@ const Main: FC<IMainProps> = ({
const [inited, setInited] = useState<boolean>(false)
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<any>(null)
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
useEffect(() => {
if (siteInfo?.title) {
@@ -283,10 +284,11 @@ const Main: FC<IMainProps> = ({
(async () => {
try {
const [appData, conversationData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, plan, can_replace_logo }: any = appData
const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData
setAppId(appId)
setPlan(plan)
setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
const tempIsPublicVersion = siteInfo.prompt_public
setIsPublicVersion(tempIsPublicVersion)
const prompt_template = ''
@@ -592,6 +594,7 @@ const Main: FC<IMainProps> = ({
onInputsChange={setCurrInputs}
plan={plan}
canReplaceLogo={canReplaceLogo}
customConfig={customConfig}
></ConfigScene>
{
shouldReload && (

View File

@@ -27,6 +27,10 @@ export type IWelcomeProps = {
onInputsChange: (inputs: Record<string, any>) => void
plan: string
canReplaceLogo?: boolean
customConfig?: {
remove_webapp_brand?: boolean
replace_webapp_logo?: string
}
}
const Welcome: FC<IWelcomeProps> = ({
@@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({
hasSetInputs,
isPublicVersion,
siteInfo,
plan,
promptConfig,
onStartChat,
canEditInputs,
savedInputs,
onInputsChange,
canReplaceLogo,
customConfig,
}) => {
const { t } = useTranslation()
const hasVar = promptConfig.prompt_variables.length > 0
@@ -353,10 +356,20 @@ const Welcome: FC<IWelcomeProps> = ({
</div>
: <div>
</div>}
{!canReplaceLogo && <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
<span className='uppercase'>{t('share.chat.powerBy')}</span>
<FootLogo />
</a>}
{
customConfig?.remove_webapp_brand
? null
: (
<a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
<span className='uppercase'>{t('share.chat.powerBy')}</span>
{
customConfig?.replace_webapp_logo
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
: <FootLogo />
}
</a>
)
}
</div>
)}
</div>