| import React, { useEffect } from 'react' | import React, { useEffect } from 'react' | ||||
| import cn from 'classnames' | import cn from 'classnames' | ||||
| import type { IMainProps } from '@/app/components/share/chat' | import type { IMainProps } from '@/app/components/share/chat' | ||||
| import Main from '@/app/components/share/chatbot' | |||||
| import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' | |||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import { fetchSystemFeatures } from '@/service/share' | import { fetchSystemFeatures } from '@/service/share' | ||||
| import LogoSite from '@/app/components/base/logo/logo-site' | import LogoSite from '@/app/components/base/logo/logo-site' | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| : <Main /> | |||||
| : <EmbeddedChatbot /> | |||||
| } | } | ||||
| </> | </> | ||||
| )} | )} |
| ) | ) | ||||
| export const ReplayIcon = ({ className }: SVGProps<SVGElement>) => ( | export const ReplayIcon = ({ className }: SVGProps<SVGElement>) => ( | ||||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||||
| <path d="M1.33301 6.66667C1.33301 6.66667 2.66966 4.84548 3.75556 3.75883C4.84147 2.67218 6.34207 2 7.99967 2C11.3134 2 13.9997 4.68629 13.9997 8C13.9997 11.3137 11.3134 14 7.99967 14C5.26428 14 2.95642 12.1695 2.23419 9.66667M1.33301 6.66667V2.66667M1.33301 6.66667H5.33301" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> | |||||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}> | |||||
| <path d="M1.33301 6.66667C1.33301 6.66667 2.66966 4.84548 3.75556 3.75883C4.84147 2.67218 6.34207 2 7.99967 2C11.3134 2 13.9997 4.68629 13.9997 8C13.9997 11.3137 11.3134 14 7.99967 14C5.26428 14 2.95642 12.1695 2.23419 9.66667M1.33301 6.66667V2.66667M1.33301 6.66667H5.33301" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||||
| </svg> | </svg> | ||||
| ) | ) |
| type IAppUnavailableProps = { | type IAppUnavailableProps = { | ||||
| code?: number | code?: number | ||||
| isUnknwonReason?: boolean | |||||
| isUnknownReason?: boolean | |||||
| unknownReason?: string | unknownReason?: string | ||||
| } | } | ||||
| const AppUnavailable: FC<IAppUnavailableProps> = ({ | const AppUnavailable: FC<IAppUnavailableProps> = ({ | ||||
| code = 404, | code = 404, | ||||
| isUnknwonReason, | |||||
| isUnknownReason, | |||||
| unknownReason, | unknownReason, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| style={{ | style={{ | ||||
| borderRight: '1px solid rgba(0,0,0,.3)', | borderRight: '1px solid rgba(0,0,0,.3)', | ||||
| }}>{code}</h1> | }}>{code}</h1> | ||||
| <div className='text-sm'>{unknownReason || (isUnknwonReason ? t('share.common.appUnkonwError') : t('share.common.appUnavailable'))}</div> | |||||
| <div className='text-sm'>{unknownReason || (isUnknownReason ? t('share.common.appUnkonwError') : t('share.common.appUnavailable'))}</div> | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| installedAppInfo, | installedAppInfo, | ||||
| className, | className, | ||||
| }) => { | }) => { | ||||
| const [inited, setInited] = useState(false) | |||||
| const [initialized, setInitialized] = useState(false) | |||||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | ||||
| const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false) | |||||
| const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false) | |||||
| useAsyncEffect(async () => { | useAsyncEffect(async () => { | ||||
| if (!inited) { | |||||
| if (!initialized) { | |||||
| if (!installedAppInfo) { | if (!installedAppInfo) { | ||||
| try { | try { | ||||
| await checkOrSetAccessToken() | await checkOrSetAccessToken() | ||||
| setAppUnavailable(true) | setAppUnavailable(true) | ||||
| } | } | ||||
| else { | else { | ||||
| setIsUnknwonReason(true) | |||||
| setIsUnknownReason(true) | |||||
| setAppUnavailable(true) | setAppUnavailable(true) | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| setInited(true) | |||||
| setInitialized(true) | |||||
| } | } | ||||
| }, []) | }, []) | ||||
| if (appUnavailable) | |||||
| return <AppUnavailable isUnknwonReason={isUnknwonReason} /> | |||||
| if (!inited) | |||||
| if (!initialized) | |||||
| return null | return null | ||||
| if (appUnavailable) | |||||
| return <AppUnavailable isUnknownReason={isUnknownReason} /> | |||||
| return ( | return ( | ||||
| <ChatWithHistoryWrap | <ChatWithHistoryWrap | ||||
| installedAppInfo={installedAppInfo} | installedAppInfo={installedAppInfo} |
| import { useCallback, useEffect, useMemo } from 'react' | |||||
| import cn from 'classnames' | |||||
| import Chat from '../chat' | |||||
| import type { | |||||
| ChatConfig, | |||||
| OnSend, | |||||
| } from '../types' | |||||
| import { useChat } from '../chat/hooks' | |||||
| import { useEmbeddedChatbotContext } from './context' | |||||
| import ConfigPanel from './config-panel' | |||||
| import { isDify } from './utils' | |||||
| import { | |||||
| fetchSuggestedQuestions, | |||||
| getUrl, | |||||
| stopChatMessageResponding, | |||||
| } from '@/service/share' | |||||
| import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar' | |||||
| const ChatWrapper = () => { | |||||
| const { | |||||
| appParams, | |||||
| appPrevChatList, | |||||
| currentConversationId, | |||||
| currentConversationItem, | |||||
| inputsForms, | |||||
| newConversationInputs, | |||||
| handleNewConversationCompleted, | |||||
| isMobile, | |||||
| isInstalledApp, | |||||
| appId, | |||||
| appMeta, | |||||
| handleFeedback, | |||||
| currentChatInstanceRef, | |||||
| } = useEmbeddedChatbotContext() | |||||
| const appConfig = useMemo(() => { | |||||
| const config = appParams || {} | |||||
| return { | |||||
| ...config, | |||||
| supportFeedback: true, | |||||
| opening_statement: currentConversationId ? currentConversationItem?.introduction : (config as any).opening_statement, | |||||
| } as ChatConfig | |||||
| }, [appParams, currentConversationItem?.introduction, currentConversationId]) | |||||
| const { | |||||
| chatList, | |||||
| handleSend, | |||||
| handleStop, | |||||
| isResponding, | |||||
| suggestedQuestions, | |||||
| } = useChat( | |||||
| appConfig, | |||||
| { | |||||
| inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any, | |||||
| promptVariables: inputsForms, | |||||
| }, | |||||
| appPrevChatList, | |||||
| taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), | |||||
| ) | |||||
| useEffect(() => { | |||||
| if (currentChatInstanceRef.current) | |||||
| currentChatInstanceRef.current.handleStop = handleStop | |||||
| }, []) | |||||
| const doSend: OnSend = useCallback((message, files) => { | |||||
| const data: any = { | |||||
| query: message, | |||||
| inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs, | |||||
| conversation_id: currentConversationId, | |||||
| } | |||||
| if (appConfig?.file_upload?.image.enabled && files?.length) | |||||
| data.files = files | |||||
| handleSend( | |||||
| getUrl('chat-messages', isInstalledApp, appId || ''), | |||||
| data, | |||||
| { | |||||
| onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId), | |||||
| onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted, | |||||
| isPublicAPI: !isInstalledApp, | |||||
| }, | |||||
| ) | |||||
| }, [ | |||||
| appConfig, | |||||
| currentConversationId, | |||||
| currentConversationItem, | |||||
| handleSend, | |||||
| newConversationInputs, | |||||
| handleNewConversationCompleted, | |||||
| isInstalledApp, | |||||
| appId, | |||||
| ]) | |||||
| const chatNode = useMemo(() => { | |||||
| if (inputsForms.length) { | |||||
| return ( | |||||
| <> | |||||
| {!currentConversationId && ( | |||||
| <div className={cn('mx-auto w-full max-w-[720px] 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> | |||||
| )} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| return <div className='mb-6' /> | |||||
| }, [currentConversationId, inputsForms, isMobile]) | |||||
| return ( | |||||
| <Chat | |||||
| config={appConfig} | |||||
| chatList={chatList} | |||||
| isResponding={isResponding} | |||||
| chatContainerInnerClassName={cn('mx-auto w-full max-w-[720px] tablet:px-4', isMobile && 'px-4')} | |||||
| chatFooterClassName='pb-4' | |||||
| chatFooterInnerClassName={cn('mx-auto w-full max-w-[720px] tablet:px-4', isMobile && 'px-4')} | |||||
| onSend={doSend} | |||||
| onStopResponding={handleStop} | |||||
| chatNode={chatNode} | |||||
| allToolIcons={appMeta?.tool_icons || {}} | |||||
| onFeedback={handleFeedback} | |||||
| suggestedQuestions={suggestedQuestions} | |||||
| answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null} | |||||
| hideProcessDetail | |||||
| /> | |||||
| ) | |||||
| } | |||||
| export default ChatWrapper |
| import type { FC } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { memo } from 'react' | |||||
| type 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='grow h-[104px] rounded-lg bg-gray-100 px-2.5 py-2 outline-none appearance-none 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) |
| 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' | |||||
| const Form = () => { | |||||
| const { t } = useTranslation() | |||||
| const { | |||||
| inputsForms, | |||||
| newConversationInputs, | |||||
| handleNewConversationInputsChange, | |||||
| isMobile, | |||||
| } = useEmbeddedChatbotContext() | |||||
| const handleFormChange = useCallback((variable: string, value: string) => { | |||||
| handleNewConversationInputsChange({ | |||||
| ...newConversationInputs, | |||||
| [variable]: value, | |||||
| }) | |||||
| }, [newConversationInputs, 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')})` : ''}`} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| 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 |
| import { useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import cn from 'classnames' | |||||
| import { useEmbeddedChatbotContext } from '../context' | |||||
| import Form from './form' | |||||
| 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 { FootLogo } from '@/app/components/share/chat/welcome/massive-component' | |||||
| 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 | |||||
| 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 | |||||
| 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 | |||||
| icon={appData?.site.icon} | |||||
| background='transparent' | |||||
| size='small' | |||||
| /> | |||||
| {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 | |||||
| className='shrink-0 px-2 py-0 h-6 bg-white text-xs font-medium text-primary-600 rounded-md' | |||||
| 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 | |||||
| type='primary' | |||||
| className='mr-2 text-sm font-medium' | |||||
| onClick={() => { | |||||
| setCollapsed(true) | |||||
| handleStartChat() | |||||
| }} | |||||
| > | |||||
| {t('common.operation.save')} | |||||
| </Button> | |||||
| <Button | |||||
| className='text-sm font-medium' | |||||
| onClick={() => setCollapsed(true)} | |||||
| > | |||||
| {t('common.operation.cancel')} | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showConfigPanelBeforeChat && ( | |||||
| <div className='p-6 rounded-b-xl'> | |||||
| <Form /> | |||||
| <Button | |||||
| className={cn('px-4 py-0 h-9', inputsForms.length && !isMobile && 'ml-[136px]')} | |||||
| type='primary' | |||||
| 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.powerBy')}</span> | |||||
| { | |||||
| customConfig?.replace_webapp_logo | |||||
| ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' /> | |||||
| : <FootLogo /> | |||||
| } | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default ConfigPanel |
| 'use client' | |||||
| import type { RefObject } from 'react' | |||||
| import { createContext, useContext } from 'use-context-selector' | |||||
| import type { | |||||
| ChatConfig, | |||||
| ChatItem, | |||||
| Feedback, | |||||
| } from '../types' | |||||
| import type { | |||||
| AppConversationData, | |||||
| AppData, | |||||
| AppMeta, | |||||
| ConversationItem, | |||||
| } from '@/models/share' | |||||
| export type EmbeddedChatbotContextValue = { | |||||
| appInfoError?: any | |||||
| appInfoLoading?: boolean | |||||
| appMeta?: AppMeta | |||||
| appData?: AppData | |||||
| appParams?: ChatConfig | |||||
| appChatListDataLoading?: boolean | |||||
| currentConversationId: string | |||||
| currentConversationItem?: ConversationItem | |||||
| appPrevChatList: ChatItem[] | |||||
| pinnedConversationList: AppConversationData['data'] | |||||
| conversationList: AppConversationData['data'] | |||||
| showConfigPanelBeforeChat: boolean | |||||
| newConversationInputs: Record<string, any> | |||||
| handleNewConversationInputsChange: (v: Record<string, any>) => void | |||||
| inputsForms: any[] | |||||
| handleNewConversation: () => void | |||||
| handleStartChat: () => void | |||||
| handleChangeConversation: (conversationId: string) => void | |||||
| handleNewConversationCompleted: (newConversationId: string) => void | |||||
| chatShouldReloadKey: string | |||||
| isMobile: boolean | |||||
| isInstalledApp: boolean | |||||
| appId?: string | |||||
| handleFeedback: (messageId: string, feedback: Feedback) => void | |||||
| currentChatInstanceRef: RefObject<{ handleStop: () => void }> | |||||
| } | |||||
| export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ | |||||
| currentConversationId: '', | |||||
| appPrevChatList: [], | |||||
| pinnedConversationList: [], | |||||
| conversationList: [], | |||||
| showConfigPanelBeforeChat: false, | |||||
| newConversationInputs: {}, | |||||
| handleNewConversationInputsChange: () => {}, | |||||
| inputsForms: [], | |||||
| handleNewConversation: () => {}, | |||||
| handleStartChat: () => {}, | |||||
| handleChangeConversation: () => {}, | |||||
| handleNewConversationCompleted: () => {}, | |||||
| chatShouldReloadKey: '', | |||||
| isMobile: false, | |||||
| isInstalledApp: false, | |||||
| handleFeedback: () => {}, | |||||
| currentChatInstanceRef: { current: { handleStop: () => {} } }, | |||||
| }) | |||||
| export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext) |
| import type { FC } from 'react' | |||||
| import React from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| // import AppIcon from '@/app/components/base/app-icon' | |||||
| import { ReplayIcon } from '@/app/components/app/chat/icon-component' | |||||
| import Tooltip from '@/app/components/base/tooltip' | |||||
| export type IHeaderProps = { | |||||
| isMobile?: boolean | |||||
| customerIcon?: React.ReactNode | |||||
| title: string | |||||
| // icon: string | |||||
| // icon_background: string | |||||
| onCreateNewChat?: () => void | |||||
| } | |||||
| const Header: FC<IHeaderProps> = ({ | |||||
| isMobile, | |||||
| customerIcon, | |||||
| title, | |||||
| // icon, | |||||
| // icon_background, | |||||
| onCreateNewChat, | |||||
| }) => { | |||||
| const { t } = useTranslation() | |||||
| if (!isMobile) | |||||
| return null | |||||
| 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 | |||||
| `} | |||||
| > | |||||
| <div className="flex items-center space-x-2"> | |||||
| {customerIcon} | |||||
| <div | |||||
| className={'text-sm font-bold text-white'} | |||||
| > | |||||
| {title} | |||||
| </div> | |||||
| </div> | |||||
| <Tooltip | |||||
| selector={'embed-scene-restart-button'} | |||||
| htmlContent={t('share.chat.resetChat')} | |||||
| position='top' | |||||
| > | |||||
| <div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => { | |||||
| onCreateNewChat?.() | |||||
| }}> | |||||
| <ReplayIcon className="h-4 w-4 text-sm font-bold text-white" /> | |||||
| </div> | |||||
| </Tooltip> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default React.memo(Header) |
| import { | |||||
| useCallback, | |||||
| useEffect, | |||||
| useMemo, | |||||
| useRef, | |||||
| useState, | |||||
| } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import useSWR from 'swr' | |||||
| import { useLocalStorageState } from 'ahooks' | |||||
| import produce from 'immer' | |||||
| import type { | |||||
| ChatConfig, | |||||
| ChatItem, | |||||
| Feedback, | |||||
| } from '../types' | |||||
| import { CONVERSATION_ID_INFO } from '../constants' | |||||
| import { | |||||
| fetchAppInfo, | |||||
| fetchAppMeta, | |||||
| fetchAppParams, | |||||
| fetchChatList, | |||||
| fetchConversations, | |||||
| generationConversationName, | |||||
| updateFeedback, | |||||
| } from '@/service/share' | |||||
| import type { | |||||
| // AppData, | |||||
| ConversationItem, | |||||
| } from '@/models/share' | |||||
| import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' | |||||
| import { useToastContext } from '@/app/components/base/toast' | |||||
| import { changeLanguage } from '@/i18n/i18next-config' | |||||
| export const useEmbeddedChatbot = () => { | |||||
| const isInstalledApp = false | |||||
| const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) | |||||
| const appData = useMemo(() => { | |||||
| return appInfo | |||||
| }, [appInfo]) | |||||
| const appId = useMemo(() => appData?.app_id, [appData]) | |||||
| useEffect(() => { | |||||
| if (appInfo?.site.default_language) | |||||
| changeLanguage(appInfo.site.default_language) | |||||
| }, [appInfo]) | |||||
| const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, { | |||||
| defaultValue: {}, | |||||
| }) | |||||
| const currentConversationId = useMemo(() => conversationIdInfo?.[appId || ''] || '', [appId, conversationIdInfo]) | |||||
| const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { | |||||
| if (appId) { | |||||
| setConversationIdInfo({ | |||||
| ...conversationIdInfo, | |||||
| [appId || '']: changeConversationId, | |||||
| }) | |||||
| } | |||||
| }, [appId, conversationIdInfo, setConversationIdInfo]) | |||||
| const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true) | |||||
| const [newConversationId, setNewConversationId] = useState('') | |||||
| const chatShouldReloadKey = useMemo(() => { | |||||
| if (currentConversationId === newConversationId) | |||||
| return '' | |||||
| return currentConversationId | |||||
| }, [currentConversationId, newConversationId]) | |||||
| const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId)) | |||||
| const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId)) | |||||
| const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) | |||||
| const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) | |||||
| const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) | |||||
| const appPrevChatList = useMemo(() => { | |||||
| const data = appChatListData?.data || [] | |||||
| const chatList: ChatItem[] = [] | |||||
| if (currentConversationId && data.length) { | |||||
| data.forEach((item: any) => { | |||||
| chatList.push({ | |||||
| id: `question-${item.id}`, | |||||
| content: item.query, | |||||
| isAnswer: false, | |||||
| message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], | |||||
| }) | |||||
| chatList.push({ | |||||
| id: item.id, | |||||
| content: item.answer, | |||||
| agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files), | |||||
| feedback: item.feedback, | |||||
| isAnswer: true, | |||||
| citation: item.retriever_resources, | |||||
| message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], | |||||
| }) | |||||
| }) | |||||
| } | |||||
| return chatList | |||||
| }, [appChatListData, currentConversationId]) | |||||
| const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) | |||||
| const pinnedConversationList = useMemo(() => { | |||||
| return appPinnedConversationData?.data || [] | |||||
| }, [appPinnedConversationData]) | |||||
| const { t } = useTranslation() | |||||
| const newConversationInputsRef = useRef<Record<string, any>>({}) | |||||
| const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({}) | |||||
| const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => { | |||||
| newConversationInputsRef.current = newInputs | |||||
| setNewConversationInputs(newInputs) | |||||
| }, []) | |||||
| const inputsForms = useMemo(() => { | |||||
| return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input'] || item.number).map((item: any) => { | |||||
| if (item.paragraph) { | |||||
| return { | |||||
| ...item.paragraph, | |||||
| type: 'paragraph', | |||||
| } | |||||
| } | |||||
| if (item.number) { | |||||
| return { | |||||
| ...item.number, | |||||
| type: 'number', | |||||
| } | |||||
| } | |||||
| if (item.select) { | |||||
| return { | |||||
| ...item.select, | |||||
| type: 'select', | |||||
| } | |||||
| } | |||||
| return { | |||||
| ...item['text-input'], | |||||
| type: 'text-input', | |||||
| } | |||||
| }) | |||||
| }, [appParams]) | |||||
| useEffect(() => { | |||||
| const conversationInputs: Record<string, any> = {} | |||||
| inputsForms.forEach((item: any) => { | |||||
| conversationInputs[item.variable] = item.default || '' | |||||
| }) | |||||
| handleNewConversationInputsChange(conversationInputs) | |||||
| }, [handleNewConversationInputsChange, inputsForms]) | |||||
| const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false }) | |||||
| const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([]) | |||||
| useEffect(() => { | |||||
| if (appConversationData?.data && !appConversationDataLoading) | |||||
| setOriginConversationList(appConversationData?.data) | |||||
| }, [appConversationData, appConversationDataLoading]) | |||||
| const conversationList = useMemo(() => { | |||||
| const data = originConversationList.slice() | |||||
| if (showNewConversationItemInList && data[0]?.id !== '') { | |||||
| data.unshift({ | |||||
| id: '', | |||||
| name: t('share.chat.newChatDefaultName'), | |||||
| inputs: {}, | |||||
| introduction: '', | |||||
| }) | |||||
| } | |||||
| return data | |||||
| }, [originConversationList, showNewConversationItemInList, t]) | |||||
| useEffect(() => { | |||||
| if (newConversation) { | |||||
| setOriginConversationList(produce((draft) => { | |||||
| const index = draft.findIndex(item => item.id === newConversation.id) | |||||
| if (index > -1) | |||||
| draft[index] = newConversation | |||||
| else | |||||
| draft.unshift(newConversation) | |||||
| })) | |||||
| } | |||||
| }, [newConversation]) | |||||
| const currentConversationItem = useMemo(() => { | |||||
| let conversationItem = conversationList.find(item => item.id === currentConversationId) | |||||
| if (!conversationItem && pinnedConversationList.length) | |||||
| conversationItem = pinnedConversationList.find(item => item.id === currentConversationId) | |||||
| return conversationItem | |||||
| }, [conversationList, currentConversationId, pinnedConversationList]) | |||||
| const { notify } = useToastContext() | |||||
| const checkInputsRequired = useCallback((silent?: boolean) => { | |||||
| if (inputsForms.length) { | |||||
| for (let i = 0; i < inputsForms.length; i += 1) { | |||||
| const item = inputsForms[i] | |||||
| if (item.required && !newConversationInputsRef.current[item.variable]) { | |||||
| if (!silent) { | |||||
| notify({ | |||||
| type: 'error', | |||||
| message: t('appDebug.errorMessage.valueOfVarRequired', { key: item.variable }), | |||||
| }) | |||||
| } | |||||
| return | |||||
| } | |||||
| } | |||||
| return true | |||||
| } | |||||
| return true | |||||
| }, [inputsForms, notify, t]) | |||||
| const handleStartChat = useCallback(() => { | |||||
| if (checkInputsRequired()) { | |||||
| setShowConfigPanelBeforeChat(false) | |||||
| setShowNewConversationItemInList(true) | |||||
| } | |||||
| }, [setShowConfigPanelBeforeChat, 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]) | |||||
| const handleNewConversation = useCallback(() => { | |||||
| currentChatInstanceRef.current.handleStop() | |||||
| setNewConversationId('') | |||||
| if (showNewConversationItemInList) { | |||||
| handleChangeConversation('') | |||||
| } | |||||
| else if (currentConversationId) { | |||||
| handleConversationIdInfoChange('') | |||||
| setShowConfigPanelBeforeChat(true) | |||||
| setShowNewConversationItemInList(true) | |||||
| handleNewConversationInputsChange({}) | |||||
| } | |||||
| }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange]) | |||||
| const handleNewConversationCompleted = useCallback((newConversationId: string) => { | |||||
| setNewConversationId(newConversationId) | |||||
| handleConversationIdInfoChange(newConversationId) | |||||
| setShowNewConversationItemInList(false) | |||||
| mutateAppConversationData() | |||||
| }, [mutateAppConversationData, handleConversationIdInfoChange]) | |||||
| const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => { | |||||
| await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, appId) | |||||
| notify({ type: 'success', message: t('common.api.success') }) | |||||
| }, [isInstalledApp, appId, t, notify]) | |||||
| return { | |||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| isInstalledApp, | |||||
| appId, | |||||
| currentConversationId, | |||||
| currentConversationItem, | |||||
| handleConversationIdInfoChange, | |||||
| appData, | |||||
| appParams: appParams || {} as ChatConfig, | |||||
| appMeta, | |||||
| appPinnedConversationData, | |||||
| appConversationData, | |||||
| appConversationDataLoading, | |||||
| appChatListData, | |||||
| appChatListDataLoading, | |||||
| appPrevChatList, | |||||
| pinnedConversationList, | |||||
| conversationList, | |||||
| showConfigPanelBeforeChat, | |||||
| setShowConfigPanelBeforeChat, | |||||
| setShowNewConversationItemInList, | |||||
| newConversationInputs, | |||||
| handleNewConversationInputsChange, | |||||
| inputsForms, | |||||
| handleNewConversation, | |||||
| handleStartChat, | |||||
| handleChangeConversation, | |||||
| handleNewConversationCompleted, | |||||
| newConversationId, | |||||
| chatShouldReloadKey, | |||||
| handleFeedback, | |||||
| currentChatInstanceRef, | |||||
| } | |||||
| } |
| import { | |||||
| useEffect, | |||||
| useState, | |||||
| } from 'react' | |||||
| import cn from 'classnames' | |||||
| import { useAsyncEffect } from 'ahooks' | |||||
| import { | |||||
| EmbeddedChatbotContext, | |||||
| useEmbeddedChatbotContext, | |||||
| } from './context' | |||||
| import { useEmbeddedChatbot } from './hooks' | |||||
| import { isDify } from './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-embeded-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' | |||||
| const Chatbot = () => { | |||||
| const { | |||||
| isMobile, | |||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | |||||
| appPrevChatList, | |||||
| showConfigPanelBeforeChat, | |||||
| appChatListDataLoading, | |||||
| handleNewConversation, | |||||
| } = useEmbeddedChatbotContext() | |||||
| const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length) | |||||
| const customConfig = appData?.custom_config | |||||
| const site = appData?.site | |||||
| const difyIcon = <LogoHeader /> | |||||
| useEffect(() => { | |||||
| if (site) { | |||||
| if (customConfig) | |||||
| document.title = `${site.title}` | |||||
| else | |||||
| document.title = `${site.title} - Powered by Dify` | |||||
| } | |||||
| }, [site, customConfig]) | |||||
| if (appInfoLoading) { | |||||
| return ( | |||||
| <Loading type='app' /> | |||||
| ) | |||||
| } | |||||
| if (appInfoError) { | |||||
| return ( | |||||
| <AppUnavailable /> | |||||
| ) | |||||
| } | |||||
| return ( | |||||
| <div> | |||||
| <Header | |||||
| isMobile={isMobile} | |||||
| title={site?.title || ''} | |||||
| customerIcon={isDify() ? difyIcon : ''} | |||||
| 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 && ( | |||||
| <Loading type='app' /> | |||||
| )} | |||||
| {chatReady && !appChatListDataLoading && ( | |||||
| <ChatWrapper /> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| const EmbeddedChatbotWrapper = () => { | |||||
| const media = useBreakpoints() | |||||
| const isMobile = media === MediaType.mobile | |||||
| const { | |||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | |||||
| appParams, | |||||
| appMeta, | |||||
| appChatListDataLoading, | |||||
| currentConversationId, | |||||
| currentConversationItem, | |||||
| appPrevChatList, | |||||
| pinnedConversationList, | |||||
| conversationList, | |||||
| showConfigPanelBeforeChat, | |||||
| newConversationInputs, | |||||
| handleNewConversationInputsChange, | |||||
| inputsForms, | |||||
| handleNewConversation, | |||||
| handleStartChat, | |||||
| handleChangeConversation, | |||||
| handleNewConversationCompleted, | |||||
| chatShouldReloadKey, | |||||
| isInstalledApp, | |||||
| appId, | |||||
| handleFeedback, | |||||
| currentChatInstanceRef, | |||||
| } = useEmbeddedChatbot() | |||||
| return <EmbeddedChatbotContext.Provider value={{ | |||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | |||||
| appParams, | |||||
| appMeta, | |||||
| appChatListDataLoading, | |||||
| currentConversationId, | |||||
| currentConversationItem, | |||||
| appPrevChatList, | |||||
| pinnedConversationList, | |||||
| conversationList, | |||||
| showConfigPanelBeforeChat, | |||||
| newConversationInputs, | |||||
| handleNewConversationInputsChange, | |||||
| inputsForms, | |||||
| handleNewConversation, | |||||
| handleStartChat, | |||||
| handleChangeConversation, | |||||
| handleNewConversationCompleted, | |||||
| chatShouldReloadKey, | |||||
| isMobile, | |||||
| isInstalledApp, | |||||
| appId, | |||||
| handleFeedback, | |||||
| currentChatInstanceRef, | |||||
| }}> | |||||
| <Chatbot /> | |||||
| </EmbeddedChatbotContext.Provider> | |||||
| } | |||||
| const EmbeddedChatbot = () => { | |||||
| const [initialized, setInitialized] = useState(false) | |||||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | |||||
| const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false) | |||||
| useAsyncEffect(async () => { | |||||
| if (!initialized) { | |||||
| try { | |||||
| await checkOrSetAccessToken() | |||||
| } | |||||
| catch (e: any) { | |||||
| if (e.status === 404) { | |||||
| setAppUnavailable(true) | |||||
| } | |||||
| else { | |||||
| setIsUnknownReason(true) | |||||
| setAppUnavailable(true) | |||||
| } | |||||
| } | |||||
| setInitialized(true) | |||||
| } | |||||
| }, []) | |||||
| if (!initialized) | |||||
| return null | |||||
| if (appUnavailable) | |||||
| return <AppUnavailable isUnknownReason={isUnknownReason} /> | |||||
| return <EmbeddedChatbotWrapper /> | |||||
| } | |||||
| export default EmbeddedChatbot |
| export const isDify = () => { | |||||
| return document.referrer.includes('dify.ai') | |||||
| } |
| * app info | * app info | ||||
| */ | */ | ||||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | ||||
| const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false) | |||||
| const [isUnknownReason, setIsUnknwonReason] = useState<boolean>(false) | |||||
| const [appId, setAppId] = useState<string>('') | const [appId, setAppId] = useState<string>('') | ||||
| const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true) | const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true) | ||||
| const [siteInfo, setSiteInfo] = useState<SiteInfo | null>() | const [siteInfo, setSiteInfo] = useState<SiteInfo | null>() | ||||
| }, [appId, messageTaskId, isInstalledApp, installedAppInfo?.id]) | }, [appId, messageTaskId, isInstalledApp, installedAppInfo?.id]) | ||||
| if (appUnavailable) | if (appUnavailable) | ||||
| return <AppUnavailable isUnknwonReason={isUnknwonReason} /> | |||||
| return <AppUnavailable isUnknownReason={isUnknownReason} /> | |||||
| if (!appId || !siteInfo || !promptConfig) { | if (!appId || !siteInfo || !promptConfig) { | ||||
| return <div className='flex h-screen w-full'> | return <div className='flex h-screen w-full'> |
| * app info | * app info | ||||
| */ | */ | ||||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | ||||
| const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false) | |||||
| const [isUnknownReason, setIsUnknwonReason] = useState<boolean>(false) | |||||
| const [appId, setAppId] = useState<string>('') | const [appId, setAppId] = useState<string>('') | ||||
| const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true) | const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true) | ||||
| const [siteInfo, setSiteInfo] = useState<SiteInfo | null>() | const [siteInfo, setSiteInfo] = useState<SiteInfo | null>() | ||||
| ) | ) | ||||
| if (appUnavailable) | if (appUnavailable) | ||||
| return <AppUnavailable isUnknwonReason={isUnknwonReason} /> | |||||
| return <AppUnavailable isUnknownReason={isUnknownReason} /> | |||||
| if (!appId || !siteInfo || !promptConfig) { | if (!appId || !siteInfo || !promptConfig) { | ||||
| return <div className='flex h-screen w-full'> | return <div className='flex h-screen w-full'> |