mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-24 18:23:07 +08:00
Feat: web app dark mode (#14732)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@@ -9,16 +9,19 @@ import type {
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
import { useEmbeddedChatbotContext } from './context'
|
||||
import ConfigPanel from './config-panel'
|
||||
import { isDify } from './utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
|
||||
import {
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/share'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
|
||||
import AnswerIcon from '@/app/components/base/answer-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
const {
|
||||
@@ -29,6 +32,7 @@ const ChatWrapper = () => {
|
||||
currentConversationItem,
|
||||
inputsForms,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationCompleted,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
@@ -67,6 +71,38 @@ const ChatWrapper = () => {
|
||||
appPrevChatList,
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
)
|
||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
||||
const inputDisabled = useMemo(() => {
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required }) => required)
|
||||
if (requiredVars.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (fileIsUploading)
|
||||
return
|
||||
|
||||
if (!inputsFormValue?.[variable])
|
||||
hasEmptyInput = label as string
|
||||
|
||||
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
|
||||
const files = inputsFormValue[variable]
|
||||
if (Array.isArray(files))
|
||||
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
|
||||
else
|
||||
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
|
||||
}
|
||||
})
|
||||
}
|
||||
if (hasEmptyInput)
|
||||
return true
|
||||
|
||||
if (fileIsUploading)
|
||||
return true
|
||||
return false
|
||||
}, [inputsFormValue, inputsForms])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentChatInstanceRef.current)
|
||||
@@ -108,26 +144,48 @@ const ChatWrapper = () => {
|
||||
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
<>
|
||||
{!currentConversationId && (
|
||||
<div className={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}>
|
||||
<div className='mb-6' />
|
||||
<ConfigPanel />
|
||||
<div
|
||||
className='my-6 h-[1px]'
|
||||
style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const messageList = useMemo(() => {
|
||||
if (currentConversationId)
|
||||
return chatList
|
||||
return chatList.filter(item => !item.isOpeningStatement)
|
||||
}, [chatList, currentConversationId])
|
||||
|
||||
return null
|
||||
}, [currentConversationId, inputsForms, isMobile])
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId)
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (!inputsForms.length)
|
||||
return null
|
||||
if (isMobile) {
|
||||
if (!currentConversationId)
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
return <div className='mb-4'></div>
|
||||
}
|
||||
else {
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
}
|
||||
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
|
||||
|
||||
const welcome = useMemo(() => {
|
||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||
if (currentConversationId)
|
||||
return null
|
||||
if (!welcomeMessage)
|
||||
return null
|
||||
if (!collapsed && inputsForms.length > 0)
|
||||
return null
|
||||
return (
|
||||
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
||||
<AppIcon
|
||||
size='xl'
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background={appData?.site.icon_background}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
/>
|
||||
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
|
||||
</div>
|
||||
)
|
||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
|
||||
|
||||
const answerIcon = isDify()
|
||||
? <LogoAvatar className='relative shrink-0' />
|
||||
@@ -144,17 +202,22 @@ const ChatWrapper = () => {
|
||||
<Chat
|
||||
appData={appData}
|
||||
config={appConfig}
|
||||
chatList={chatList}
|
||||
chatList={messageList}
|
||||
isResponding={isResponding}
|
||||
chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||
chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
|
||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')}
|
||||
onSend={doSend}
|
||||
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
||||
inputsForm={inputsForms}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
chatNode={
|
||||
<>
|
||||
{chatNode}
|
||||
{welcome}
|
||||
</>
|
||||
}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
onFeedback={handleFeedback}
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
@@ -162,6 +225,8 @@ const ChatWrapper = () => {
|
||||
hideProcessDetail
|
||||
themeBuilder={themeBuilder}
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
inputDisabled={inputDisabled}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
interface InputProps {
|
||||
form: any
|
||||
value: string
|
||||
onChange: (variable: string, value: string) => void
|
||||
}
|
||||
const FormInput: FC<InputProps> = ({
|
||||
form,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
required,
|
||||
max_length,
|
||||
variable,
|
||||
} = form
|
||||
|
||||
if (type === 'paragraph') {
|
||||
return (
|
||||
<Textarea
|
||||
value={value}
|
||||
className='resize-none'
|
||||
onChange={e => onChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
|
||||
value={value || ''}
|
||||
maxLength={max_length}
|
||||
onChange={e => onChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FormInput)
|
||||
@@ -1,129 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import Input from './form-input'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
|
||||
const Form = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appParams,
|
||||
inputsForms,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
isMobile,
|
||||
} = useEmbeddedChatbotContext()
|
||||
|
||||
const handleFormChange = useCallback((variable: string, value: any) => {
|
||||
handleNewConversationInputsChange({
|
||||
...newConversationInputsRef.current,
|
||||
[variable]: value,
|
||||
})
|
||||
}, [newConversationInputsRef, handleNewConversationInputsChange])
|
||||
|
||||
const renderField = (form: any) => {
|
||||
const {
|
||||
label,
|
||||
required,
|
||||
variable,
|
||||
options,
|
||||
} = form
|
||||
|
||||
if (form.type === 'text-input' || form.type === 'paragraph') {
|
||||
return (
|
||||
<Input
|
||||
form={form}
|
||||
value={newConversationInputs[variable]}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === 'number') {
|
||||
return (
|
||||
<input
|
||||
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
|
||||
type="number"
|
||||
value={newConversationInputs[variable] || ''}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (form.type === 'number') {
|
||||
return (
|
||||
<input
|
||||
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
|
||||
type="number"
|
||||
value={newConversationInputs[variable] || ''}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === InputVarType.singleFile) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
|
||||
onChange={files => handleFormChange(variable, files[0])}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === InputVarType.multiFiles) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={newConversationInputs[variable]}
|
||||
onChange={files => handleFormChange(variable, files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: form.max_length,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
value={newConversationInputs[variable]}
|
||||
items={options.map((option: string) => ({ value: option, name: option }))}
|
||||
onSelect={item => handleFormChange(variable, item.value as string)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!inputsForms.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-4 py-2'>
|
||||
{
|
||||
inputsForms.map(form => (
|
||||
<div
|
||||
key={form.variable}
|
||||
className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
|
||||
>
|
||||
<div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
|
||||
{renderField(form)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import { useThemeContext } from '../theme/theme-context'
|
||||
import { CssTransform } from '../theme/utils'
|
||||
import Form from './form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
|
||||
const ConfigPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appData,
|
||||
inputsForms,
|
||||
handleStartChat,
|
||||
showConfigPanelBeforeChat,
|
||||
isMobile,
|
||||
} = useEmbeddedChatbotContext()
|
||||
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]'>
|
||||
<div
|
||||
className={cn(
|
||||
'grow rounded-xl overflow-y-auto',
|
||||
showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg',
|
||||
!showConfigPanelBeforeChat && collapsed && 'border border-indigo-100',
|
||||
!showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg',
|
||||
)}
|
||||
>
|
||||
<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'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
showConfigPanelBeforeChat && (
|
||||
<>
|
||||
<div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
|
||||
<AppIcon
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
background='transparent'
|
||||
size='small'
|
||||
className="mr-2"
|
||||
/>
|
||||
{appData?.site.title}
|
||||
</div>
|
||||
{
|
||||
appData?.site.description && (
|
||||
<div className='mt-2 w-full text-sm text-gray-500'>
|
||||
{appData?.site.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showConfigPanelBeforeChat && collapsed && (
|
||||
<>
|
||||
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
|
||||
{t('share.chat.configStatusDes')}
|
||||
</div>
|
||||
<Button
|
||||
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||
variant='secondary-accent'
|
||||
size='small'
|
||||
className='shrink-0 text-white'
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<Edit02 className='mr-1 w-3 h-3' />
|
||||
{t('common.operation.edit')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showConfigPanelBeforeChat && !collapsed && (
|
||||
<>
|
||||
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
|
||||
{t('share.chat.privatePromptConfigTitle')}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!collapsed && !showConfigPanelBeforeChat && (
|
||||
<div className='p-6 rounded-b-xl'>
|
||||
<Form />
|
||||
<div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
|
||||
<Button
|
||||
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||
variant='primary'
|
||||
className='mr-2'
|
||||
onClick={() => {
|
||||
setCollapsed(true)
|
||||
handleStartChat()
|
||||
}}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCollapsed(true)}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showConfigPanelBeforeChat && (
|
||||
<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'
|
||||
onClick={handleStartChat}
|
||||
>
|
||||
<MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
|
||||
{t('share.chat.startChat')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showConfigPanelBeforeChat && (site || customConfig) && (
|
||||
<div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
|
||||
{site?.privacy_policy
|
||||
? <div className={cn(isMobile && 'mb-2 w-full text-center')}>{t('share.chat.privacyPolicyLeft')}
|
||||
<a
|
||||
className='text-gray-500 px-1'
|
||||
href={site?.privacy_policy}
|
||||
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
|
||||
{t('share.chat.privacyPolicyRight')}
|
||||
</div>
|
||||
: <div>
|
||||
</div>}
|
||||
{
|
||||
customConfig?.remove_webapp_brand
|
||||
? null
|
||||
: (
|
||||
<div className={cn('flex items-center justify-end', isMobile && 'w-full')}>
|
||||
<div className='flex items-center pr-3 space-x-3'>
|
||||
<span className='uppercase'>{t('share.chat.poweredBy')}</span>
|
||||
{
|
||||
customConfig?.replace_webapp_logo
|
||||
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
: <LogoSite className='!h-5' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfigPanel
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
|
||||
export interface EmbeddedChatbotContextValue {
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
@@ -27,13 +27,12 @@ export interface EmbeddedChatbotContextValue {
|
||||
appPrevChatList: ChatItem[]
|
||||
pinnedConversationList: AppConversationData['data']
|
||||
conversationList: AppConversationData['data']
|
||||
showConfigPanelBeforeChat: boolean
|
||||
newConversationInputs: Record<string, any>
|
||||
newConversationInputsRef: RefObject<Record<string, any>>
|
||||
handleNewConversationInputsChange: (v: Record<string, any>) => void
|
||||
inputsForms: any[]
|
||||
handleNewConversation: () => void
|
||||
handleStartChat: () => void
|
||||
handleStartChat: (callback?: any) => void
|
||||
handleChangeConversation: (conversationId: string) => void
|
||||
handleNewConversationCompleted: (newConversationId: string) => void
|
||||
chatShouldReloadKey: string
|
||||
@@ -50,7 +49,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
||||
appPrevChatList: [],
|
||||
pinnedConversationList: [],
|
||||
conversationList: [],
|
||||
showConfigPanelBeforeChat: false,
|
||||
newConversationInputs: {},
|
||||
newConversationInputsRef: { current: {} },
|
||||
handleNewConversationInputsChange: () => {},
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
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()
|
||||
if (!isMobile)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
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>
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<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" color={theme?.colorPathOnHeader}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
109
web/app/components/base/chat/embedded-chatbot/header/index.tsx
Normal file
109
web/app/components/base/chat/embedded-chatbot/header/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiResetLeftLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Theme } from '../theme/theme-context'
|
||||
import { CssTransform } from '../theme/utils'
|
||||
import {
|
||||
useEmbeddedChatbotContext,
|
||||
} from '../context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
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()
|
||||
const {
|
||||
appData,
|
||||
currentConversationId,
|
||||
inputsForms,
|
||||
} = useEmbeddedChatbotContext()
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<div className='shrink-0 h-14 p-3 flex items-center justify-end'>
|
||||
<div className='flex items-center gap-1'>
|
||||
{/* powered by */}
|
||||
<div className='shrink-0'>
|
||||
{!appData?.custom_config?.remove_webapp_brand && (
|
||||
<div className={cn(
|
||||
'shrink-0 px-2 flex items-center gap-1.5',
|
||||
)}>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
|
||||
{appData?.custom_config?.replace_webapp_logo && (
|
||||
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
)}
|
||||
{!appData?.custom_config?.replace_webapp_logo && (
|
||||
<LogoSite className='!h-5' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentConversationId && (
|
||||
<Divider type='vertical' className='h-3.5' />
|
||||
)}
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={onCreateNewChat}>
|
||||
<RiResetLeftLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('shrink-0 flex items-center justify-between h-14 px-3 rounded-t-2xl')}
|
||||
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
|
||||
>
|
||||
<div className="grow flex items-center space-x-3">
|
||||
{customerIcon}
|
||||
<div
|
||||
className='system-md-semibold truncate'
|
||||
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={onCreateNewChat}>
|
||||
<RiResetLeftLine className={cn('w-[18px] h-[18px]', theme?.colorPathOnHeader)} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown iconColor={theme?.colorPathOnHeader} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -88,7 +88,6 @@ export const useEmbeddedChatbot = () => {
|
||||
})
|
||||
}
|
||||
}, [appId, conversationIdInfo, setConversationIdInfo])
|
||||
const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
|
||||
|
||||
const [newConversationId, setNewConversationId] = useState('')
|
||||
const chatShouldReloadKey = useMemo(() => {
|
||||
@@ -273,23 +272,18 @@ export const useEmbeddedChatbot = () => {
|
||||
|
||||
return true
|
||||
}, [inputsForms, notify, t])
|
||||
const handleStartChat = useCallback(() => {
|
||||
const handleStartChat = useCallback((callback?: any) => {
|
||||
if (checkInputsRequired()) {
|
||||
setShowConfigPanelBeforeChat(false)
|
||||
setShowNewConversationItemInList(true)
|
||||
callback?.()
|
||||
}
|
||||
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired])
|
||||
}, [setShowNewConversationItemInList, checkInputsRequired])
|
||||
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
|
||||
const handleChangeConversation = useCallback((conversationId: string) => {
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setNewConversationId('')
|
||||
handleConversationIdInfoChange(conversationId)
|
||||
|
||||
if (conversationId === '' && !checkInputsRequired(true))
|
||||
setShowConfigPanelBeforeChat(true)
|
||||
else
|
||||
setShowConfigPanelBeforeChat(false)
|
||||
}, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
|
||||
}, [handleConversationIdInfoChange])
|
||||
const handleNewConversation = useCallback(() => {
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setNewConversationId('')
|
||||
@@ -299,11 +293,10 @@ export const useEmbeddedChatbot = () => {
|
||||
}
|
||||
else if (currentConversationId) {
|
||||
handleConversationIdInfoChange('')
|
||||
setShowConfigPanelBeforeChat(true)
|
||||
setShowNewConversationItemInList(true)
|
||||
handleNewConversationInputsChange({})
|
||||
}
|
||||
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
|
||||
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
|
||||
|
||||
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
||||
setNewConversationId(newConversationId)
|
||||
@@ -336,8 +329,6 @@ export const useEmbeddedChatbot = () => {
|
||||
appPrevChatList,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
setShowConfigPanelBeforeChat,
|
||||
setShowNewConversationItemInList,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from 'react'
|
||||
import { useAsyncEffect } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiLoopLeftLine } from '@remixicon/react'
|
||||
import {
|
||||
EmbeddedChatbotContext,
|
||||
useEmbeddedChatbotContext,
|
||||
@@ -12,32 +11,30 @@ import {
|
||||
import { useEmbeddedChatbot } from './hooks'
|
||||
import { isDify } from './utils'
|
||||
import { useThemeContext } from './theme/theme-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CssTransform } from './theme/utils'
|
||||
import { checkOrSetAccessToken } from '@/app/components/share/utils'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
|
||||
import Header from '@/app/components/base/chat/embedded-chatbot/header'
|
||||
import ConfigPanel from '@/app/components/base/chat/embedded-chatbot/config-panel'
|
||||
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Chatbot = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isMobile,
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
appData,
|
||||
appPrevChatList,
|
||||
showConfigPanelBeforeChat,
|
||||
appChatListDataLoading,
|
||||
chatShouldReloadKey,
|
||||
handleNewConversation,
|
||||
themeBuilder,
|
||||
} = useEmbeddedChatbotContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
|
||||
const customConfig = appData?.custom_config
|
||||
const site = appData?.site
|
||||
|
||||
@@ -55,52 +52,76 @@ const Chatbot = () => {
|
||||
|
||||
if (appInfoLoading) {
|
||||
return (
|
||||
<Loading type='app' />
|
||||
<>
|
||||
{!isMobile && <Loading type='app' />}
|
||||
{isMobile && (
|
||||
<div className={cn('relative')}>
|
||||
<div className={cn('flex flex-col h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border rounded-2xl shadow-xs')}>
|
||||
<Loading type='app' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (appInfoError) {
|
||||
return (
|
||||
<AppUnavailable />
|
||||
<>
|
||||
{!isMobile && <AppUnavailable />}
|
||||
{isMobile && (
|
||||
<div className={cn('relative')}>
|
||||
<div className={cn('flex flex-col h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border rounded-2xl shadow-xs')}>
|
||||
<AppUnavailable />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Header
|
||||
isMobile={isMobile}
|
||||
title={site?.title || ''}
|
||||
customerIcon={isDify() ? difyIcon : ''}
|
||||
theme={themeBuilder?.theme}
|
||||
onCreateNewChat={handleNewConversation}
|
||||
/>
|
||||
<div className='flex bg-white overflow-hidden'>
|
||||
<div className={cn('h-[100vh] grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)]')}>
|
||||
{showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
|
||||
<div className={cn('flex w-full items-center justify-center h-full tablet:px-4', isMobile && 'px-4')}>
|
||||
<ConfigPanel />
|
||||
</div>
|
||||
)}
|
||||
{appChatListDataLoading && chatReady && (
|
||||
<div className='relative'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col border border-components-panel-border-subtle rounded-2xl',
|
||||
isMobile ? 'h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border shadow-xs' : 'h-[100vh] bg-chatbot-bg',
|
||||
)}
|
||||
style={isMobile ? Object.assign({}, CssTransform(themeBuilder?.theme?.backgroundHeaderColorStyle ?? '')) : {}}
|
||||
>
|
||||
<Header
|
||||
isMobile={isMobile}
|
||||
title={site?.title || ''}
|
||||
customerIcon={isDify() ? difyIcon : ''}
|
||||
theme={themeBuilder?.theme}
|
||||
onCreateNewChat={handleNewConversation}
|
||||
/>
|
||||
<div className={cn('grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)] bg-chatbot-bg rounded-2xl')}>
|
||||
{appChatListDataLoading && (
|
||||
<Loading type='app' />
|
||||
)}
|
||||
{chatReady && !appChatListDataLoading && (
|
||||
<div className='relative h-full pt-8 mx-auto w-full max-w-[720px]'>
|
||||
{!isMobile && (
|
||||
<div className='absolute top-2.5 right-3 z-20'>
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<div className='p-1.5 bg-white border-[0.5px] border-gray-100 rounded-lg shadow-md cursor-pointer' onClick={handleNewConversation}>
|
||||
<RiLoopLeftLine className="h-4 w-4 text-gray-500"/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<ChatWrapper />
|
||||
</div>
|
||||
{!appChatListDataLoading && (
|
||||
<ChatWrapper key={chatShouldReloadKey} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* powered by */}
|
||||
{isMobile && (
|
||||
<div className='shrink-0 h-[60px] pl-2 flex items-center'>
|
||||
{!appData?.custom_config?.remove_webapp_brand && (
|
||||
<div className={cn(
|
||||
'shrink-0 px-2 flex items-center gap-1.5',
|
||||
)}>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
|
||||
{appData?.custom_config?.replace_webapp_logo && (
|
||||
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
)}
|
||||
{!appData?.custom_config?.replace_webapp_logo && (
|
||||
<LogoSite className='!h-5' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -122,7 +143,6 @@ const EmbeddedChatbotWrapper = () => {
|
||||
appPrevChatList,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
@@ -150,7 +170,6 @@ const EmbeddedChatbotWrapper = () => {
|
||||
appPrevChatList,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
showTip?: boolean
|
||||
}
|
||||
|
||||
const InputsFormContent = ({ showTip }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appParams,
|
||||
inputsForms,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
} = useEmbeddedChatbotContext()
|
||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputs
|
||||
const readonly = !!currentConversationId
|
||||
|
||||
const handleFormChange = useCallback((variable: string, value: any) => {
|
||||
handleNewConversationInputsChange({
|
||||
...newConversationInputsRef.current,
|
||||
[variable]: value,
|
||||
})
|
||||
}, [newConversationInputsRef, handleNewConversationInputsChange])
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
{inputsForms.map(form => (
|
||||
<div key={form.variable} className='space-y-1'>
|
||||
<div className='h-6 flex items-center gap-1'>
|
||||
<div className='text-text-secondary system-md-semibold'>{form.label}</div>
|
||||
{!form.required && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{t('appDebug.variableTable.optional')}</div>
|
||||
)}
|
||||
</div>
|
||||
{form.type === InputVarType.textInput && (
|
||||
<Input
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.number && (
|
||||
<Input
|
||||
type='number'
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.select && (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
value={inputsFormValue?.[form.variable]}
|
||||
items={form.options.map((option: string) => ({ value: option, name: option }))}
|
||||
onSelect={item => handleFormChange(form.variable, item.value as string)}
|
||||
placeholder={form.label}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.singleFile && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={inputsFormValue?.[form.variable] ? [inputsFormValue?.[form.variable]] : []}
|
||||
onChange={files => handleFormChange(form.variable, files[0])}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.multiFiles && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={inputsFormValue?.[form.variable] || []}
|
||||
onChange={files => handleFormChange(form.variable, files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: form.max_length,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{showTip && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{t('share.chat.chatFormTip')}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputsFormContent
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
collapsed: boolean
|
||||
setCollapsed: (collapsed: boolean) => void
|
||||
}
|
||||
|
||||
const InputsFormNode = ({
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isMobile,
|
||||
currentConversationId,
|
||||
themeBuilder,
|
||||
handleStartChat,
|
||||
} = useEmbeddedChatbotContext()
|
||||
|
||||
return (
|
||||
<div className={cn('mb-6 pt-6 px-4 flex flex-col items-center', isMobile && 'mb-4 pt-4')}>
|
||||
<div className={cn(
|
||||
'w-full max-w-[672px] bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-md',
|
||||
collapsed && 'bg-components-card-bg border border-components-card-border shadow-none',
|
||||
)}>
|
||||
<div className={cn(
|
||||
'flex items-center gap-3 px-6 py-4 rounded-t-2xl',
|
||||
!collapsed && 'border-b border-divider-subtle',
|
||||
isMobile && 'px-4 py-3',
|
||||
)}>
|
||||
<Message3Fill className='shrink-0 w-6 h-6' />
|
||||
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
|
||||
{collapsed && (
|
||||
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(false)}>{currentConversationId ? t('common.operation.view') : t('common.operation.edit')}</Button>
|
||||
)}
|
||||
{!collapsed && currentConversationId && (
|
||||
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(true)}>{t('common.operation.close')}</Button>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className={cn('p-6', isMobile && 'p-4')}>
|
||||
<InputsFormContent showTip={!!currentConversationId} />
|
||||
</div>
|
||||
)}
|
||||
{!collapsed && !currentConversationId && (
|
||||
<div className={cn('p-6', isMobile && 'p-4')}>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={() => handleStartChat(() => setCollapsed(true))}
|
||||
style={
|
||||
themeBuilder?.theme
|
||||
? {
|
||||
backgroundColor: themeBuilder?.theme.primaryColor,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>{t('share.chat.startChat')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{collapsed && (
|
||||
<div className='py-4 flex items-center w-full max-w-[720px]'>
|
||||
<Divider bgStyle='gradient' className='basis-1/2 h-px rotate-180' />
|
||||
<Divider bgStyle='gradient' className='basis-1/2 h-px' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputsFormNode
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiChatSettingsLine,
|
||||
} from '@remixicon/react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
iconColor?: string
|
||||
}
|
||||
const ViewFormDropdown = ({ iconColor }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
|
||||
<RiChatSettingsLine className={cn('w-[18px] h-[18px]', iconColor)} />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div className='w-[400px] bg-components-panel-bg backdrop-blur-sm rounded-2xl border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<div className='flex items-center gap-3 px-6 py-4 rounded-t-2xl border-b border-divider-subtle'>
|
||||
<Message3Fill className='shrink-0 w-6 h-6' />
|
||||
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
|
||||
</div>
|
||||
<div className='p-6'>
|
||||
<InputsFormContent showTip />
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewFormDropdown
|
||||
@@ -9,7 +9,7 @@ export class Theme {
|
||||
public backgroundHeaderColorStyle = 'backgroundImage: linear-gradient(to right, #2563eb, #0ea5e9)'
|
||||
public headerBorderBottomStyle = ''
|
||||
public colorFontOnHeaderStyle = 'color: white'
|
||||
public colorPathOnHeader = 'white'
|
||||
public colorPathOnHeader = 'text-text-primary-on-surface'
|
||||
public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2'
|
||||
public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)'
|
||||
public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)'
|
||||
|
||||
Reference in New Issue
Block a user