Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR: #4048 - Add color customization to the chatbot #4885

Merged
merged 21 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/controllers/console/app/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def parse_app_site_args():
parser.add_argument('icon_background', type=str, required=False, location='json')
parser.add_argument('description', type=str, required=False, location='json')
parser.add_argument('default_language', type=supported_language, required=False, location='json')
parser.add_argument('chat_color_theme', type=str, required=False, location='json')
parser.add_argument('chat_color_theme_inverted', type=bool, required=False, location='json')
parser.add_argument('customize_domain', type=str, required=False, location='json')
parser.add_argument('copyright', type=str, required=False, location='json')
parser.add_argument('privacy_policy', type=str, required=False, location='json')
Expand Down Expand Up @@ -55,6 +57,8 @@ def post(self, app_model):
'icon_background',
'description',
'default_language',
'chat_color_theme',
'chat_color_theme_inverted',
'customize_domain',
'copyright',
'privacy_policy',
Expand Down
2 changes: 2 additions & 0 deletions api/controllers/web/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class AppSiteApi(WebApiResource):

site_fields = {
'title': fields.String,
'chat_color_theme': fields.String,
'chat_color_theme_inverted': fields.Boolean,
'icon': fields.String,
'icon_background': fields.String,
'description': fields.String,
Expand Down
2 changes: 2 additions & 0 deletions api/fields/app_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@
'icon_background': fields.String,
'description': fields.String,
'default_language': fields.String,
'chat_color_theme': fields.String,
'chat_color_theme_inverted': fields.Boolean,
'customize_domain': fields.String,
'copyright': fields.String,
'privacy_policy': fields.String,
Expand Down
22 changes: 22 additions & 0 deletions api/migrations/versions/63f9175e515b_merge_branches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""merge branches

Revision ID: 63f9175e515b
Revises: 2a3aebbbf4bb, b69ca54b9208
Create Date: 2024-06-26 09:46:36.573505

"""
import models as models

# revision identifiers, used by Alembic.
revision = '63f9175e515b'
down_revision = ('2a3aebbbf4bb', 'b69ca54b9208')
branch_labels = None
depends_on = None


def upgrade():
pass


def downgrade():
pass
35 changes: 35 additions & 0 deletions api/migrations/versions/b69ca54b9208_add_chatbot_color_theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""add chatbot color theme

Revision ID: b69ca54b9208
Revises: 4ff534e1eb11
Create Date: 2024-06-25 01:14:21.523873

"""
import sqlalchemy as sa
from alembic import op

import models as models

# revision identifiers, used by Alembic.
revision = 'b69ca54b9208'
down_revision = '4ff534e1eb11'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('sites', schema=None) as batch_op:
batch_op.add_column(sa.Column('chat_color_theme', sa.String(length=255), nullable=True))
batch_op.add_column(sa.Column('chat_color_theme_inverted', sa.Boolean(), server_default=sa.text('false'), nullable=False))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('sites', schema=None) as batch_op:
batch_op.drop_column('chat_color_theme_inverted')
batch_op.drop_column('chat_color_theme')

# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions api/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,8 @@ class Site(db.Model):
icon_background = db.Column(db.String(255))
description = db.Column(db.Text)
default_language = db.Column(db.String(255), nullable=False)
chat_color_theme = db.Column(db.String(255))
chat_color_theme_inverted = db.Column(db.Boolean, nullable=False, server_default=db.text('false'))
copyright = db.Column(db.String(255))
privacy_policy = db.Column(db.String(255))
show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text('true'))
Expand Down
2 changes: 1 addition & 1 deletion web/.vscode/settings.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
}
1 change: 1 addition & 0 deletions web/app/components/app/app-publisher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ const AppPublisher = ({
</div>
</PortalToFollowElemContent>
<EmbeddedModal
siteInfo={appDetail?.site}
isShow={embeddingModalOpen}
onClose={() => setEmbeddingModalOpen(false)}
appBaseUrl={appBaseURL}
Expand Down
2 changes: 2 additions & 0 deletions web/app/components/app/overview/appCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,14 @@ function AppCard({
? (
<>
<SettingsModal
isChat={appMode === 'chat'}
appInfo={appInfo}
isShow={showSettingsModal}
onClose={() => setShowSettingsModal(false)}
onSave={onSaveSiteConfig}
/>
<EmbeddedModal
siteInfo={appInfo.site}
isShow={showEmbedded}
onClose={() => setShowEmbedded(false)}
appBaseUrl={app_base_url}
Expand Down
20 changes: 15 additions & 5 deletions web/app/components/app/overview/embedded/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import copyStyle from '@/app/components/base/copy-btn/style.module.css'
import Tooltip from '@/app/components/base/tooltip'
import { useAppContext } from '@/context/app-context'
import { IS_CE_EDITION } from '@/config'
import type { SiteInfo } from '@/models/share'
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'

type Props = {
siteInfo?: SiteInfo
isShow: boolean
onClose: () => void
accessToken: string
Expand All @@ -28,7 +31,7 @@ const OPTION_MAP = {
</iframe>`,
},
scripts: {
getContent: (url: string, token: string, isTestEnv?: boolean) =>
getContent: (url: string, token: string, primaryColor: string, isTestEnv?: boolean) =>
`<script>
window.difyChatbotConfig = {
token: '${token}'${isTestEnv
Expand All @@ -44,7 +47,12 @@ const OPTION_MAP = {
src="${url}/embed.min.js"
id="${token}"
defer>
</script>`,
</script>
<style>
#dify-chatbot-bubble-button {
background-color: ${primaryColor} !important;
}
</style>`,
},
chromePlugin: {
getContent: (url: string, token: string) => `ChatBot URL: ${url}/chatbot/${token}`,
Expand All @@ -60,12 +68,14 @@ type OptionStatus = {
chromePlugin: boolean
}

const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
const { t } = useTranslation()
const [option, setOption] = useState<Option>('iframe')
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })

const { langeniusVersionInfo } = useAppContext()
const themeBuilder = useThemeContext()
themeBuilder.buildTheme(siteInfo?.chat_color_theme ?? null, siteInfo?.chat_color_theme_inverted ?? false)
const isTestEnv = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
const onClickCopy = () => {
if (option === 'chromePlugin') {
Expand All @@ -74,7 +84,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props
copy(splitUrl[1])
}
else {
copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv))
copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv))
}
setIsCopied({ ...isCopied, [option]: true })
}
Expand Down Expand Up @@ -154,7 +164,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props
</div>
<div className="flex items-start justify-start w-full gap-2 p-3 overflow-x-auto">
<div className="grow shrink basis-0 text-slate-700 text-[13px] leading-tight font-mono">
<pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv)}</pre>
<pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre>
</div>
</div>
</div>
Expand Down
73 changes: 69 additions & 4 deletions web/app/components/app/overview/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useToastContext } from '@/app/components/base/toast'
import { languages } from '@/i18n/language'

export type ISettingsModalProps = {
isChat: boolean
appInfo: AppDetailResponse
isShow: boolean
defaultValue?: string
Expand All @@ -28,6 +29,8 @@ export type ConfigParams = {
title: string
description: string
default_language: string
chat_color_theme: string
chat_color_theme_inverted: boolean
prompt_public: boolean
copyright: string
privacy_policy: string
Expand All @@ -40,6 +43,7 @@ export type ConfigParams = {
const prefixSettings = 'appOverview.overview.appInfo.settings'

const SettingsModal: FC<ISettingsModalProps> = ({
isChat,
appInfo,
isShow = false,
onClose,
Expand All @@ -48,8 +52,27 @@ const SettingsModal: FC<ISettingsModalProps> = ({
const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false)
const { icon, icon_background } = appInfo
const { title, description, copyright, privacy_policy, custom_disclaimer, default_language, show_workflow_steps } = appInfo.site
const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps })
const {
title,
description,
chat_color_theme,
chat_color_theme_inverted,
copyright,
privacy_policy,
custom_disclaimer,
default_language,
show_workflow_steps,
} = appInfo.site
const [inputInfo, setInputInfo] = useState({
title,
desc: description,
chatColorTheme: chat_color_theme,
chatColorThemeInverted: chat_color_theme_inverted,
copyright,
privacyPolicy: privacy_policy,
customDisclaimer: custom_disclaimer,
show_workflow_steps,
})
const [language, setLanguage] = useState(default_language)
const [saveLoading, setSaveLoading] = useState(false)
const { t } = useTranslation()
Expand All @@ -58,7 +81,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
const [emoji, setEmoji] = useState({ icon, icon_background })

useEffect(() => {
setInputInfo({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps })
setInputInfo({
title,
desc: description,
chatColorTheme: chat_color_theme,
chatColorThemeInverted: chat_color_theme_inverted,
copyright,
privacyPolicy: privacy_policy,
customDisclaimer: custom_disclaimer,
show_workflow_steps,
})
setLanguage(default_language)
setEmoji({ icon, icon_background })
}, [appInfo])
Expand All @@ -75,11 +107,30 @@ const SettingsModal: FC<ISettingsModalProps> = ({
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
return
}

const validateColorHex = (hex: string | null) => {
if (hex === null || hex.length === 0)
return true

const regex = /#([A-Fa-f0-9]{6})/
const check = regex.test(hex)
return check
}

if (inputInfo !== null) {
if (!validateColorHex(inputInfo.chatColorTheme)) {
notify({ type: 'error', message: t(`${prefixSettings}.invalidHexMessage`) })
return
}
}

setSaveLoading(true)
const params = {
title: inputInfo.title,
description: inputInfo.desc,
default_language: language,
chat_color_theme: inputInfo.chatColorTheme,
chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
prompt_public: false,
copyright: inputInfo.copyright,
privacy_policy: inputInfo.privacyPolicy,
Expand All @@ -95,7 +146,13 @@ const SettingsModal: FC<ISettingsModalProps> = ({

const onChange = (field: string) => {
return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setInputInfo(item => ({ ...item, [field]: e.target.value }))
let value: string | boolean
if (e.target.type === 'checkbox')
value = (e.target as HTMLInputElement).checked
else
value = e.target.value

setInputInfo(item => ({ ...item, [field]: value }))
}
}

Expand Down Expand Up @@ -144,6 +201,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onSelect={item => setInputInfo({ ...inputInfo, show_workflow_steps: item.value === 'true' })}
/>
</>}
{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={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
value={inputInfo.chatColorTheme ?? ''}
onChange={onChange('chatColorTheme')}
placeholder= 'E.g #A020F0'
/>
</>}
{!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>
Expand Down
5 changes: 4 additions & 1 deletion web/app/components/base/button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import classNames from 'classnames'
Expand Down Expand Up @@ -29,15 +30,17 @@ const buttonVariants = cva(

export type ButtonProps = {
loading?: boolean
styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, children, ...props }, ref) => {
({ className, variant, size, loading, styleCss, children, ...props }, ref) => {
return (
<button
type='button'
className={classNames(buttonVariants({ variant, size, className }))}
ref={ref}
style={styleCss}
{...props}
>
{children}
Expand Down
Loading