Merge branch 'zhangyuankun-main-patch-48108' into 'zhangyuankun-main-patch-41282'

Zhangyuankun main patch 48108

See merge request line-group/dify-conversation!1
This commit is contained in:
Administrator 2025-07-10 13:35:37 +08:00
commit fe1b17c408
6 changed files with 710 additions and 468 deletions

View File

@ -6,7 +6,7 @@ import { API_KEY, API_URL } from '@/config'
export const getInfo = (request: NextRequest) => { export const getInfo = (request: NextRequest) => {
const username = request.cookies.get('username')?.value || 'no-user' const username = request.cookies.get('username')?.value || 'no-user'
const sessionId = request.cookies.get('session_id')?.value || v4() const sessionId = request.cookies.get('session_id')?.value || v4()
const user = `user_${username}:${sessionId}` const user = `${username}`
return { return {
sessionId, sessionId,
user, user,

View File

@ -0,0 +1,55 @@
import React, { useState } from 'react';
import { Modal, Checkbox, Input, message } from 'antd';
const { TextArea } = Input;
interface FeedbackModalProps {
open: boolean;
onOk: (selectedOption: number | null, feedbackText: string) => void;
onCancel: () => void;
}
const FeedbackModal: React.FC<FeedbackModalProps> = ({ open, onOk, onCancel }) => {
const [selectedOption, setSelectedOption] = useState<number | null>(null);
const [feedbackText, setFeedbackText] = useState<string>('');
const handleOk = () => {
if (selectedOption === null || !feedbackText) {
message.warning('请选择操作类型并填写反馈建议');
return;
}
onOk(selectedOption, feedbackText);
};
return (
<Modal
title="反馈"
open={open}
onOk={handleOk}
onCancel={onCancel}
>
<div>
<Checkbox
checked={selectedOption === 0}
onChange={() => setSelectedOption(0)}
>
</Checkbox>
<Checkbox
checked={selectedOption === 1}
onChange={() => setSelectedOption(1)}
>
</Checkbox>
</div>
<TextArea
rows={4}
placeholder="请输入您的反馈建议"
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
/>
</Modal>
);
};
export default FeedbackModal;

View File

@ -1,25 +1,15 @@
import { import { useCallback, useEffect, useMemo, useRef, useState } from "react";
useCallback, import { useTranslation } from "react-i18next";
useEffect, import useSWR from "swr";
useMemo, import { useLocalStorageState } from "ahooks";
useRef, import { produce } from "immer";
useState, import { message } from "antd";
} from 'react' import { client, getInfo } from "@/app/api/utils/common";
import { useTranslation } from 'react-i18next' import type { Callback, ChatConfig, ChatItem, Feedback } from "../types";
import useSWR from 'swr' import { CONVERSATION_ID_INFO } from "../constants";
import { useLocalStorageState } from 'ahooks' import { buildChatItemTree } from "../utils";
import { produce } from 'immer' import { addFileInfos, sortAgentSorts } from "@/app/components/tools/utils";
import type { import { getProcessedFilesFromResponse } from "@/app/components/base/file-uploader/utils";
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'
// TODO mars
import { import {
delConversation, delConversation,
fetchAppInfo, fetchAppInfo,
@ -32,50 +22,66 @@ import {
renameConversation, renameConversation,
unpinConversation, unpinConversation,
updateFeedback, updateFeedback,
} from '@/service/index' } from "@/service/index";
import type { InstalledApp } from '@/models/explore' import type { InstalledApp } from "@/models/explore";
import type { import type { AppData, ConversationItem } from "@/models/share";
AppData, import { useToastContext } from "@/app/components/base/toast";
ConversationItem, import { changeLanguage } from "@/i18n/i18next-config";
} from '@/models/share' import { useAppFavicon } from "@/hooks/use-app-favicon";
import { useToastContext } from '@/app/components/base/toast' import { InputVarType } from "@/app/components/workflow/types";
import { changeLanguage } from '@/i18n/i18next-config' import { TransferMethod } from "@/types/app";
import { useAppFavicon } from '@/hooks/use-app-favicon' import FeedbackModal from "@/app/components/chat-with-history/FeedbackModal"; // 引入 FeedbackModal 组件
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
function getFormattedChatList(messages: any[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = [];
messages.forEach((item) => { messages.forEach((item) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || [] const questionFiles =
item.message_files?.filter((file: any) => file.belongs_to === "user") ||
[];
newChatList.push({ newChatList.push({
id: `question-${item.id}`, id: `question-${item.id}`,
content: item.query, content: item.query,
isAnswer: false, isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))), message_files: getProcessedFilesFromResponse(
questionFiles.map((item: any) => ({ ...item, related_id: item.id }))
),
parentMessageId: item.parent_message_id || undefined, parentMessageId: item.parent_message_id || undefined,
}) });
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [] const answerFiles =
item.message_files?.filter(
(file: any) => file.belongs_to === "assistant"
) || [];
if (answerFiles.length > 0 && questionFiles.length > 0) if (answerFiles.length > 0 && questionFiles.length > 0)
// 获取本地文件名 // 获取本地文件名
answerFiles[0].filename = questionFiles[0]?.filename answerFiles[0].filename = questionFiles[0]?.filename;
newChatList.push({ newChatList.push({
id: item.id, id: item.id,
content: item.answer, content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files), agent_thoughts: addFileInfos(
item.agent_thoughts
? sortAgentSorts(item.agent_thoughts)
: item.agent_thoughts,
item.message_files
),
feedback: item.feedback, feedback: item.feedback,
isAnswer: true, isAnswer: true,
citation: item.retriever_resources, citation: item.retriever_resources,
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))), message_files: getProcessedFilesFromResponse(
answerFiles.map((item: any) => ({ ...item, related_id: item.id }))
),
parentMessageId: `question-${item.id}`, parentMessageId: `question-${item.id}`,
}) });
}) });
return newChatList return newChatList;
} }
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]);
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) const {
data: appInfo,
isLoading: appInfoLoading,
error: appInfoError,
} = useSWR(installedAppInfo ? null : "appInfo", fetchAppInfo);
useAppFavicon({ useAppFavicon({
enable: !installedAppInfo, enable: !installedAppInfo,
@ -83,11 +89,11 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
icon: appInfo?.site?.icon, icon: appInfo?.site?.icon,
icon_background: appInfo?.site?.icon_background, icon_background: appInfo?.site?.icon_background,
icon_url: appInfo?.site?.icon_url, icon_url: appInfo?.site?.icon_url,
}) });
const appData = useMemo(() => { const appData = useMemo(() => {
if (isInstalledApp) { if (isInstalledApp) {
const { id, app } = installedAppInfo! const { id, app } = installedAppInfo!;
return { return {
app_id: id, app_id: id,
site: { site: {
@ -97,326 +103,507 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
icon_background: app.icon_background, icon_background: app.icon_background,
icon_url: app.icon_url, icon_url: app.icon_url,
prompt_public: false, prompt_public: false,
copyright: '', copyright: "",
show_workflow_steps: true, show_workflow_steps: true,
use_icon_as_answer_icon: app.use_icon_as_answer_icon, use_icon_as_answer_icon: app.use_icon_as_answer_icon,
}, },
plan: 'basic', plan: "basic",
} as AppData } as AppData;
} }
return appInfo return appInfo;
}, [isInstalledApp, installedAppInfo, appInfo]) }, [isInstalledApp, installedAppInfo, appInfo]);
const appId = useMemo(() => appData?.app_id, [appData]) const appId = useMemo(() => appData?.app_id, [appData]);
useEffect(() => { useEffect(() => {
if (appData?.site?.default_language) if (appData?.site?.default_language)
changeLanguage(appData.site?.default_language) changeLanguage(appData.site?.default_language);
}, [appData]) }, [appData]);
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, { const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<
Record<string, string>
>(CONVERSATION_ID_INFO, {
defaultValue: {}, defaultValue: {},
}) });
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || ''] || '', [appId, conversationIdInfo]) const currentConversationId = useMemo(
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { () => conversationIdInfo?.[appId || ""] || "",
if (appId) { [appId, conversationIdInfo]
setConversationIdInfo({ );
...conversationIdInfo, const handleConversationIdInfoChange = useCallback(
[appId || '']: changeConversationId, (changeConversationId: string) => {
}) if (appId) {
} setConversationIdInfo({
}, [appId, conversationIdInfo, setConversationIdInfo]) ...conversationIdInfo,
const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true) [appId || ""]: changeConversationId,
const [newConversationId, setNewConversationId] = useState('') });
}
},
[appId, conversationIdInfo, setConversationIdInfo]
);
const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] =
useState(true);
const [newConversationId, setNewConversationId] = useState("");
const chatShouldReloadKey = useMemo(() => { const chatShouldReloadKey = useMemo(() => {
if (currentConversationId === newConversationId) if (currentConversationId === newConversationId) return "";
return ''
return currentConversationId return currentConversationId;
}, [currentConversationId, newConversationId]) }, [currentConversationId, newConversationId]);
const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams()) const { data: appParams } = useSWR(["appParams", isInstalledApp, appId], () =>
const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta()) fetchAppParams()
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: appMeta } = useSWR(["appMeta", isInstalledApp, appId], () =>
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey)) 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( const appPrevChatTree = useMemo(
() => (currentConversationId && appChatListData?.data.length) () =>
? buildChatItemTree(getFormattedChatList(appChatListData.data)) currentConversationId && appChatListData?.data.length
: [], ? buildChatItemTree(getFormattedChatList(appChatListData.data))
[appChatListData, currentConversationId], : [],
) [appChatListData, currentConversationId]
);
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) const [showNewConversationItemInList, setShowNewConversationItemInList] =
useState(false);
const pinnedConversationList = useMemo(() => { const pinnedConversationList = useMemo(() => {
return appPinnedConversationData?.data || [] return appPinnedConversationData?.data || [];
}, [appPinnedConversationData]) }, [appPinnedConversationData]);
const { t } = useTranslation() const { t } = useTranslation();
const newConversationInputsRef = useRef<Record<string, any>>({}) const newConversationInputsRef = useRef<Record<string, any>>({});
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({}) const [newConversationInputs, setNewConversationInputs] = useState<
const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => { Record<string, any>
newConversationInputsRef.current = newInputs >({});
setNewConversationInputs(newInputs) const handleNewConversationInputsChange = useCallback(
}, []) (newInputs: Record<string, any>) => {
newConversationInputsRef.current = newInputs;
setNewConversationInputs(newInputs);
},
[]
);
const inputsForms = useMemo(() => { const inputsForms = useMemo(() => {
return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => { return (appParams?.user_input_form || [])
if (item.paragraph) { .filter((item: any) => !item.external_data_tool)
return { .map((item: any) => {
...item.paragraph, if (item.paragraph) {
type: 'paragraph', return {
...item.paragraph,
type: "paragraph",
};
} }
} if (item.number) {
if (item.number) { return {
return { ...item.number,
...item.number, type: "number",
type: 'number', };
} }
} if (item.select) {
if (item.select) { return {
return { ...item.select,
...item.select, type: "select",
type: 'select', };
} }
}
if (item['file-list']) { if (item["file-list"]) {
return { return {
...item['file-list'], ...item["file-list"],
type: 'file-list', type: "file-list",
};
} }
}
if (item.file) { if (item.file) {
return { return {
...item.file, ...item.file,
type: 'file', type: "file",
};
} }
}
return { return {
...item['text-input'], ...item["text-input"],
type: 'text-input', type: "text-input",
} };
}) });
}, [appParams]) }, [appParams]);
useEffect(() => { useEffect(() => {
const conversationInputs: Record<string, any> = {} const conversationInputs: Record<string, any> = {};
inputsForms.forEach((item: any) => { inputsForms.forEach((item: any) => {
conversationInputs[item.variable] = item.default || null conversationInputs[item.variable] = item.default || null;
}) });
handleNewConversationInputsChange(conversationInputs) handleNewConversationInputsChange(conversationInputs);
}, [handleNewConversationInputsChange, inputsForms]) }, [handleNewConversationInputsChange, inputsForms]);
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(newConversationId)) const { data: newConversation } = useSWR(
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([]) newConversationId ? [isInstalledApp, appId, newConversationId] : null,
() => generationConversationName(newConversationId)
);
const [originConversationList, setOriginConversationList] = useState<
ConversationItem[]
>([]);
useEffect(() => { useEffect(() => {
if (appConversationData?.data && !appConversationDataLoading) if (appConversationData?.data && !appConversationDataLoading)
setOriginConversationList(appConversationData?.data) setOriginConversationList(appConversationData?.data);
}, [appConversationData, appConversationDataLoading]) }, [appConversationData, appConversationDataLoading]);
const conversationList = useMemo(() => { const conversationList = useMemo(() => {
const data = originConversationList.slice() const data = originConversationList.slice();
if (showNewConversationItemInList && data[0]?.id !== '') { if (showNewConversationItemInList && data[0]?.id !== "") {
data.unshift({ data.unshift({
id: '', id: "",
name: t('share.chat.newChatDefaultName'), name: t("share.chat.newChatDefaultName"),
inputs: {}, inputs: {},
introduction: '', introduction: "",
}) });
} }
return data return data;
}, [originConversationList, showNewConversationItemInList, t]) }, [originConversationList, showNewConversationItemInList, t]);
useEffect(() => { useEffect(() => {
if (newConversation) { if (newConversation) {
setOriginConversationList(produce((draft) => { setOriginConversationList(
const index = draft.findIndex(item => item.id === newConversation.id) produce((draft) => {
const index = draft.findIndex(
(item) => item.id === newConversation.id
);
if (index > -1) if (index > -1) draft[index] = newConversation;
draft[index] = newConversation else draft.unshift(newConversation);
else })
draft.unshift(newConversation) );
}))
} }
}, [newConversation]) }, [newConversation]);
const currentConversationItem = useMemo(() => { const currentConversationItem = useMemo(() => {
let conversationItem = conversationList.find(item => item.id === currentConversationId) let conversationItem = conversationList.find(
(item) => item.id === currentConversationId
);
if (!conversationItem && pinnedConversationList.length) if (!conversationItem && pinnedConversationList.length)
conversationItem = pinnedConversationList.find(item => item.id === currentConversationId) conversationItem = pinnedConversationList.find(
(item) => item.id === currentConversationId
);
return conversationItem return conversationItem;
}, [conversationList, currentConversationId, pinnedConversationList]) }, [conversationList, currentConversationId, pinnedConversationList]);
const { notify } = useToastContext() const { notify } = useToastContext();
const checkInputsRequired = useCallback((silent?: boolean) => { const checkInputsRequired = useCallback(
let hasEmptyInput = '' (silent?: boolean) => {
let fileIsUploading = false let hasEmptyInput = "";
const requiredVars = inputsForms.filter(({ required }) => required) let fileIsUploading = false;
if (requiredVars.length) { const requiredVars = inputsForms.filter(({ required }) => required);
requiredVars.forEach(({ variable, label, type }) => { if (requiredVars.length) {
if (hasEmptyInput) requiredVars.forEach(({ variable, label, type }) => {
return if (hasEmptyInput) return;
if (fileIsUploading) if (fileIsUploading) return;
return
if (!newConversationInputsRef.current[variable] && !silent) if (!newConversationInputsRef.current[variable] && !silent)
hasEmptyInput = label as string hasEmptyInput = label as string;
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) { if (
const files = newConversationInputsRef.current[variable] (type === InputVarType.singleFile ||
if (Array.isArray(files)) type === InputVarType.multiFiles) &&
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId) newConversationInputsRef.current[variable] &&
else !silent
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId ) {
} 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) { if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) notify({
return false type: "error",
} message: t("appDebug.errorMessage.valueOfVarRequired", {
key: hasEmptyInput,
}),
});
return false;
}
if (fileIsUploading) { if (fileIsUploading) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') }) notify({
return type: "info",
} message: t("appDebug.errorMessage.waitForFileUpload"),
});
return;
}
return true return true;
}, [inputsForms, notify, t]) },
[inputsForms, notify, t]
);
const handleStartChat = useCallback(() => { const handleStartChat = useCallback(() => {
if (checkInputsRequired()) { if (checkInputsRequired()) {
setShowConfigPanelBeforeChat(false) setShowConfigPanelBeforeChat(false);
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true);
} }
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired]) }, [
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } }) setShowConfigPanelBeforeChat,
const handleChangeConversation = useCallback((conversationId: string) => { setShowNewConversationItemInList,
currentChatInstanceRef.current.handleStop() checkInputsRequired,
setNewConversationId('') ]);
handleConversationIdInfoChange(conversationId) const currentChatInstanceRef = useRef<{ handleStop: () => void }>({
handleStop: () => {},
});
const handleChangeConversation = useCallback(
(conversationId: string) => {
currentChatInstanceRef.current.handleStop();
setNewConversationId("");
handleConversationIdInfoChange(conversationId);
if (conversationId === '' && !checkInputsRequired(true)) if (conversationId === "" && !checkInputsRequired(true))
setShowConfigPanelBeforeChat(true) setShowConfigPanelBeforeChat(true);
else else setShowConfigPanelBeforeChat(false);
setShowConfigPanelBeforeChat(false) },
}, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired]) [
handleConversationIdInfoChange,
setShowConfigPanelBeforeChat,
checkInputsRequired,
]
);
const handleNewConversation = useCallback(() => { const handleNewConversation = useCallback(() => {
currentChatInstanceRef.current.handleStop() currentChatInstanceRef.current.handleStop();
setNewConversationId('') setNewConversationId("");
if (showNewConversationItemInList) { if (showNewConversationItemInList) {
handleChangeConversation('') handleChangeConversation("");
} else if (currentConversationId) {
handleConversationIdInfoChange("");
setShowConfigPanelBeforeChat(true);
setShowNewConversationItemInList(true);
handleNewConversationInputsChange({});
} }
else if (currentConversationId) { }, [
handleConversationIdInfoChange('') handleChangeConversation,
setShowConfigPanelBeforeChat(true) currentConversationId,
setShowNewConversationItemInList(true) handleConversationIdInfoChange,
handleNewConversationInputsChange({}) setShowConfigPanelBeforeChat,
} setShowNewConversationItemInList,
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange]) showNewConversationItemInList,
handleNewConversationInputsChange,
]);
const handleUpdateConversationList = useCallback(() => { const handleUpdateConversationList = useCallback(() => {
mutateAppConversationData() mutateAppConversationData();
mutateAppPinnedConversationData() mutateAppPinnedConversationData();
}, [mutateAppConversationData, mutateAppPinnedConversationData]) }, [mutateAppConversationData, mutateAppPinnedConversationData]);
const handlePinConversation = useCallback(async (conversationId: string) => { const handlePinConversation = useCallback(
await pinConversation(isInstalledApp, appId, conversationId) async (conversationId: string) => {
notify({ type: 'success', message: t('common.api.success') }) await pinConversation(isInstalledApp, appId, conversationId);
handleUpdateConversationList() notify({ type: "success", message: t("common.api.success") });
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) handleUpdateConversationList();
},
[isInstalledApp, appId, notify, t, handleUpdateConversationList]
);
const handleUnpinConversation = useCallback(async (conversationId: string) => { const handleUnpinConversation = useCallback(
await unpinConversation(isInstalledApp, appId, conversationId) async (conversationId: string) => {
notify({ type: 'success', message: t('common.api.success') }) await unpinConversation(isInstalledApp, appId, conversationId);
handleUpdateConversationList() notify({ type: "success", message: t("common.api.success") });
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) handleUpdateConversationList();
},
[isInstalledApp, appId, notify, t, handleUpdateConversationList]
);
const [conversationDeleting, setConversationDeleting] = useState(false) const [conversationDeleting, setConversationDeleting] = useState(false);
const handleDeleteConversation = useCallback(async ( const handleDeleteConversation = useCallback(
conversationId: string, async (conversationId: string, { onSuccess }: Callback) => {
{ if (conversationDeleting) return;
onSuccess,
}: Callback,
) => {
if (conversationDeleting)
return
try { try {
setConversationDeleting(true) setConversationDeleting(true);
await delConversation(conversationId) await delConversation(conversationId);
notify({ type: 'success', message: t('common.api.success') }) notify({ type: "success", message: t("common.api.success") });
onSuccess() onSuccess();
} } finally {
finally { setConversationDeleting(false);
setConversationDeleting(false) }
}
if (conversationId === currentConversationId) if (conversationId === currentConversationId) handleNewConversation();
handleNewConversation()
handleUpdateConversationList() handleUpdateConversationList();
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting]) },
[
isInstalledApp,
appId,
notify,
t,
handleUpdateConversationList,
handleNewConversation,
currentConversationId,
conversationDeleting,
]
);
const [conversationRenaming, setConversationRenaming] = useState(false) const [conversationRenaming, setConversationRenaming] = useState(false);
const handleRenameConversation = useCallback(async ( const handleRenameConversation = useCallback(
conversationId: string, async (
newName: string, conversationId: string,
{ newName: string,
onSuccess, { onSuccess }: Callback
}: Callback, ) => {
) => { if (conversationRenaming) return;
if (conversationRenaming)
return
if (!newName.trim()) { if (!newName.trim()) {
notify({ notify({
type: 'error', type: "error",
message: t('common.chat.conversationNameCanNotEmpty'), message: t("common.chat.conversationNameCanNotEmpty"),
}) });
return return;
} }
setConversationRenaming(true) setConversationRenaming(true);
try { try {
await renameConversation(conversationId, newName) await renameConversation(conversationId, newName);
notify({ notify({
type: 'success', type: "success",
message: t('common.actionMsg.modifiedSuccessfully'), message: t("common.actionMsg.modifiedSuccessfully"),
}) });
setOriginConversationList(produce((draft) => { setOriginConversationList(
const index = originConversationList.findIndex(item => item.id === conversationId) produce((draft) => {
const item = draft[index] const index = originConversationList.findIndex(
(item) => item.id === conversationId
);
const item = draft[index];
draft[index] = { draft[index] = {
...item, ...item,
name: newName, name: newName,
} };
})) })
onSuccess() );
} onSuccess();
finally { } finally {
setConversationRenaming(false) setConversationRenaming(false);
} }
}, [isInstalledApp, appId, notify, t, conversationRenaming, originConversationList]) },
[
isInstalledApp,
appId,
notify,
t,
conversationRenaming,
originConversationList,
]
);
const handleNewConversationCompleted = useCallback((newConversationId: string) => { const handleNewConversationCompleted = useCallback(
setNewConversationId(newConversationId) (newConversationId: string) => {
handleConversationIdInfoChange(newConversationId) setNewConversationId(newConversationId);
setShowNewConversationItemInList(false) handleConversationIdInfoChange(newConversationId);
mutateAppConversationData() setShowNewConversationItemInList(false);
}, [mutateAppConversationData, handleConversationIdInfoChange]) mutateAppConversationData();
},
[mutateAppConversationData, handleConversationIdInfoChange]
);
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => { const [isFeedbackModalVisible, setIsFeedbackModalVisible] = useState(false);
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, appId) const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
notify({ type: 'success', message: t('common.api.success') }) const [currentFeedback, setCurrentFeedback] = useState<Feedback | null>(null);
}, [isInstalledApp, appId, t, notify]) 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 { return {
appInfoError, appInfoError,
@ -427,7 +614,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
currentConversationItem, currentConversationItem,
handleConversationIdInfoChange, handleConversationIdInfoChange,
appData, appData,
appParams: appParams || {} as ChatConfig, appParams: appParams || ({} as ChatConfig),
appMeta, appMeta,
appPinnedConversationData, appPinnedConversationData,
appConversationData, appConversationData,
@ -458,5 +645,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
chatShouldReloadKey, chatShouldReloadKey,
handleFeedback, handleFeedback,
currentChatInstanceRef, currentChatInstanceRef,
} feedbackModal: (
} <FeedbackModal
open={isFeedbackModalVisible}
onOk={handleFeedbackSubmit}
onCancel={() => setIsFeedbackModalVisible(false)}
/>
),
};
};

View File

@ -1,33 +1,26 @@
import type { FC } from 'react' import type { FC } from "react";
import { import "antd/dist/reset.css"; // 引入 antd 的样式
useEffect, import { useEffect, useState } from "react";
useState, import { useAsyncEffect } from "ahooks";
} from 'react' import { useThemeContext } from "@/app/components/chat/embedded-chatbot/theme/theme-context";
import { useAsyncEffect } from 'ahooks' import { ChatWithHistoryContext, useChatWithHistoryContext } from "./context";
import { useThemeContext } from '@/app/components/chat/embedded-chatbot/theme/theme-context' import { useChatWithHistory } from "./hooks";
import { import Sidebar from "./sidebar";
ChatWithHistoryContext, import HeaderInMobile from "./header-in-mobile";
useChatWithHistoryContext, import ConfigPanel from "./config-panel";
} from './context' import ChatWrapper from "./chat-wrapper";
import { useChatWithHistory } from './hooks' import type { InstalledApp } from "@/models/explore";
import Sidebar from './sidebar' import Loading from "@/app/components/base/loading";
import HeaderInMobile from './header-in-mobile' import useBreakpoints, { MediaType } from "@/hooks/use-breakpoints";
import ConfigPanel from './config-panel' import { useRouter } from "next/navigation";
import ChatWrapper from './chat-wrapper' import AppUnavailable from "@/app/components/base/app-unavailable";
import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useRouter } from 'next/navigation'
import AppUnavailable from '@/app/components/base/app-unavailable'
type ChatWithHistoryProps = { type ChatWithHistoryProps = {
className?: string className?: string;
userStr?: string userStr?: string;
} };
const ChatWithHistory: FC<ChatWithHistoryProps> = ({
className, const ChatWithHistory: FC<ChatWithHistoryProps> = ({ className, userStr }) => {
userStr,
}) => {
const { const {
appInfoError, appInfoError,
appData, appData,
@ -38,82 +31,78 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
chatShouldReloadKey, chatShouldReloadKey,
isMobile, isMobile,
themeBuilder, themeBuilder,
} = useChatWithHistoryContext() } = useChatWithHistoryContext();
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length) const chatReady = !showConfigPanelBeforeChat || !!appPrevChatTree.length;
const customConfig = appData?.custom_config const customConfig = appData?.custom_config;
const site = appData?.site const site = appData?.site;
useEffect(() => { useEffect(() => {
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted) themeBuilder?.buildTheme(
site?.chat_color_theme,
site?.chat_color_theme_inverted
);
if (site) { if (site) {
if (customConfig) if (customConfig) document.title = `${site.title}`;
document.title = `${site.title}` else document.title = `${site.title} - Powered by Dify`;
else
document.title = `${site.title} - Powered by Dify`
} }
}, [site, customConfig, themeBuilder]) }, [site, customConfig, themeBuilder]);
if (appInfoLoading) { if (appInfoLoading) {
return ( return <Loading type="app" />;
<Loading type='app' />
)
} }
if (appInfoError) { if (appInfoError) {
return ( return <AppUnavailable />;
<AppUnavailable />
)
} }
return ( return (
<div className={`h-full flex bg-white ${className} ${isMobile && 'flex-col'}`}> <div
{ className={`h-full flex bg-white ${className} ${isMobile && "flex-col"}`}
!isMobile && ( >
<Sidebar userStr={userStr} /> {!isMobile && <Sidebar userStr={userStr} />}
) {isMobile && <HeaderInMobile />}
} <div
{ className={`grow overflow-hidden ${
isMobile && ( showConfigPanelBeforeChat &&
<HeaderInMobile /> !appPrevChatTree.length &&
) "flex items-center justify-center"
} }`}
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}> >
{ {showConfigPanelBeforeChat &&
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && ( !appChatListDataLoading &&
<div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}> !appPrevChatTree.length && (
<div
className={`flex w-full items-center justify-center h-full ${
isMobile && "px-4"
}`}
>
<ConfigPanel /> <ConfigPanel />
</div> </div>
) )}
} {appChatListDataLoading && chatReady && <Loading type="app" />}
{ {chatReady && !appChatListDataLoading && (
appChatListDataLoading && chatReady && ( <ChatWrapper key={chatShouldReloadKey} />
<Loading type='app' /> )}
)
}
{
chatReady && !appChatListDataLoading && (
<ChatWrapper key={chatShouldReloadKey} />
)
}
</div> </div>
</div> </div>
) );
} };
export type ChatWithHistoryWrapProps = { export type ChatWithHistoryWrapProps = {
installedAppInfo?: InstalledApp installedAppInfo?: InstalledApp;
className?: string className?: string;
userStr?: string userStr?: string;
} };
const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
installedAppInfo, installedAppInfo,
className, className,
userStr, userStr,
}) => { }) => {
const media = useBreakpoints() const media = useBreakpoints();
const isMobile = media === MediaType.mobile const isMobile = media === MediaType.mobile;
const themeBuilder = useThemeContext() const themeBuilder = useThemeContext();
const { const {
appInfoError, appInfoError,
@ -146,93 +135,99 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appId, appId,
handleFeedback, handleFeedback,
currentChatInstanceRef, currentChatInstanceRef,
} = useChatWithHistory(installedAppInfo) feedbackModal,
} = useChatWithHistory(installedAppInfo);
return ( return (
<ChatWithHistoryContext.Provider value={{ <ChatWithHistoryContext.Provider
appInfoError, value={{
appInfoLoading, appInfoError,
appData, appInfoLoading,
appParams, appData,
appMeta, appParams,
appChatListDataLoading, appMeta,
currentConversationId, appChatListDataLoading,
currentConversationItem, currentConversationId,
appPrevChatTree, currentConversationItem,
pinnedConversationList, appPrevChatTree,
conversationList, pinnedConversationList,
showConfigPanelBeforeChat, conversationList,
newConversationInputs, showConfigPanelBeforeChat,
newConversationInputsRef, newConversationInputs,
handleNewConversationInputsChange, newConversationInputsRef,
inputsForms, handleNewConversationInputsChange,
handleNewConversation, inputsForms,
handleStartChat, handleNewConversation,
handleChangeConversation, handleStartChat,
handlePinConversation, handleChangeConversation,
handleUnpinConversation, handlePinConversation,
handleDeleteConversation, handleUnpinConversation,
conversationRenaming, handleDeleteConversation,
handleRenameConversation, conversationRenaming,
handleNewConversationCompleted, handleRenameConversation,
chatShouldReloadKey, handleNewConversationCompleted,
isMobile, chatShouldReloadKey,
isInstalledApp, isMobile,
appId, isInstalledApp,
handleFeedback, appId,
currentChatInstanceRef, handleFeedback,
themeBuilder, currentChatInstanceRef,
}}> themeBuilder,
}}
>
<ChatWithHistory className={className} userStr={userStr} /> <ChatWithHistory className={className} userStr={userStr} />
{feedbackModal} {/* 确保 feedbackModal 正确渲染 */}
</ChatWithHistoryContext.Provider> </ChatWithHistoryContext.Provider>
) );
} };
const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({ const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({
installedAppInfo, installedAppInfo,
className, className,
}) => { }) => {
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false);
const [appUnavailable, setAppUnavailable] = useState<boolean>(false) const [appUnavailable, setAppUnavailable] = useState<boolean>(false);
const [resCode, setResCode] = useState<number>(200) const [resCode, setResCode] = useState<number>(200);
const router = useRouter() const router = useRouter();
const [userName, setUserName] = useState('') const [userName, setUserName] = useState("");
const [nickName, setNickName] = useState('') const [nickName, setNickName] = useState("");
useAsyncEffect(async () => { useAsyncEffect(async () => {
if (!initialized) { if (!initialized) {
if (!installedAppInfo) { if (!installedAppInfo) {
const accessToken = localStorage.getItem('token') const accessToken = localStorage.getItem("token");
if (!accessToken) { if (!accessToken) {
router.replace('/login') router.replace("/login");
} else { } else {
fetch(`${process.env.NEXT_PUBLIC_BASE_API_URL}/getInfo`, { fetch(`${process.env.NEXT_PUBLIC_BASE_API_URL}/getInfo`, {
method: 'GET', method: "GET",
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}, },
}).then(res => res.json()).then(data => {
if (data.code !== 200) {
localStorage.removeItem('token')
router.replace('/login')
} else {
setUserName(data.user.userName)
setNickName(data.user.nickName)
}
}).catch(() => {
setResCode(500)
setAppUnavailable(true)
}) })
.then((res) => res.json())
.then((data) => {
if (data.code !== 200) {
localStorage.removeItem("token");
router.replace("/login");
} else {
setUserName(data.user.userName);
setNickName(data.user.nickName);
}
})
.catch(() => {
setResCode(500);
setAppUnavailable(true);
});
} }
} }
setInitialized(true) setInitialized(true);
} }
}, []) }, []);
if (!initialized) if (!initialized) return null;
return null
if (appUnavailable) if (appUnavailable) return <AppUnavailable code={resCode} />;
return <AppUnavailable code={resCode} />
return ( return (
<ChatWithHistoryWrap <ChatWithHistoryWrap
@ -240,7 +235,7 @@ const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({
className={className} className={className}
userStr={`${nickName}[${userName}]`} userStr={`${nickName}[${userName}]`}
/> />
) );
} };
export default ChatWithHistoryWrapWithCheckToken export default ChatWithHistoryWrapWithCheckToken;

View File

@ -1,34 +1,32 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
devIndicators: false, // show dev tools reactStrictMode: true,
productionBrowserSourceMaps: false, // enable browser source map generation during the production build // 压缩优化Next.js 13+默认启用SWC无需手动配置
// Configure pageExtensions to include md and mdx compress: true,
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
experimental: { // 图片优化
// appDir: true, images: {
formats: ["image/avif", "image/webp"],
domains: ["cdn.yourdomain.com"],
}, },
// fix all before production. Now it slow the develop speed.
eslint: { // 修正后的开发指示器配置
// Warning: This allows production builds to successfully complete even if devIndicators: {
// your project has ESLint errors. position: "bottom-right", // 新版统一用position
ignoreDuringBuilds: true,
},
typescript: {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
ignoreBuildErrors: true,
}, },
// 页面扩展名
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
// 路由重写
async rewrites() { async rewrites() {
return [ return [
{ {
source: '/files/:path*', source: "/dev-api/:path*",
destination: `${process.env.NEXT_PUBLIC_FILES_URL}/:path*`, destination: "http://192.168.6.9:8085/:path*",
}, },
{ ];
source: '/dev-api/:path*',
destination: 'http://192.168.6.9:8085/:path*',
},
]
}, },
} };
module.exports = nextConfig module.exports = nextConfig;

View File

@ -44,9 +44,9 @@ export const fetchConversations = async (limit = 100, last_id = null) => {
return get('conversations', { params: { limit, last_id } }) return get('conversations', { params: { limit, last_id } })
} }
export const fetchChatList = async (conversationId: string, limit = 20, last_id = null) => { export const fetchChatList = async (conversationId: string, limit = 20) => {
return get('messages', { return get('messages', {
params: { conversation_id: conversationId, limit, last_id } params: { conversation_id: conversationId, limit}
}) })
} }