mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-15 14:45:58 +08:00
Co-authored-by: crazywoola <427733928@qq.com>
This commit is contained in:
parent
8fa6cb5e03
commit
4c0a31d38b
@ -20,6 +20,8 @@ def parse_app_site_args():
|
|||||||
parser.add_argument('icon_background', type=str, required=False, location='json')
|
parser.add_argument('icon_background', type=str, required=False, location='json')
|
||||||
parser.add_argument('description', 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('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('customize_domain', type=str, required=False, location='json')
|
||||||
parser.add_argument('copyright', 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')
|
parser.add_argument('privacy_policy', type=str, required=False, location='json')
|
||||||
@ -55,6 +57,8 @@ class AppSite(Resource):
|
|||||||
'icon_background',
|
'icon_background',
|
||||||
'description',
|
'description',
|
||||||
'default_language',
|
'default_language',
|
||||||
|
'chat_color_theme',
|
||||||
|
'chat_color_theme_inverted',
|
||||||
'customize_domain',
|
'customize_domain',
|
||||||
'copyright',
|
'copyright',
|
||||||
'privacy_policy',
|
'privacy_policy',
|
||||||
|
@ -26,6 +26,8 @@ class AppSiteApi(WebApiResource):
|
|||||||
|
|
||||||
site_fields = {
|
site_fields = {
|
||||||
'title': fields.String,
|
'title': fields.String,
|
||||||
|
'chat_color_theme': fields.String,
|
||||||
|
'chat_color_theme_inverted': fields.Boolean,
|
||||||
'icon': fields.String,
|
'icon': fields.String,
|
||||||
'icon_background': fields.String,
|
'icon_background': fields.String,
|
||||||
'description': fields.String,
|
'description': fields.String,
|
||||||
|
@ -111,6 +111,8 @@ site_fields = {
|
|||||||
'icon_background': fields.String,
|
'icon_background': fields.String,
|
||||||
'description': fields.String,
|
'description': fields.String,
|
||||||
'default_language': fields.String,
|
'default_language': fields.String,
|
||||||
|
'chat_color_theme': fields.String,
|
||||||
|
'chat_color_theme_inverted': fields.Boolean,
|
||||||
'customize_domain': fields.String,
|
'customize_domain': fields.String,
|
||||||
'copyright': fields.String,
|
'copyright': fields.String,
|
||||||
'privacy_policy': fields.String,
|
'privacy_policy': fields.String,
|
||||||
|
22
api/migrations/versions/63f9175e515b_merge_branches.py
Normal file
22
api/migrations/versions/63f9175e515b_merge_branches.py
Normal 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
|
@ -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 ###
|
@ -1042,6 +1042,8 @@ class Site(db.Model):
|
|||||||
icon_background = db.Column(db.String(255))
|
icon_background = db.Column(db.String(255))
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
default_language = db.Column(db.String(255), nullable=False)
|
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))
|
copyright = db.Column(db.String(255))
|
||||||
privacy_policy = 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'))
|
show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text('true'))
|
||||||
|
@ -226,6 +226,7 @@ const AppPublisher = ({
|
|||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemContent>
|
</PortalToFollowElemContent>
|
||||||
<EmbeddedModal
|
<EmbeddedModal
|
||||||
|
siteInfo={appDetail?.site}
|
||||||
isShow={embeddingModalOpen}
|
isShow={embeddingModalOpen}
|
||||||
onClose={() => setEmbeddingModalOpen(false)}
|
onClose={() => setEmbeddingModalOpen(false)}
|
||||||
appBaseUrl={appBaseURL}
|
appBaseUrl={appBaseURL}
|
||||||
|
@ -247,12 +247,14 @@ function AppCard({
|
|||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
|
isChat={appMode === 'chat'}
|
||||||
appInfo={appInfo}
|
appInfo={appInfo}
|
||||||
isShow={showSettingsModal}
|
isShow={showSettingsModal}
|
||||||
onClose={() => setShowSettingsModal(false)}
|
onClose={() => setShowSettingsModal(false)}
|
||||||
onSave={onSaveSiteConfig}
|
onSave={onSaveSiteConfig}
|
||||||
/>
|
/>
|
||||||
<EmbeddedModal
|
<EmbeddedModal
|
||||||
|
siteInfo={appInfo.site}
|
||||||
isShow={showEmbedded}
|
isShow={showEmbedded}
|
||||||
onClose={() => setShowEmbedded(false)}
|
onClose={() => setShowEmbedded(false)}
|
||||||
appBaseUrl={app_base_url}
|
appBaseUrl={app_base_url}
|
||||||
|
@ -8,8 +8,11 @@ import copyStyle from '@/app/components/base/copy-btn/style.module.css'
|
|||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
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 = {
|
type Props = {
|
||||||
|
siteInfo?: SiteInfo
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
accessToken: string
|
accessToken: string
|
||||||
@ -28,7 +31,7 @@ const OPTION_MAP = {
|
|||||||
</iframe>`,
|
</iframe>`,
|
||||||
},
|
},
|
||||||
scripts: {
|
scripts: {
|
||||||
getContent: (url: string, token: string, isTestEnv?: boolean) =>
|
getContent: (url: string, token: string, primaryColor: string, isTestEnv?: boolean) =>
|
||||||
`<script>
|
`<script>
|
||||||
window.difyChatbotConfig = {
|
window.difyChatbotConfig = {
|
||||||
token: '${token}'${isTestEnv
|
token: '${token}'${isTestEnv
|
||||||
@ -44,7 +47,12 @@ const OPTION_MAP = {
|
|||||||
src="${url}/embed.min.js"
|
src="${url}/embed.min.js"
|
||||||
id="${token}"
|
id="${token}"
|
||||||
defer>
|
defer>
|
||||||
</script>`,
|
</script>
|
||||||
|
<style>
|
||||||
|
#dify-chatbot-bubble-button {
|
||||||
|
background-color: ${primaryColor} !important;
|
||||||
|
}
|
||||||
|
</style>`,
|
||||||
},
|
},
|
||||||
chromePlugin: {
|
chromePlugin: {
|
||||||
getContent: (url: string, token: string) => `ChatBot URL: ${url}/chatbot/${token}`,
|
getContent: (url: string, token: string) => `ChatBot URL: ${url}/chatbot/${token}`,
|
||||||
@ -60,12 +68,14 @@ type OptionStatus = {
|
|||||||
chromePlugin: boolean
|
chromePlugin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
|
const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [option, setOption] = useState<Option>('iframe')
|
const [option, setOption] = useState<Option>('iframe')
|
||||||
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
|
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
|
||||||
|
|
||||||
const { langeniusVersionInfo } = useAppContext()
|
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 isTestEnv = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
|
||||||
const onClickCopy = () => {
|
const onClickCopy = () => {
|
||||||
if (option === 'chromePlugin') {
|
if (option === 'chromePlugin') {
|
||||||
@ -74,7 +84,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props
|
|||||||
copy(splitUrl[1])
|
copy(splitUrl[1])
|
||||||
}
|
}
|
||||||
else {
|
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 })
|
setIsCopied({ ...isCopied, [option]: true })
|
||||||
}
|
}
|
||||||
@ -154,7 +164,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-start w-full gap-2 p-3 overflow-x-auto">
|
<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">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||||||
import { languages } from '@/i18n/language'
|
import { languages } from '@/i18n/language'
|
||||||
|
|
||||||
export type ISettingsModalProps = {
|
export type ISettingsModalProps = {
|
||||||
|
isChat: boolean
|
||||||
appInfo: AppDetailResponse
|
appInfo: AppDetailResponse
|
||||||
isShow: boolean
|
isShow: boolean
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
@ -28,6 +29,8 @@ export type ConfigParams = {
|
|||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
default_language: string
|
default_language: string
|
||||||
|
chat_color_theme: string
|
||||||
|
chat_color_theme_inverted: boolean
|
||||||
prompt_public: boolean
|
prompt_public: boolean
|
||||||
copyright: string
|
copyright: string
|
||||||
privacy_policy: string
|
privacy_policy: string
|
||||||
@ -40,6 +43,7 @@ export type ConfigParams = {
|
|||||||
const prefixSettings = 'appOverview.overview.appInfo.settings'
|
const prefixSettings = 'appOverview.overview.appInfo.settings'
|
||||||
|
|
||||||
const SettingsModal: FC<ISettingsModalProps> = ({
|
const SettingsModal: FC<ISettingsModalProps> = ({
|
||||||
|
isChat,
|
||||||
appInfo,
|
appInfo,
|
||||||
isShow = false,
|
isShow = false,
|
||||||
onClose,
|
onClose,
|
||||||
@ -48,8 +52,27 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||||||
const { notify } = useToastContext()
|
const { notify } = useToastContext()
|
||||||
const [isShowMore, setIsShowMore] = useState(false)
|
const [isShowMore, setIsShowMore] = useState(false)
|
||||||
const { icon, icon_background } = appInfo
|
const { icon, icon_background } = appInfo
|
||||||
const { title, description, copyright, privacy_policy, custom_disclaimer, default_language, show_workflow_steps } = appInfo.site
|
const {
|
||||||
const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps })
|
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 [language, setLanguage] = useState(default_language)
|
||||||
const [saveLoading, setSaveLoading] = useState(false)
|
const [saveLoading, setSaveLoading] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -58,7 +81,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||||||
const [emoji, setEmoji] = useState({ icon, icon_background })
|
const [emoji, setEmoji] = useState({ icon, icon_background })
|
||||||
|
|
||||||
useEffect(() => {
|
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)
|
setLanguage(default_language)
|
||||||
setEmoji({ icon, icon_background })
|
setEmoji({ icon, icon_background })
|
||||||
}, [appInfo])
|
}, [appInfo])
|
||||||
@ -75,11 +107,30 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||||||
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
|
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
|
||||||
return
|
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)
|
setSaveLoading(true)
|
||||||
const params = {
|
const params = {
|
||||||
title: inputInfo.title,
|
title: inputInfo.title,
|
||||||
description: inputInfo.desc,
|
description: inputInfo.desc,
|
||||||
default_language: language,
|
default_language: language,
|
||||||
|
chat_color_theme: inputInfo.chatColorTheme,
|
||||||
|
chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
|
||||||
prompt_public: false,
|
prompt_public: false,
|
||||||
copyright: inputInfo.copyright,
|
copyright: inputInfo.copyright,
|
||||||
privacy_policy: inputInfo.privacyPolicy,
|
privacy_policy: inputInfo.privacyPolicy,
|
||||||
@ -95,7 +146,13 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||||||
|
|
||||||
const onChange = (field: string) => {
|
const onChange = (field: string) => {
|
||||||
return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
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' })}
|
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)}>
|
{!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
|
<div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { CSSProperties } from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { type VariantProps, cva } from 'class-variance-authority'
|
import { type VariantProps, cva } from 'class-variance-authority'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
@ -29,15 +30,17 @@ const buttonVariants = cva(
|
|||||||
|
|
||||||
export type ButtonProps = {
|
export type ButtonProps = {
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
styleCss?: CSSProperties
|
||||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
|
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, loading, children, ...props }, ref) => {
|
({ className, variant, size, loading, styleCss, children, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className={classNames(buttonVariants({ variant, size, className }))}
|
className={classNames(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
style={styleCss}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -15,6 +15,8 @@ import type {
|
|||||||
} from '../types'
|
} from '../types'
|
||||||
import { TransferMethod } from '../types'
|
import { TransferMethod } from '../types'
|
||||||
import { useChatWithHistoryContext } from '../chat-with-history/context'
|
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 TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
@ -35,11 +37,13 @@ type ChatInputProps = {
|
|||||||
visionConfig?: VisionConfig
|
visionConfig?: VisionConfig
|
||||||
speechToTextConfig?: EnableType
|
speechToTextConfig?: EnableType
|
||||||
onSend?: OnSend
|
onSend?: OnSend
|
||||||
|
theme?: Theme | null
|
||||||
}
|
}
|
||||||
const ChatInput: FC<ChatInputProps> = ({
|
const ChatInput: FC<ChatInputProps> = ({
|
||||||
visionConfig,
|
visionConfig,
|
||||||
speechToTextConfig,
|
speechToTextConfig,
|
||||||
onSend,
|
onSend,
|
||||||
|
theme,
|
||||||
}) => {
|
}) => {
|
||||||
const { appData } = useChatWithHistoryContext()
|
const { appData } = useChatWithHistoryContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -112,14 +116,25 @@ const ChatInput: FC<ChatInputProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [isActiveIconFocused, setActiveIconFocused] = useState(false)
|
||||||
|
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
const isMobile = media === MediaType.mobile
|
const isMobile = media === MediaType.mobile
|
||||||
|
const sendIconThemeStyle = theme
|
||||||
|
? {
|
||||||
|
color: (isActiveIconFocused || query || (query.trim() !== '')) ? theme.primaryColor : '#d1d5db',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
const sendBtn = (
|
const sendBtn = (
|
||||||
<div
|
<div
|
||||||
className='group flex items-center justify-center w-8 h-8 rounded-lg hover:bg-[#EBF5FF] cursor-pointer'
|
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}
|
onClick={handleSend}
|
||||||
|
style={isActiveIconFocused ? CssTransform(theme?.chatBubbleColorStyle ?? '') : {}}
|
||||||
>
|
>
|
||||||
<Send03
|
<Send03
|
||||||
|
style={sendIconThemeStyle}
|
||||||
className={`
|
className={`
|
||||||
w-5 h-5 text-gray-300 group-hover:text-primary-600
|
w-5 h-5 text-gray-300 group-hover:text-primary-600
|
||||||
${!!query.trim() && 'text-primary-600'}
|
${!!query.trim() && 'text-primary-600'}
|
||||||
|
@ -19,6 +19,7 @@ import type {
|
|||||||
Feedback,
|
Feedback,
|
||||||
OnSend,
|
OnSend,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||||
import Question from './question'
|
import Question from './question'
|
||||||
import Answer from './answer'
|
import Answer from './answer'
|
||||||
import ChatInput from './chat-input'
|
import ChatInput from './chat-input'
|
||||||
@ -58,7 +59,9 @@ export type ChatProps = {
|
|||||||
chatAnswerContainerInner?: string
|
chatAnswerContainerInner?: string
|
||||||
hideProcessDetail?: boolean
|
hideProcessDetail?: boolean
|
||||||
hideLogModal?: boolean
|
hideLogModal?: boolean
|
||||||
|
themeBuilder?: ThemeBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<ChatProps> = ({
|
const Chat: FC<ChatProps> = ({
|
||||||
appData,
|
appData,
|
||||||
config,
|
config,
|
||||||
@ -85,6 +88,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
chatAnswerContainerInner,
|
chatAnswerContainerInner,
|
||||||
hideProcessDetail,
|
hideProcessDetail,
|
||||||
hideLogModal,
|
hideLogModal,
|
||||||
|
themeBuilder,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
|
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
|
||||||
@ -221,6 +225,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
questionIcon={questionIcon}
|
questionIcon={questionIcon}
|
||||||
|
theme={themeBuilder?.theme}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -262,6 +267,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
visionConfig={config?.file_upload?.image}
|
visionConfig={config?.file_upload?.image}
|
||||||
speechToTextConfig={config?.speech_to_text}
|
speechToTextConfig={config?.speech_to_text}
|
||||||
onSend={onSend}
|
onSend={onSend}
|
||||||
|
theme={themeBuilder?.theme}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
memo,
|
memo,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import type { ChatItem } from '../types'
|
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 { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||||
import { User } from '@/app/components/base/icons/src/public/avatar'
|
import { User } from '@/app/components/base/icons/src/public/avatar'
|
||||||
import { Markdown } from '@/app/components/base/markdown'
|
import { Markdown } from '@/app/components/base/markdown'
|
||||||
@ -14,10 +16,12 @@ import ImageGallery from '@/app/components/base/image-gallery'
|
|||||||
type QuestionProps = {
|
type QuestionProps = {
|
||||||
item: ChatItem
|
item: ChatItem
|
||||||
questionIcon?: ReactNode
|
questionIcon?: ReactNode
|
||||||
|
theme: Theme | null | undefined
|
||||||
}
|
}
|
||||||
const Question: FC<QuestionProps> = ({
|
const Question: FC<QuestionProps> = ({
|
||||||
item,
|
item,
|
||||||
questionIcon,
|
questionIcon,
|
||||||
|
theme,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
content,
|
content,
|
||||||
@ -25,12 +29,17 @@ const Question: FC<QuestionProps> = ({
|
|||||||
} = item
|
} = item
|
||||||
|
|
||||||
const imgSrcs = message_files?.length ? message_files.map(item => item.url) : []
|
const imgSrcs = message_files?.length ? message_files.map(item => item.url) : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-end mb-2 last:mb-0 pl-10'>
|
<div className='flex justify-end mb-2 last:mb-0 pl-10'>
|
||||||
<div className='group relative mr-4'>
|
<div className='group relative mr-4'>
|
||||||
<QuestionTriangle className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' />
|
<QuestionTriangle
|
||||||
<div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'>
|
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 && (
|
!!imgSrcs.length && (
|
||||||
<ImageGallery srcs={imgSrcs} />
|
<ImageGallery srcs={imgSrcs} />
|
||||||
|
@ -32,6 +32,7 @@ const ChatWrapper = () => {
|
|||||||
appMeta,
|
appMeta,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
|
themeBuilder,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
const appConfig = useMemo(() => {
|
const appConfig = useMemo(() => {
|
||||||
const config = appParams || {}
|
const config = appParams || {}
|
||||||
@ -130,6 +131,7 @@ const ChatWrapper = () => {
|
|||||||
suggestedQuestions={suggestedQuestions}
|
suggestedQuestions={suggestedQuestions}
|
||||||
answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null}
|
answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null}
|
||||||
hideProcessDetail
|
hideProcessDetail
|
||||||
|
themeBuilder={themeBuilder}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import { useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { useEmbeddedChatbotContext } from '../context'
|
import { useEmbeddedChatbotContext } from '../context'
|
||||||
|
import { useThemeContext } from '../theme/theme-context'
|
||||||
|
import { CssTransform } from '../theme/utils'
|
||||||
import Form from './form'
|
import Form from './form'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
@ -22,6 +24,7 @@ const ConfigPanel = () => {
|
|||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
const customConfig = appData?.custom_config
|
const customConfig = appData?.custom_config
|
||||||
const site = appData?.site
|
const site = appData?.site
|
||||||
|
const themeBuilder = useThemeContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
|
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
|
||||||
@ -34,6 +37,7 @@ const ConfigPanel = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
style={CssTransform(themeBuilder.theme?.roundedBackgroundColorStyle ?? '')}
|
||||||
className={`
|
className={`
|
||||||
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
|
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
|
||||||
${isMobile && '!px-4 !py-3'}
|
${isMobile && '!px-4 !py-3'}
|
||||||
@ -68,6 +72,7 @@ const ConfigPanel = () => {
|
|||||||
{t('share.chat.configStatusDes')}
|
{t('share.chat.configStatusDes')}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||||
variant='secondary-accent'
|
variant='secondary-accent'
|
||||||
size='small'
|
size='small'
|
||||||
className='shrink-0'
|
className='shrink-0'
|
||||||
@ -96,6 +101,7 @@ const ConfigPanel = () => {
|
|||||||
<Form />
|
<Form />
|
||||||
<div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
|
<div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
|
||||||
<Button
|
<Button
|
||||||
|
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||||
variant='primary'
|
variant='primary'
|
||||||
className='mr-2'
|
className='mr-2'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -119,6 +125,7 @@ const ConfigPanel = () => {
|
|||||||
<div className='p-6 rounded-b-xl'>
|
<div className='p-6 rounded-b-xl'>
|
||||||
<Form />
|
<Form />
|
||||||
<Button
|
<Button
|
||||||
|
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||||
className={cn(inputsForms.length && !isMobile && 'ml-[136px]')}
|
className={cn(inputsForms.length && !isMobile && 'ml-[136px]')}
|
||||||
variant='primary'
|
variant='primary'
|
||||||
size='large'
|
size='large'
|
||||||
|
@ -7,6 +7,7 @@ import type {
|
|||||||
ChatItem,
|
ChatItem,
|
||||||
Feedback,
|
Feedback,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
import type { ThemeBuilder } from './theme/theme-context'
|
||||||
import type {
|
import type {
|
||||||
AppConversationData,
|
AppConversationData,
|
||||||
AppData,
|
AppData,
|
||||||
@ -40,6 +41,7 @@ export type EmbeddedChatbotContextValue = {
|
|||||||
appId?: string
|
appId?: string
|
||||||
handleFeedback: (messageId: string, feedback: Feedback) => void
|
handleFeedback: (messageId: string, feedback: Feedback) => void
|
||||||
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
||||||
|
themeBuilder?: ThemeBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
||||||
|
@ -2,18 +2,22 @@ import type { FC } from 'react'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { RiRefreshLine } from '@remixicon/react'
|
import { RiRefreshLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { Theme } from './theme/theme-context'
|
||||||
|
import { CssTransform } from './theme/utils'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
|
||||||
export type IHeaderProps = {
|
export type IHeaderProps = {
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
customerIcon?: React.ReactNode
|
customerIcon?: React.ReactNode
|
||||||
title: string
|
title: string
|
||||||
|
theme?: Theme
|
||||||
onCreateNewChat?: () => void
|
onCreateNewChat?: () => void
|
||||||
}
|
}
|
||||||
const Header: FC<IHeaderProps> = ({
|
const Header: FC<IHeaderProps> = ({
|
||||||
isMobile,
|
isMobile,
|
||||||
customerIcon,
|
customerIcon,
|
||||||
title,
|
title,
|
||||||
|
theme,
|
||||||
onCreateNewChat,
|
onCreateNewChat,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -23,14 +27,15 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
shrink-0 flex items-center justify-between h-14 px-4 bg-gray-100
|
shrink-0 flex items-center justify-between h-14 px-4
|
||||||
bg-gradient-to-r from-blue-600 to-sky-500
|
|
||||||
`}
|
`}
|
||||||
|
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{customerIcon}
|
{customerIcon}
|
||||||
<div
|
<div
|
||||||
className={'text-sm font-bold text-white'}
|
className={'text-sm font-bold text-white'}
|
||||||
|
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</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={() => {
|
<div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => {
|
||||||
onCreateNewChat?.()
|
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>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from './context'
|
} from './context'
|
||||||
import { useEmbeddedChatbot } from './hooks'
|
import { useEmbeddedChatbot } from './hooks'
|
||||||
import { isDify } from './utils'
|
import { isDify } from './utils'
|
||||||
|
import { useThemeContext } from './theme/theme-context'
|
||||||
import { checkOrSetAccessToken } from '@/app/components/share/utils'
|
import { checkOrSetAccessToken } from '@/app/components/share/utils'
|
||||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
@ -29,6 +30,7 @@ const Chatbot = () => {
|
|||||||
showConfigPanelBeforeChat,
|
showConfigPanelBeforeChat,
|
||||||
appChatListDataLoading,
|
appChatListDataLoading,
|
||||||
handleNewConversation,
|
handleNewConversation,
|
||||||
|
themeBuilder,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
|
|
||||||
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
|
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
|
||||||
@ -38,6 +40,7 @@ const Chatbot = () => {
|
|||||||
const difyIcon = <LogoHeader />
|
const difyIcon = <LogoHeader />
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
|
||||||
if (site) {
|
if (site) {
|
||||||
if (customConfig)
|
if (customConfig)
|
||||||
document.title = `${site.title}`
|
document.title = `${site.title}`
|
||||||
@ -63,6 +66,7 @@ const Chatbot = () => {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
title={site?.title || ''}
|
title={site?.title || ''}
|
||||||
customerIcon={isDify() ? difyIcon : ''}
|
customerIcon={isDify() ? difyIcon : ''}
|
||||||
|
theme={themeBuilder?.theme}
|
||||||
onCreateNewChat={handleNewConversation}
|
onCreateNewChat={handleNewConversation}
|
||||||
/>
|
/>
|
||||||
<div className='flex bg-white overflow-hidden'>
|
<div className='flex bg-white overflow-hidden'>
|
||||||
@ -87,6 +91,7 @@ const Chatbot = () => {
|
|||||||
const EmbeddedChatbotWrapper = () => {
|
const EmbeddedChatbotWrapper = () => {
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
const isMobile = media === MediaType.mobile
|
const isMobile = media === MediaType.mobile
|
||||||
|
const themeBuilder = useThemeContext()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
appInfoError,
|
appInfoError,
|
||||||
@ -141,6 +146,7 @@ const EmbeddedChatbotWrapper = () => {
|
|||||||
appId,
|
appId,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
|
themeBuilder,
|
||||||
}}>
|
}}>
|
||||||
<Chatbot />
|
<Chatbot />
|
||||||
</EmbeddedChatbotContext.Provider>
|
</EmbeddedChatbotContext.Provider>
|
||||||
|
@ -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)
|
29
web/app/components/base/chat/embedded-chatbot/theme/utils.ts
Normal file
29
web/app/components/base/chat/embedded-chatbot/theme/utils.ts
Normal 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
|
||||||
|
}
|
@ -33,7 +33,7 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"d": "M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z",
|
"d": "M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z",
|
||||||
"fill": "currentColor",
|
"fill": "currentColor",
|
||||||
"fill-opacity": "0.5"
|
"fill-opacity": "0"
|
||||||
},
|
},
|
||||||
"children": []
|
"children": []
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Anzeigen',
|
show: 'Anzeigen',
|
||||||
hide: 'Verbergen',
|
hide: 'Verbergen',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'Chat-Farbschema',
|
||||||
|
chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest',
|
||||||
|
chatColorThemeInverted: 'Invertiert',
|
||||||
|
invalidHexMessage: 'Ungültiger Hex-Wert',
|
||||||
more: {
|
more: {
|
||||||
entry: 'Mehr Einstellungen anzeigen',
|
entry: 'Mehr Einstellungen anzeigen',
|
||||||
copyright: 'Urheberrecht',
|
copyright: 'Urheberrecht',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Show',
|
show: 'Show',
|
||||||
hide: 'Hide',
|
hide: 'Hide',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'Chat color theme',
|
||||||
|
chatColorThemeDesc: 'Set the color theme of the chatbot',
|
||||||
|
chatColorThemeInverted: 'Inverted',
|
||||||
|
invalidHexMessage: 'Invalid hex value',
|
||||||
more: {
|
more: {
|
||||||
entry: 'Show more settings',
|
entry: 'Show more settings',
|
||||||
copyright: 'Copyright',
|
copyright: 'Copyright',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Afficher',
|
show: 'Afficher',
|
||||||
hide: 'Masquer',
|
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: {
|
more: {
|
||||||
entry: 'Afficher plus de paramètres',
|
entry: 'Afficher plus de paramètres',
|
||||||
copyright: 'Droits d\'auteur',
|
copyright: 'Droits d\'auteur',
|
||||||
|
@ -53,6 +53,10 @@ const translation = {
|
|||||||
show: 'दिखाएं',
|
show: 'दिखाएं',
|
||||||
hide: 'छुपाएं',
|
hide: 'छुपाएं',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'चैटबॉट का रंग थीम',
|
||||||
|
chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें',
|
||||||
|
chatColorThemeInverted: 'उल्टा',
|
||||||
|
invalidHexMessage: 'अमान्य हेक्स मान',
|
||||||
more: {
|
more: {
|
||||||
entry: 'अधिक सेटिंग्स दिखाएं',
|
entry: 'अधिक सेटिंग्स दिखाएं',
|
||||||
copyright: 'कॉपीराइट',
|
copyright: 'कॉपीराइट',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: '表示',
|
show: '表示',
|
||||||
hide: '非表示',
|
hide: '非表示',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'チャットボットのカラーテーマ',
|
||||||
|
chatColorThemeDesc: 'チャットボットのカラーテーマを設定します',
|
||||||
|
chatColorThemeInverted: '反転',
|
||||||
|
invalidHexMessage: '無効な16進数値',
|
||||||
more: {
|
more: {
|
||||||
entry: 'その他の設定を表示',
|
entry: 'その他の設定を表示',
|
||||||
copyright: '著作権',
|
copyright: '著作権',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: '표시',
|
show: '표시',
|
||||||
hide: '숨기기',
|
hide: '숨기기',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: '챗봇 색상 테마',
|
||||||
|
chatColorThemeDesc: '챗봇의 색상 테마를 설정하세요',
|
||||||
|
chatColorThemeInverted: '반전',
|
||||||
|
invalidHexMessage: '잘못된 16진수 값',
|
||||||
more: {
|
more: {
|
||||||
entry: '추가 설정 보기',
|
entry: '추가 설정 보기',
|
||||||
copyright: '저작권',
|
copyright: '저작권',
|
||||||
|
@ -53,6 +53,10 @@ const translation = {
|
|||||||
show: 'Pokaż',
|
show: 'Pokaż',
|
||||||
hide: 'Ukryj',
|
hide: 'Ukryj',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'Motyw kolorystyczny czatu',
|
||||||
|
chatColorThemeDesc: 'Ustaw motyw kolorystyczny czatu',
|
||||||
|
chatColorThemeInverted: 'Odwrócony',
|
||||||
|
invalidHexMessage: 'Nieprawidłowa wartość heksadecymalna',
|
||||||
more: {
|
more: {
|
||||||
entry: 'Pokaż więcej ustawień',
|
entry: 'Pokaż więcej ustawień',
|
||||||
copyright: 'Prawa autorskie',
|
copyright: 'Prawa autorskie',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Mostrar',
|
show: 'Mostrar',
|
||||||
hide: 'Ocultar',
|
hide: 'Ocultar',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'Tema de cor do chatbot',
|
||||||
|
chatColorThemeDesc: 'Defina o tema de cor do chatbot',
|
||||||
|
chatColorThemeInverted: 'Inve',
|
||||||
|
invalidHexMessage: 'Valor hex inválido',
|
||||||
more: {
|
more: {
|
||||||
entry: 'Mostrar mais configurações',
|
entry: 'Mostrar mais configurações',
|
||||||
copyright: 'Direitos autorais',
|
copyright: 'Direitos autorais',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Afișați',
|
show: 'Afișați',
|
||||||
hide: 'Ascundeț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: {
|
more: {
|
||||||
entry: 'Afișați mai multe setări',
|
entry: 'Afișați mai multe setări',
|
||||||
copyright: 'Drepturi de autor',
|
copyright: 'Drepturi de autor',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Показати',
|
show: 'Показати',
|
||||||
hide: 'Приховати',
|
hide: 'Приховати',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: 'Тема кольору чату',
|
||||||
|
chatColorThemeDesc: 'Встановіть тему кольору чат-бота',
|
||||||
|
chatColorThemeInverted: 'Інвертовано',
|
||||||
|
invalidHexMessage: 'Недійсне шістнадцяткове значення',
|
||||||
more: {
|
more: {
|
||||||
entry: 'Показати додаткові налаштування',
|
entry: 'Показати додаткові налаштування',
|
||||||
copyright: 'Авторське право',
|
copyright: 'Авторське право',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: 'Hiển thị',
|
show: 'Hiển thị',
|
||||||
hide: 'Ẩn',
|
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: {
|
more: {
|
||||||
entry: 'Hiển thị thêm cài đặt',
|
entry: 'Hiển thị thêm cài đặt',
|
||||||
copyright: 'Bản quyền',
|
copyright: 'Bản quyền',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: '显示',
|
show: '显示',
|
||||||
hide: '隐藏',
|
hide: '隐藏',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: '聊天颜色主题',
|
||||||
|
chatColorThemeDesc: '设置聊天机器人的颜色主题',
|
||||||
|
chatColorThemeInverted: '反转',
|
||||||
|
invalidHexMessage: '无效的十六进制值',
|
||||||
more: {
|
more: {
|
||||||
entry: '展示更多设置',
|
entry: '展示更多设置',
|
||||||
copyright: '版权',
|
copyright: '版权',
|
||||||
|
@ -49,6 +49,10 @@ const translation = {
|
|||||||
show: '展示',
|
show: '展示',
|
||||||
hide: '隱藏',
|
hide: '隱藏',
|
||||||
},
|
},
|
||||||
|
chatColorTheme: '聊天顏色主題',
|
||||||
|
chatColorThemeDesc: '設定聊天機器人的顏色主題',
|
||||||
|
chatColorThemeInverted: '反轉',
|
||||||
|
invalidHexMessage: '無效的十六進制值',
|
||||||
more: {
|
more: {
|
||||||
entry: '展示更多設定',
|
entry: '展示更多設定',
|
||||||
copyright: '版權',
|
copyright: '版權',
|
||||||
|
@ -11,6 +11,8 @@ export type ConversationItem = {
|
|||||||
|
|
||||||
export type SiteInfo = {
|
export type SiteInfo = {
|
||||||
title: string
|
title: string
|
||||||
|
chat_color_theme?: string
|
||||||
|
chat_color_theme_inverted?: boolean
|
||||||
icon?: string
|
icon?: string
|
||||||
icon_background?: string
|
icon_background?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
@ -246,6 +246,12 @@ export type SiteConfig = {
|
|||||||
title: string
|
title: string
|
||||||
/** Application Description will be shown in the Client */
|
/** Application Description will be shown in the Client */
|
||||||
description: string
|
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 */
|
||||||
author: string
|
author: string
|
||||||
/** User Support Email Address */
|
/** User Support Email Address */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user