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