mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-24 18:23:07 +08:00
Co-authored-by: crazywoola <427733928@qq.com>
This commit is contained in:
committed by
GitHub
parent
8fa6cb5e03
commit
4c0a31d38b
@@ -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": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user