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>(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>({}) const [newConversationInputs, setNewConversationInputs] = useState>({}) const handleNewConversationInputsChange = useCallback((newInputs: Record) => { 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 = {} 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([]) 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, } }