| 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, |
| } |
| } |
|
|