Co-authored-by: crazywoola <427733928@qq.com>tags/0.6.12
| @@ -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', | |||
| @@ -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, | |||
| @@ -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, | |||
| @@ -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)) | |||
| 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')) | |||
| @@ -22,4 +22,4 @@ | |||
| }, | |||
| "typescript.tsdk": "node_modules/typescript/lib", | |||
| "typescript.enablePromptUseWorkspaceTsdk": true | |||
| } | |||
| } | |||
| @@ -226,6 +226,7 @@ const AppPublisher = ({ | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| <EmbeddedModal | |||
| siteInfo={appDetail?.site} | |||
| isShow={embeddingModalOpen} | |||
| onClose={() => setEmbeddingModalOpen(false)} | |||
| appBaseUrl={appBaseURL} | |||
| @@ -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} | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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,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'} | |||
| @@ -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} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -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} /> | |||
| @@ -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} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -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' | |||
| @@ -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>({ | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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) | |||
| @@ -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": { | |||
| "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": [] | |||
| } | |||
| @@ -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', | |||
| @@ -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', | |||
| @@ -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', | |||
| @@ -53,6 +53,10 @@ const translation = { | |||
| show: 'दिखाएं', | |||
| hide: 'छुपाएं', | |||
| }, | |||
| chatColorTheme: 'चैटबॉट का रंग थीम', | |||
| chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें', | |||
| chatColorThemeInverted: 'उल्टा', | |||
| invalidHexMessage: 'अमान्य हेक्स मान', | |||
| more: { | |||
| entry: 'अधिक सेटिंग्स दिखाएं', | |||
| copyright: 'कॉपीराइट', | |||
| @@ -49,6 +49,10 @@ const translation = { | |||
| show: '表示', | |||
| hide: '非表示', | |||
| }, | |||
| chatColorTheme: 'チャットボットのカラーテーマ', | |||
| chatColorThemeDesc: 'チャットボットのカラーテーマを設定します', | |||
| chatColorThemeInverted: '反転', | |||
| invalidHexMessage: '無効な16進数値', | |||
| more: { | |||
| entry: 'その他の設定を表示', | |||
| copyright: '著作権', | |||
| @@ -49,6 +49,10 @@ const translation = { | |||
| show: '표시', | |||
| hide: '숨기기', | |||
| }, | |||
| chatColorTheme: '챗봇 색상 테마', | |||
| chatColorThemeDesc: '챗봇의 색상 테마를 설정하세요', | |||
| chatColorThemeInverted: '반전', | |||
| invalidHexMessage: '잘못된 16진수 값', | |||
| more: { | |||
| entry: '추가 설정 보기', | |||
| copyright: '저작권', | |||
| @@ -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', | |||
| @@ -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', | |||
| @@ -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', | |||
| @@ -49,6 +49,10 @@ const translation = { | |||
| show: 'Показати', | |||
| hide: 'Приховати', | |||
| }, | |||
| chatColorTheme: 'Тема кольору чату', | |||
| chatColorThemeDesc: 'Встановіть тему кольору чат-бота', | |||
| chatColorThemeInverted: 'Інвертовано', | |||
| invalidHexMessage: 'Недійсне шістнадцяткове значення', | |||
| more: { | |||
| entry: 'Показати додаткові налаштування', | |||
| copyright: 'Авторське право', | |||
| @@ -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', | |||
| @@ -49,6 +49,10 @@ const translation = { | |||
| show: '显示', | |||
| hide: '隐藏', | |||
| }, | |||
| chatColorTheme: '聊天颜色主题', | |||
| chatColorThemeDesc: '设置聊天机器人的颜色主题', | |||
| chatColorThemeInverted: '反转', | |||
| invalidHexMessage: '无效的十六进制值', | |||
| more: { | |||
| entry: '展示更多设置', | |||
| copyright: '版权', | |||
| @@ -49,6 +49,10 @@ const translation = { | |||
| show: '展示', | |||
| hide: '隱藏', | |||
| }, | |||
| chatColorTheme: '聊天顏色主題', | |||
| chatColorThemeDesc: '設定聊天機器人的顏色主題', | |||
| chatColorThemeInverted: '反轉', | |||
| invalidHexMessage: '無效的十六進制值', | |||
| more: { | |||
| entry: '展示更多設定', | |||
| copyright: '版權', | |||
| @@ -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 | |||
| @@ -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 */ | |||