Spaces:
Build error
Build error
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 { | |
Callback, | |
ChatConfig, | |
Feedback, | |
} from '../types' | |
import { CONVERSATION_ID_INFO } from '../constants' | |
import { getPrevChatList } from '../utils' | |
import { | |
delConversation, | |
fetchAppInfo, | |
fetchAppMeta, | |
fetchAppParams, | |
fetchChatList, | |
fetchConversations, | |
generationConversationName, | |
pinConversation, | |
renameConversation, | |
unpinConversation, | |
updateFeedback, | |
} from '@/service/share' | |
import type { InstalledApp } from '@/models/explore' | |
import type { | |
AppData, | |
ConversationItem, | |
} from '@/models/share' | |
import { useToastContext } from '@/app/components/base/toast' | |
import { changeLanguage } from '@/i18n/i18next-config' | |
import { useAppFavicon } from '@/hooks/use-app-favicon' | |
import { InputVarType } from '@/app/components/workflow/types' | |
import { TransferMethod } from '@/types/app' | |
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | |
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) | |
useAppFavicon({ | |
enable: !installedAppInfo, | |
icon_type: appInfo?.site.icon_type, | |
icon: appInfo?.site.icon, | |
icon_background: appInfo?.site.icon_background, | |
icon_url: appInfo?.site.icon_url, | |
}) | |
const appData = useMemo(() => { | |
if (isInstalledApp) { | |
const { id, app } = installedAppInfo! | |
return { | |
app_id: id, | |
site: { | |
title: app.name, | |
icon_type: app.icon_type, | |
icon: app.icon, | |
icon_background: app.icon_background, | |
icon_url: app.icon_url, | |
prompt_public: false, | |
copyright: '', | |
show_workflow_steps: true, | |
use_icon_as_answer_icon: app.use_icon_as_answer_icon, | |
}, | |
plan: 'basic', | |
} as AppData | |
} | |
return appInfo | |
}, [isInstalledApp, installedAppInfo, appInfo]) | |
const appId = useMemo(() => appData?.app_id, [appData]) | |
useEffect(() => { | |
if (appData?.site.default_language) | |
changeLanguage(appData.site.default_language) | |
}, [appData]) | |
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, mutate: mutateAppPinnedConversationData } = 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 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) { | |
return { | |
...item.paragraph, | |
type: 'paragraph', | |
} | |
} | |
if (item.number) { | |
return { | |
...item.number, | |
type: 'number', | |
} | |
} | |
if (item.select) { | |
return { | |
...item.select, | |
type: 'select', | |
} | |
} | |
if (item['file-list']) { | |
return { | |
...item['file-list'], | |
type: 'file-list', | |
} | |
} | |
if (item.file) { | |
return { | |
...item.file, | |
type: 'file', | |
} | |
} | |
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) => { | |
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 handleUpdateConversationList = useCallback(() => { | |
mutateAppConversationData() | |
mutateAppPinnedConversationData() | |
}, [mutateAppConversationData, mutateAppPinnedConversationData]) | |
const handlePinConversation = useCallback(async (conversationId: string) => { | |
await pinConversation(isInstalledApp, appId, conversationId) | |
notify({ type: 'success', message: t('common.api.success') }) | |
handleUpdateConversationList() | |
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) | |
const handleUnpinConversation = useCallback(async (conversationId: string) => { | |
await unpinConversation(isInstalledApp, appId, conversationId) | |
notify({ type: 'success', message: t('common.api.success') }) | |
handleUpdateConversationList() | |
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) | |
const [conversationDeleting, setConversationDeleting] = useState(false) | |
const handleDeleteConversation = useCallback(async ( | |
conversationId: string, | |
{ | |
onSuccess, | |
}: Callback, | |
) => { | |
if (conversationDeleting) | |
return | |
try { | |
setConversationDeleting(true) | |
await delConversation(isInstalledApp, appId, conversationId) | |
notify({ type: 'success', message: t('common.api.success') }) | |
onSuccess() | |
} | |
finally { | |
setConversationDeleting(false) | |
} | |
if (conversationId === currentConversationId) | |
handleNewConversation() | |
handleUpdateConversationList() | |
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting]) | |
const [conversationRenaming, setConversationRenaming] = useState(false) | |
const handleRenameConversation = useCallback(async ( | |
conversationId: string, | |
newName: string, | |
{ | |
onSuccess, | |
}: Callback, | |
) => { | |
if (conversationRenaming) | |
return | |
if (!newName.trim()) { | |
notify({ | |
type: 'error', | |
message: t('common.chat.conversationNameCanNotEmpty'), | |
}) | |
return | |
} | |
setConversationRenaming(true) | |
try { | |
await renameConversation(isInstalledApp, appId, conversationId, newName) | |
notify({ | |
type: 'success', | |
message: t('common.actionMsg.modifiedSuccessfully'), | |
}) | |
setOriginConversationList(produce((draft) => { | |
const index = originConversationList.findIndex(item => item.id === conversationId) | |
const item = draft[index] | |
draft[index] = { | |
...item, | |
name: newName, | |
} | |
})) | |
onSuccess() | |
} | |
finally { | |
setConversationRenaming(false) | |
} | |
}, [isInstalledApp, appId, notify, t, conversationRenaming, originConversationList]) | |
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, | |
handlePinConversation, | |
handleUnpinConversation, | |
conversationDeleting, | |
handleDeleteConversation, | |
conversationRenaming, | |
handleRenameConversation, | |
handleNewConversationCompleted, | |
newConversationId, | |
chatShouldReloadKey, | |
handleFeedback, | |
currentChatInstanceRef, | |
} | |
} | |