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 { message } from "antd"; import { client, getInfo } from "@/app/api/utils/common"; import type { Callback, ChatConfig, ChatItem, Feedback } from "../types"; import { CONVERSATION_ID_INFO } from "../constants"; import { buildChatItemTree } from "../utils"; import { addFileInfos, sortAgentSorts } from "@/app/components/tools/utils"; import { getProcessedFilesFromResponse } from "@/app/components/base/file-uploader/utils"; import { delConversation, fetchAppInfo, fetchAppMeta, fetchAppParams, fetchChatList, fetchConversations, generationConversationName, pinConversation, renameConversation, unpinConversation, updateFeedback, } from "@/service/index"; 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"; import FeedbackModal from "@/app/components/chat-with-history/FeedbackModal"; // 引入 FeedbackModal 组件 function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = []; messages.forEach((item) => { const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === "user") || []; newChatList.push({ id: `question-${item.id}`, content: item.query, isAnswer: false, message_files: getProcessedFilesFromResponse( questionFiles.map((item: any) => ({ ...item, related_id: item.id })) ), parentMessageId: item.parent_message_id || undefined, }); const answerFiles = item.message_files?.filter( (file: any) => file.belongs_to === "assistant" ) || []; if (answerFiles.length > 0 && questionFiles.length > 0) // 获取本地文件名 answerFiles[0].filename = questionFiles[0]?.filename; newChatList.push({ id: item.id, content: item.answer, agent_thoughts: addFileInfos( item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files ), feedback: item.feedback, isAnswer: true, citation: item.retriever_resources, message_files: getProcessedFilesFromResponse( answerFiles.map((item: any) => ({ ...item, related_id: item.id })) ), parentMessageId: `question-${item.id}`, }); }); return newChatList; } 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 >(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() ); const { data: appMeta } = useSWR(["appMeta", isInstalledApp, appId], () => fetchAppMeta() ); const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData, } = useSWR(["appConversationData", isInstalledApp, appId, true], () => fetchConversations() ); const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData, } = useSWR(["appConversationData", isInstalledApp, appId, false], () => fetchConversations() ); const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR( chatShouldReloadKey ? ["appChatList", chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey) ); const appPrevChatTree = useMemo( () => currentConversationId && appChatListData?.data.length ? buildChatItemTree(getFormattedChatList(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< Record >({}); 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 || null; }); handleNewConversationInputsChange(conversationInputs); }, [handleNewConversationInputsChange, inputsForms]); const { data: newConversation } = useSWR( newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(newConversationId) ); 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(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(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 [isFeedbackModalVisible, setIsFeedbackModalVisible] = useState(false); const [currentMessageId, setCurrentMessageId] = useState(null); const [currentFeedback, setCurrentFeedback] = useState(null); const [ currentConversationIdForFeedback, setCurrentConversationIdForFeedback, ] = useState(null); const handleFeedback = useCallback( (messageId: string, feedback: Feedback, conversationId: string) => { setCurrentMessageId(messageId); setCurrentFeedback(feedback); setCurrentConversationIdForFeedback(conversationId); setIsFeedbackModalVisible(true); }, [] ); const handleFeedbackSubmit = useCallback( async (selectedOption: number | null, feedbackText: string) => { console.log(currentMessageId); console.log(currentFeedback); try { // 构造请求参数 const requestBody = { operationType: selectedOption, // 0 或 1 feedbackText, // 用户反馈建议 conversationId: '89fba8a7-e4e2-4167-8b7c-b3c103797053', // 会话 ID messageId: currentMessageId, // 消息 ID username: "user_admin", // 用户名 }; // 调用 Java 接口 const javaResponse = await fetch( `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/conversation/feedback`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestBody), } ); // 调用原有的 updateFeedback 函数 await updateFeedback( { url: `/messages/${currentMessageId}/feedbacks`, body: { rating: currentFeedback.rating }, }, isInstalledApp, appId ); // 显示成功通知 notify({ type: "success", message: t("common.api.success") }); // 关闭对话框 setIsFeedbackModalVisible(false); } catch (error) { console.error("Error:", error); notify({ type: "error", message: t("common.api.failed") }); } }, [ currentMessageId, currentFeedback, currentConversationIdForFeedback, isInstalledApp, appId, notify, t, ] ); return { appInfoError, appInfoLoading, isInstalledApp, appId, currentConversationId, currentConversationItem, handleConversationIdInfoChange, appData, appParams: appParams || ({} as ChatConfig), appMeta, appPinnedConversationData, appConversationData, appConversationDataLoading, appChatListData, appChatListDataLoading, appPrevChatTree, pinnedConversationList, conversationList, showConfigPanelBeforeChat, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, newConversationInputs, newConversationInputsRef, handleNewConversationInputsChange, inputsForms, handleNewConversation, handleStartChat, handleChangeConversation, handlePinConversation, handleUnpinConversation, conversationDeleting, handleDeleteConversation, conversationRenaming, handleRenameConversation, handleNewConversationCompleted, newConversationId, chatShouldReloadKey, handleFeedback, currentChatInstanceRef, feedbackModal: ( setIsFeedbackModalVisible(false)} /> ), }; };