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