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

Co-authored-by: crazywoola <427733928@qq.com>
This commit is contained in:
Diego Romero-Lovo 2024-06-26 04:51:00 -05:00 committed by GitHub
parent 8fa6cb5e03
commit 4c0a31d38b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 379 additions and 18 deletions

View File

@ -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')
@ -55,6 +57,8 @@ class AppSite(Resource):
'icon_background',
'description',
'default_language',
'chat_color_theme',
'chat_color_theme_inverted',
'customize_domain',
'copyright',
'privacy_policy',

View File

@ -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,

View File

@ -111,6 +111,8 @@ site_fields = {
'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,

View File

@ -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

View File

@ -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 ###

View File

@ -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'))

View File

@ -22,4 +22,4 @@
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
}

View File

@ -226,6 +226,7 @@ const AppPublisher = ({
</div>
</PortalToFollowElemContent>
<EmbeddedModal
siteInfo={appDetail?.site}
isShow={embeddingModalOpen}
onClose={() => setEmbeddingModalOpen(false)}
appBaseUrl={appBaseURL}

View File

@ -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}

View File

@ -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
@ -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
@ -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}`,
@ -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') {
@ -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 })
}
@ -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>

View File

@ -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
@ -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
@ -40,6 +43,7 @@ export type ConfigParams = {
const prefixSettings = 'appOverview.overview.appInfo.settings'
const SettingsModal: FC<ISettingsModalProps> = ({
isChat,
appInfo,
isShow = false,
onClose,
@ -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()
@ -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])
@ -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,
@ -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 }))
}
}
@ -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>

View File

@ -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'
@ -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}

View File

@ -15,6 +15,8 @@ import type {
} from '../types'
import { TransferMethod } from '../types'
import { useChatWithHistoryContext } from '../chat-with-history/context'
import type { Theme } from '../embedded-chatbot/theme/theme-context'
import { CssTransform } from '../embedded-chatbot/theme/utils'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { ToastContext } from '@/app/components/base/toast'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@ -35,11 +37,13 @@ type ChatInputProps = {
visionConfig?: VisionConfig
speechToTextConfig?: EnableType
onSend?: OnSend
theme?: Theme | null
}
const ChatInput: FC<ChatInputProps> = ({
visionConfig,
speechToTextConfig,
onSend,
theme,
}) => {
const { appData } = useChatWithHistoryContext()
const { t } = useTranslation()
@ -112,14 +116,25 @@ const ChatInput: FC<ChatInputProps> = ({
})
}
const [isActiveIconFocused, setActiveIconFocused] = useState(false)
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const sendIconThemeStyle = theme
? {
color: (isActiveIconFocused || query || (query.trim() !== '')) ? theme.primaryColor : '#d1d5db',
}
: {}
const sendBtn = (
<div
className='group flex items-center justify-center w-8 h-8 rounded-lg hover:bg-[#EBF5FF] cursor-pointer'
onMouseEnter={() => setActiveIconFocused(true)}
onMouseLeave={() => setActiveIconFocused(false)}
onClick={handleSend}
style={isActiveIconFocused ? CssTransform(theme?.chatBubbleColorStyle ?? '') : {}}
>
<Send03
style={sendIconThemeStyle}
className={`
w-5 h-5 text-gray-300 group-hover:text-primary-600
${!!query.trim() && 'text-primary-600'}

View File

@ -19,6 +19,7 @@ import type {
Feedback,
OnSend,
} from '../types'
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
import Question from './question'
import Answer from './answer'
import ChatInput from './chat-input'
@ -58,7 +59,9 @@ export type ChatProps = {
chatAnswerContainerInner?: string
hideProcessDetail?: boolean
hideLogModal?: boolean
themeBuilder?: ThemeBuilder
}
const Chat: FC<ChatProps> = ({
appData,
config,
@ -85,6 +88,7 @@ const Chat: FC<ChatProps> = ({
chatAnswerContainerInner,
hideProcessDetail,
hideLogModal,
themeBuilder,
}) => {
const { t } = useTranslation()
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
@ -221,6 +225,7 @@ const Chat: FC<ChatProps> = ({
key={item.id}
item={item}
questionIcon={questionIcon}
theme={themeBuilder?.theme}
/>
)
})
@ -262,6 +267,7 @@ const Chat: FC<ChatProps> = ({
visionConfig={config?.file_upload?.image}
speechToTextConfig={config?.speech_to_text}
onSend={onSend}
theme={themeBuilder?.theme}
/>
)
}

View File

@ -6,6 +6,8 @@ import {
memo,
} from 'react'
import type { ChatItem } from '../types'
import type { Theme } from '../embedded-chatbot/theme/theme-context'
import { CssTransform } from '../embedded-chatbot/theme/utils'
import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general'
import { User } from '@/app/components/base/icons/src/public/avatar'
import { Markdown } from '@/app/components/base/markdown'
@ -14,10 +16,12 @@ import ImageGallery from '@/app/components/base/image-gallery'
type QuestionProps = {
item: ChatItem
questionIcon?: ReactNode
theme: Theme | null | undefined
}
const Question: FC<QuestionProps> = ({
item,
questionIcon,
theme,
}) => {
const {
content,
@ -25,12 +29,17 @@ const Question: FC<QuestionProps> = ({
} = item
const imgSrcs = message_files?.length ? message_files.map(item => item.url) : []
return (
<div className='flex justify-end mb-2 last:mb-0 pl-10'>
<div className='group relative mr-4'>
<QuestionTriangle className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' />
<div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'>
<QuestionTriangle
className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50'
style={theme ? { color: theme.chatBubbleColor } : {}}
/>
<div
className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
>
{
!!imgSrcs.length && (
<ImageGallery srcs={imgSrcs} />

View File

@ -32,6 +32,7 @@ const ChatWrapper = () => {
appMeta,
handleFeedback,
currentChatInstanceRef,
themeBuilder,
} = useEmbeddedChatbotContext()
const appConfig = useMemo(() => {
const config = appParams || {}
@ -130,6 +131,7 @@ const ChatWrapper = () => {
suggestedQuestions={suggestedQuestions}
answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null}
hideProcessDetail
themeBuilder={themeBuilder}
/>
)
}

View File

@ -2,6 +2,8 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { useEmbeddedChatbotContext } from '../context'
import { useThemeContext } from '../theme/theme-context'
import { CssTransform } from '../theme/utils'
import Form from './form'
import Button from '@/app/components/base/button'
import AppIcon from '@/app/components/base/app-icon'
@ -22,6 +24,7 @@ const ConfigPanel = () => {
const [collapsed, setCollapsed] = useState(true)
const customConfig = appData?.custom_config
const site = appData?.site
const themeBuilder = useThemeContext()
return (
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
@ -34,6 +37,7 @@ const ConfigPanel = () => {
)}
>
<div
style={CssTransform(themeBuilder.theme?.roundedBackgroundColorStyle ?? '')}
className={`
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
${isMobile && '!px-4 !py-3'}
@ -68,6 +72,7 @@ const ConfigPanel = () => {
{t('share.chat.configStatusDes')}
</div>
<Button
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
variant='secondary-accent'
size='small'
className='shrink-0'
@ -96,6 +101,7 @@ const ConfigPanel = () => {
<Form />
<div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
<Button
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
variant='primary'
className='mr-2'
onClick={() => {
@ -119,6 +125,7 @@ const ConfigPanel = () => {
<div className='p-6 rounded-b-xl'>
<Form />
<Button
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
className={cn(inputsForms.length && !isMobile && 'ml-[136px]')}
variant='primary'
size='large'

View File

@ -7,6 +7,7 @@ import type {
ChatItem,
Feedback,
} from '../types'
import type { ThemeBuilder } from './theme/theme-context'
import type {
AppConversationData,
AppData,
@ -40,6 +41,7 @@ export type EmbeddedChatbotContextValue = {
appId?: string
handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
themeBuilder?: ThemeBuilder
}
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({

View File

@ -2,18 +2,22 @@ import type { FC } from 'react'
import React from 'react'
import { RiRefreshLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { Theme } from './theme/theme-context'
import { CssTransform } from './theme/utils'
import Tooltip from '@/app/components/base/tooltip'
export type IHeaderProps = {
isMobile?: boolean
customerIcon?: React.ReactNode
title: string
theme?: Theme
onCreateNewChat?: () => void
}
const Header: FC<IHeaderProps> = ({
isMobile,
customerIcon,
title,
theme,
onCreateNewChat,
}) => {
const { t } = useTranslation()
@ -23,14 +27,15 @@ const Header: FC<IHeaderProps> = ({
return (
<div
className={`
shrink-0 flex items-center justify-between h-14 px-4 bg-gray-100
bg-gradient-to-r from-blue-600 to-sky-500
shrink-0 flex items-center justify-between h-14 px-4
`}
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
>
<div className="flex items-center space-x-2">
{customerIcon}
<div
className={'text-sm font-bold text-white'}
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
>
{title}
</div>
@ -43,7 +48,7 @@ const Header: FC<IHeaderProps> = ({
<div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => {
onCreateNewChat?.()
}}>
<RiRefreshLine className="h-4 w-4 text-sm font-bold text-white" />
<RiRefreshLine className="h-4 w-4 text-sm font-bold text-white" color={theme?.colorPathOnHeader}/>
</div>
</Tooltip>
</div>

View File

@ -10,6 +10,7 @@ import {
} from './context'
import { useEmbeddedChatbot } from './hooks'
import { isDify } from './utils'
import { useThemeContext } from './theme/theme-context'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@ -29,6 +30,7 @@ const Chatbot = () => {
showConfigPanelBeforeChat,
appChatListDataLoading,
handleNewConversation,
themeBuilder,
} = useEmbeddedChatbotContext()
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
@ -38,6 +40,7 @@ const Chatbot = () => {
const difyIcon = <LogoHeader />
useEffect(() => {
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
if (site) {
if (customConfig)
document.title = `${site.title}`
@ -63,6 +66,7 @@ const Chatbot = () => {
isMobile={isMobile}
title={site?.title || ''}
customerIcon={isDify() ? difyIcon : ''}
theme={themeBuilder?.theme}
onCreateNewChat={handleNewConversation}
/>
<div className='flex bg-white overflow-hidden'>
@ -87,6 +91,7 @@ const Chatbot = () => {
const EmbeddedChatbotWrapper = () => {
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const themeBuilder = useThemeContext()
const {
appInfoError,
@ -141,6 +146,7 @@ const EmbeddedChatbotWrapper = () => {
appId,
handleFeedback,
currentChatInstanceRef,
themeBuilder,
}}>
<Chatbot />
</EmbeddedChatbotContext.Provider>

View File

@ -0,0 +1,72 @@
import { createContext, useContext } from 'use-context-selector'
import { hexToRGBA } from './utils'
export class Theme {
public chatColorTheme: string | null
public chatColorThemeInverted: boolean
public primaryColor = '#1C64F2'
public backgroundHeaderColorStyle = 'backgroundImage: linear-gradient(to right, #2563eb, #0ea5e9)'
public headerBorderBottomStyle = ''
public colorFontOnHeaderStyle = 'color: white'
public colorPathOnHeader = 'white'
public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2'
public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)'
public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)'
public chatBubbleColor = 'rgb(225 239 254)'
constructor(chatColorTheme: string | null = null, chatColorThemeInverted = false) {
this.chatColorTheme = chatColorTheme
this.chatColorThemeInverted = chatColorThemeInverted
this.configCustomColor()
this.configInvertedColor()
}
private configCustomColor() {
if (this.chatColorTheme !== null && this.chatColorTheme !== '') {
this.primaryColor = this.chatColorTheme ?? '#1C64F2'
this.backgroundHeaderColorStyle = `backgroundColor: ${this.primaryColor}`
this.backgroundButtonDefaultColorStyle = `backgroundColor: ${this.primaryColor}`
this.roundedBackgroundColorStyle = `backgroundColor: ${hexToRGBA(this.primaryColor, 0.05)}`
this.chatBubbleColorStyle = `backgroundColor: ${hexToRGBA(this.primaryColor, 0.15)}`
this.chatBubbleColor = `${hexToRGBA(this.primaryColor, 0.15)}`
}
}
private configInvertedColor() {
if (this.chatColorThemeInverted) {
this.backgroundHeaderColorStyle = 'backgroundColor: #ffffff'
this.colorFontOnHeaderStyle = `color: ${this.primaryColor}`
this.headerBorderBottomStyle = 'borderBottom: 1px solid #ccc'
this.colorPathOnHeader = this.primaryColor
}
}
}
export class ThemeBuilder {
private _theme?: Theme
private buildChecker = false
public get theme() {
if (this._theme === undefined)
throw new Error('The theme should be built first and then accessed')
else
return this._theme
}
public buildTheme(chatColorTheme: string | null = null, chatColorThemeInverted = false) {
if (!this.buildChecker) {
this._theme = new Theme(chatColorTheme, chatColorThemeInverted)
this.buildChecker = true
}
else {
if (this.theme?.chatColorTheme !== chatColorTheme || this.theme?.chatColorThemeInverted !== chatColorThemeInverted) {
this._theme = new Theme(chatColorTheme, chatColorThemeInverted)
this.buildChecker = true
}
}
}
}
const ThemeContext = createContext<ThemeBuilder>(new ThemeBuilder())
export const useThemeContext = () => useContext(ThemeContext)

View File

@ -0,0 +1,29 @@
export function hexToRGBA(hex: string, opacity: number): string {
hex = hex.replace('#', '')
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
// Returning an RGB color object
return `rgba(${r},${g},${b},${opacity.toString()})`
}
/**
* Since strings cannot be directly assigned to the 'style' attribute in JSX,
* this method transforms the string into an object representation of the styles.
*/
export function CssTransform(cssString: string): object {
if (cssString.length === 0)
return {}
const style: object = {}
const propertyValuePairs = cssString.split(';')
for (const pair of propertyValuePairs) {
if (pair.trim().length > 0) {
const [property, value] = pair.split(':')
Object.assign(style, { [property.trim()]: value.trim() })
}
}
return style
}

View File

@ -33,7 +33,7 @@
"attributes": {
"d": "M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z",
"fill": "currentColor",
"fill-opacity": "0.5"
"fill-opacity": "0"
},
"children": []
}

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Anzeigen',
hide: 'Verbergen',
},
chatColorTheme: 'Chat-Farbschema',
chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest',
chatColorThemeInverted: 'Invertiert',
invalidHexMessage: 'Ungültiger Hex-Wert',
more: {
entry: 'Mehr Einstellungen anzeigen',
copyright: 'Urheberrecht',

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Show',
hide: 'Hide',
},
chatColorTheme: 'Chat color theme',
chatColorThemeDesc: 'Set the color theme of the chatbot',
chatColorThemeInverted: 'Inverted',
invalidHexMessage: 'Invalid hex value',
more: {
entry: 'Show more settings',
copyright: 'Copyright',

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Afficher',
hide: 'Masquer',
},
chatColorTheme: 'Thème de couleur du chatbot',
chatColorThemeDesc: 'Définir le thème de couleur du chatbot',
chatColorThemeInverted: 'Inversé',
invalidHexMessage: 'Valeur hexadécimale invalide',
more: {
entry: 'Afficher plus de paramètres',
copyright: 'Droits d\'auteur',

View File

@ -53,6 +53,10 @@ const translation = {
show: 'दिखाएं',
hide: 'छुपाएं',
},
chatColorTheme: 'चैटबॉट का रंग थीम',
chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें',
chatColorThemeInverted: 'उल्टा',
invalidHexMessage: 'अमान्य हेक्स मान',
more: {
entry: 'अधिक सेटिंग्स दिखाएं',
copyright: 'कॉपीराइट',

View File

@ -49,6 +49,10 @@ const translation = {
show: '表示',
hide: '非表示',
},
chatColorTheme: 'チャットボットのカラーテーマ',
chatColorThemeDesc: 'チャットボットのカラーテーマを設定します',
chatColorThemeInverted: '反転',
invalidHexMessage: '無効な16進数値',
more: {
entry: 'その他の設定を表示',
copyright: '著作権',

View File

@ -49,6 +49,10 @@ const translation = {
show: '표시',
hide: '숨기기',
},
chatColorTheme: '챗봇 색상 테마',
chatColorThemeDesc: '챗봇의 색상 테마를 설정하세요',
chatColorThemeInverted: '반전',
invalidHexMessage: '잘못된 16진수 값',
more: {
entry: '추가 설정 보기',
copyright: '저작권',

View File

@ -53,6 +53,10 @@ const translation = {
show: 'Pokaż',
hide: 'Ukryj',
},
chatColorTheme: 'Motyw kolorystyczny czatu',
chatColorThemeDesc: 'Ustaw motyw kolorystyczny czatu',
chatColorThemeInverted: 'Odwrócony',
invalidHexMessage: 'Nieprawidłowa wartość heksadecymalna',
more: {
entry: 'Pokaż więcej ustawień',
copyright: 'Prawa autorskie',

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Mostrar',
hide: 'Ocultar',
},
chatColorTheme: 'Tema de cor do chatbot',
chatColorThemeDesc: 'Defina o tema de cor do chatbot',
chatColorThemeInverted: 'Inve',
invalidHexMessage: 'Valor hex inválido',
more: {
entry: 'Mostrar mais configurações',
copyright: 'Direitos autorais',

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Afișați',
hide: 'Ascundeți',
},
chatColorTheme: 'Tema de culoare a chatului',
chatColorThemeDesc: 'Setați tema de culoare a chatbotului',
chatColorThemeInverted: 'Inversat',
invalidHexMessage: 'Valoare hex nevalidă',
more: {
entry: 'Afișați mai multe setări',
copyright: 'Drepturi de autor',

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Показати',
hide: 'Приховати',
},
chatColorTheme: 'Тема кольору чату',
chatColorThemeDesc: 'Встановіть тему кольору чат-бота',
chatColorThemeInverted: 'Інвертовано',
invalidHexMessage: 'Недійсне шістнадцяткове значення',
more: {
entry: 'Показати додаткові налаштування',
copyright: 'Авторське право',

View File

@ -49,6 +49,10 @@ const translation = {
show: 'Hiển thị',
hide: 'Ẩn',
},
chatColorTheme: 'Chủ đề màu sắc trò chuyện',
chatColorThemeDesc: 'Thiết lập chủ đề màu sắc của chatbot',
chatColorThemeInverted: 'Đảo ngược',
invalidHexMessage: 'Giá trị không hợp lệ của hệ màu hex',
more: {
entry: 'Hiển thị thêm cài đặt',
copyright: 'Bản quyền',

View File

@ -49,6 +49,10 @@ const translation = {
show: '显示',
hide: '隐藏',
},
chatColorTheme: '聊天颜色主题',
chatColorThemeDesc: '设置聊天机器人的颜色主题',
chatColorThemeInverted: '反转',
invalidHexMessage: '无效的十六进制值',
more: {
entry: '展示更多设置',
copyright: '版权',

View File

@ -49,6 +49,10 @@ const translation = {
show: '展示',
hide: '隱藏',
},
chatColorTheme: '聊天顏色主題',
chatColorThemeDesc: '設定聊天機器人的顏色主題',
chatColorThemeInverted: '反轉',
invalidHexMessage: '無效的十六進制值',
more: {
entry: '展示更多設定',
copyright: '版權',

View File

@ -11,6 +11,8 @@ export type ConversationItem = {
export type SiteInfo = {
title: string
chat_color_theme?: string
chat_color_theme_inverted?: boolean
icon?: string
icon_background?: string
description?: string

View File

@ -246,6 +246,12 @@ export type SiteConfig = {
title: string
/** Application Description will be shown in the Client */
description: string
/** Define the color in hex for different elements of the chatbot, such as:
* The header, the button , etc.
*/
chat_color_theme: string
/** Invert the color of the theme set in chat_color_theme */
chat_color_theme_inverted: boolean
/** Author */
author: string
/** User Support Email Address */