mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-11 01:08:57 +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('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,
|
||||
|
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))
|
||||
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'))
|
||||
|
2
web/.vscode/settings.example.json
vendored
2
web/.vscode/settings.example.json
vendored
@ -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)
|
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": {
|
||||
"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 */
|
||||
|
Loading…
x
Reference in New Issue
Block a user