|
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, |
|
Feedback, |
|
} from '../types' |
|
import { CONVERSATION_ID_INFO } from '../constants' |
|
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils' |
|
import { |
|
fetchAppInfo, |
|
fetchAppMeta, |
|
fetchAppParams, |
|
fetchChatList, |
|
fetchConversations, |
|
generationConversationName, |
|
updateFeedback, |
|
} from '@/service/share' |
|
import type { |
|
|
|
ConversationItem, |
|
} from '@/models/share' |
|
import { useToastContext } from '@/app/components/base/toast' |
|
import { changeLanguage } from '@/i18n/i18next-config' |
|
import { InputVarType } from '@/app/components/workflow/types' |
|
import { TransferMethod } from '@/types/app' |
|
|
|
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( |
|
() => (currentConversationId && appChatListData?.data.length) |
|
? getPrevChatList(appChatListData.data) |
|
: [], |
|
[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 [initInputs, setInitInputs] = 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.external_data_tool).map((item: any) => { |
|
if (item.paragraph) { |
|
let value = initInputs[item.paragraph.variable] |
|
if (value && item.paragraph.max_length && value.length > item.paragraph.max_length) |
|
value = value.slice(0, item.paragraph.max_length) |
|
|
|
return { |
|
...item.paragraph, |
|
default: value || item.default, |
|
type: 'paragraph', |
|
} |
|
} |
|
if (item.number) { |
|
const convertedNumber = Number(initInputs[item.number.variable]) ?? undefined |
|
return { |
|
...item.number, |
|
default: convertedNumber || item.default, |
|
type: 'number', |
|
} |
|
} |
|
if (item.select) { |
|
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable]) |
|
return { |
|
...item.select, |
|
default: (isInputInOptions ? initInputs[item.select.variable] : undefined) || item.default, |
|
type: 'select', |
|
} |
|
} |
|
|
|
if (item['file-list']) { |
|
return { |
|
...item['file-list'], |
|
type: 'file-list', |
|
} |
|
} |
|
|
|
if (item.file) { |
|
return { |
|
...item.file, |
|
type: 'file', |
|
} |
|
} |
|
|
|
let value = initInputs[item['text-input'].variable] |
|
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length) |
|
value = value.slice(0, item['text-input'].max_length) |
|
|
|
return { |
|
...item['text-input'], |
|
default: value || item.default, |
|
type: 'text-input', |
|
} |
|
}) |
|
}, [initInputs, appParams]) |
|
|
|
useEffect(() => { |
|
|
|
setInitInputs(getProcessedInputsFromUrlParams()) |
|
}, []) |
|
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) => { |
|
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 (!newConversationInputsRef.current[variable] && !silent) |
|
hasEmptyInput = label as string |
|
|
|
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) { |
|
const files = newConversationInputsRef.current[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) { |
|
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) |
|
return false |
|
} |
|
|
|
if (fileIsUploading) { |
|
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') }) |
|
return |
|
} |
|
|
|
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, |
|
newConversationInputsRef, |
|
handleNewConversationInputsChange, |
|
inputsForms, |
|
handleNewConversation, |
|
handleStartChat, |
|
handleChangeConversation, |
|
handleNewConversationCompleted, |
|
newConversationId, |
|
chatShouldReloadKey, |
|
handleFeedback, |
|
currentChatInstanceRef, |
|
} |
|
} |
|
|