2025-07-10 09:37:12 +08:00

657 lines
19 KiB
TypeScript

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<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()
);
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<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 || 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<string | null>(null);
const [currentFeedback, setCurrentFeedback] = useState<Feedback | null>(null);
const [
currentConversationIdForFeedback,
setCurrentConversationIdForFeedback,
] = useState<string | null>(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: (
<FeedbackModal
open={isFeedbackModalVisible}
onOk={handleFeedbackSubmit}
onCancel={() => setIsFeedbackModalVisible(false)}
/>
),
};
};