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
38 changed files with 379 additions and 18 deletions

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>