Explorar o código

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

Co-authored-by: crazywoola <427733928@qq.com>
Diego Romero-Lovo hai 10 meses
pai
achega
4c0a31d38b
Modificáronse 38 ficheiros con 379 adicións e 18 borrados
  1. 4 0
      api/controllers/console/app/site.py
  2. 2 0
      api/controllers/web/site.py
  3. 2 0
      api/fields/app_fields.py
  4. 22 0
      api/migrations/versions/63f9175e515b_merge_branches.py
  5. 35 0
      api/migrations/versions/b69ca54b9208_add_chatbot_color_theme.py
  6. 2 0
      api/models/model.py
  7. 1 1
      web/.vscode/settings.example.json
  8. 1 0
      web/app/components/app/app-publisher/index.tsx
  9. 2 0
      web/app/components/app/overview/appCard.tsx
  10. 15 5
      web/app/components/app/overview/embedded/index.tsx
  11. 69 4
      web/app/components/app/overview/settings/index.tsx
  12. 4 1
      web/app/components/base/button/index.tsx
  13. 15 0
      web/app/components/base/chat/chat/chat-input.tsx
  14. 6 0
      web/app/components/base/chat/chat/index.tsx
  15. 12 3
      web/app/components/base/chat/chat/question.tsx
  16. 2 0
      web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
  17. 7 0
      web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx
  18. 2 0
      web/app/components/base/chat/embedded-chatbot/context.tsx
  19. 8 3
      web/app/components/base/chat/embedded-chatbot/header.tsx
  20. 6 0
      web/app/components/base/chat/embedded-chatbot/index.tsx
  21. 72 0
      web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts
  22. 29 0
      web/app/components/base/chat/embedded-chatbot/theme/utils.ts
  23. 1 1
      web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json
  24. 4 0
      web/i18n/de-DE/app-overview.ts
  25. 4 0
      web/i18n/en-US/app-overview.ts
  26. 4 0
      web/i18n/fr-FR/app-overview.ts
  27. 4 0
      web/i18n/hi-IN/app-overview.ts
  28. 4 0
      web/i18n/ja-JP/app-overview.ts
  29. 4 0
      web/i18n/ko-KR/app-overview.ts
  30. 4 0
      web/i18n/pl-PL/app-overview.ts
  31. 4 0
      web/i18n/pt-BR/app-overview.ts
  32. 4 0
      web/i18n/ro-RO/app-overview.ts
  33. 4 0
      web/i18n/uk-UA/app-overview.ts
  34. 4 0
      web/i18n/vi-VN/app-overview.ts
  35. 4 0
      web/i18n/zh-Hans/app-overview.ts
  36. 4 0
      web/i18n/zh-Hant/app-overview.ts
  37. 2 0
      web/models/share.ts
  38. 6 0
      web/types/app.ts

+ 4 - 0
api/controllers/console/app/site.py

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

+ 2 - 0
api/controllers/web/site.py

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

+ 2 - 0
api/fields/app_fields.py

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

+ 22 - 0
api/migrations/versions/63f9175e515b_merge_branches.py

@@ -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 - 0
api/migrations/versions/b69ca54b9208_add_chatbot_color_theme.py

@@ -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 - 0
api/models/model.py

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

+ 1 - 1
web/.vscode/settings.example.json

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

+ 1 - 0
web/app/components/app/app-publisher/index.tsx

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

+ 2 - 0
web/app/components/app/overview/appCard.tsx

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

+ 15 - 5
web/app/components/app/overview/embedded/index.tsx

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

+ 69 - 4
web/app/components/app/overview/settings/index.tsx

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

+ 4 - 1
web/app/components/base/button/index.tsx

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

+ 15 - 0
web/app/components/base/chat/chat/chat-input.tsx

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

+ 6 - 0
web/app/components/base/chat/chat/index.tsx

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

+ 12 - 3
web/app/components/base/chat/chat/question.tsx

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

+ 2 - 0
web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx

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

+ 7 - 0
web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx

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

+ 2 - 0
web/app/components/base/chat/embedded-chatbot/context.tsx

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

+ 8 - 3
web/app/components/base/chat/embedded-chatbot/header.tsx

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

+ 6 - 0
web/app/components/base/chat/embedded-chatbot/index.tsx

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

+ 72 - 0
web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts

@@ -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 - 0
web/app/components/base/chat/embedded-chatbot/theme/utils.ts

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

+ 1 - 1
web/app/components/base/icons/src/vender/solid/general/QuestionTriangle.json

@@ -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": []
 					}

+ 4 - 0
web/i18n/de-DE/app-overview.ts

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

+ 4 - 0
web/i18n/en-US/app-overview.ts

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

+ 4 - 0
web/i18n/fr-FR/app-overview.ts

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

+ 4 - 0
web/i18n/hi-IN/app-overview.ts

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

+ 4 - 0
web/i18n/ja-JP/app-overview.ts

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

+ 4 - 0
web/i18n/ko-KR/app-overview.ts

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

+ 4 - 0
web/i18n/pl-PL/app-overview.ts

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

+ 4 - 0
web/i18n/pt-BR/app-overview.ts

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

+ 4 - 0
web/i18n/ro-RO/app-overview.ts

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

+ 4 - 0
web/i18n/uk-UA/app-overview.ts

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

+ 4 - 0
web/i18n/vi-VN/app-overview.ts

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

+ 4 - 0
web/i18n/zh-Hans/app-overview.ts

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

+ 4 - 0
web/i18n/zh-Hant/app-overview.ts

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

+ 2 - 0
web/models/share.ts

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

+ 6 - 0
web/types/app.ts

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