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) => {
const username = request.cookies.get('username')?.value || 'no-user'
const sessionId = request.cookies.get('session_id')?.value || v4()
const user = `user_${username}:${sessionId}`
const user = `${username}`
return {
sessionId,
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 {
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,
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 { 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,
@ -32,50 +22,66 @@ import {
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'
} 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[] = []
const newChatList: ChatItem[] = [];
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({
id: `question-${item.id}`,
content: item.query,
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,
})
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)
// 获取本地文件名
answerFiles[0].filename = questionFiles[0]?.filename
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),
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 }))),
message_files: getProcessedFilesFromResponse(
answerFiles.map((item: any) => ({ ...item, related_id: item.id }))
),
parentMessageId: `question-${item.id}`,
})
})
return newChatList
});
});
return newChatList;
}
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]);
const {
data: appInfo,
isLoading: appInfoLoading,
error: appInfoError,
} = useSWR(installedAppInfo ? null : "appInfo", fetchAppInfo);
useAppFavicon({
enable: !installedAppInfo,
@ -83,11 +89,11 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
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!
const { id, app } = installedAppInfo!;
return {
app_id: id,
site: {
@ -97,326 +103,507 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
icon_background: app.icon_background,
icon_url: app.icon_url,
prompt_public: false,
copyright: '',
copyright: "",
show_workflow_steps: true,
use_icon_as_answer_icon: app.use_icon_as_answer_icon,
},
plan: 'basic',
} as AppData
plan: "basic",
} as AppData;
}
return appInfo
}, [isInstalledApp, installedAppInfo, appInfo])
const appId = useMemo(() => appData?.app_id, [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])
changeLanguage(appData.site?.default_language);
}, [appData]);
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, {
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 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 ''
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))
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],
)
() =>
currentConversationId && appChatListData?.data.length
? buildChatItemTree(getFormattedChatList(appChatListData.data))
: [],
[appChatListData, currentConversationId]
);
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
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)
}, [])
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',
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.number) {
return {
...item.number,
type: "number",
};
}
}
if (item.select) {
return {
...item.select,
type: 'select',
if (item.select) {
return {
...item.select,
type: "select",
};
}
}
if (item['file-list']) {
return {
...item['file-list'],
type: 'file-list',
if (item["file-list"]) {
return {
...item["file-list"],
type: "file-list",
};
}
}
if (item.file) {
return {
...item.file,
type: 'file',
if (item.file) {
return {
...item.file,
type: "file",
};
}
}
return {
...item['text-input'],
type: 'text-input',
}
})
}, [appParams])
return {
...item["text-input"],
type: "text-input",
};
});
}, [appParams]);
useEffect(() => {
const conversationInputs: Record<string, any> = {}
const conversationInputs: Record<string, any> = {};
inputsForms.forEach((item: any) => {
conversationInputs[item.variable] = item.default || null
})
handleNewConversationInputsChange(conversationInputs)
}, [handleNewConversationInputsChange, inputsForms])
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[]>([])
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])
setOriginConversationList(appConversationData?.data);
}, [appConversationData, appConversationDataLoading]);
const conversationList = useMemo(() => {
const data = originConversationList.slice()
const data = originConversationList.slice();
if (showNewConversationItemInList && data[0]?.id !== '') {
if (showNewConversationItemInList && data[0]?.id !== "") {
data.unshift({
id: '',
name: t('share.chat.newChatDefaultName'),
id: "",
name: t("share.chat.newChatDefaultName"),
inputs: {},
introduction: '',
})
introduction: "",
});
}
return data
}, [originConversationList, showNewConversationItemInList, t])
return data;
}, [originConversationList, showNewConversationItemInList, t]);
useEffect(() => {
if (newConversation) {
setOriginConversationList(produce((draft) => {
const index = draft.findIndex(item => item.id === newConversation.id)
setOriginConversationList(
produce((draft) => {
const index = draft.findIndex(
(item) => item.id === newConversation.id
);
if (index > -1)
draft[index] = newConversation
else
draft.unshift(newConversation)
}))
if (index > -1) draft[index] = newConversation;
else draft.unshift(newConversation);
})
);
}
}, [newConversation])
}, [newConversation]);
const currentConversationItem = useMemo(() => {
let conversationItem = conversationList.find(item => item.id === currentConversationId)
let conversationItem = conversationList.find(
(item) => item.id === currentConversationId
);
if (!conversationItem && pinnedConversationList.length)
conversationItem = pinnedConversationList.find(item => item.id === currentConversationId)
conversationItem = pinnedConversationList.find(
(item) => item.id === currentConversationId
);
return conversationItem
}, [conversationList, currentConversationId, pinnedConversationList])
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
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 (fileIsUploading) return;
if (!newConversationInputsRef.current[variable] && !silent)
hasEmptyInput = label as string
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 (
(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 (hasEmptyInput) {
notify({
type: "error",
message: t("appDebug.errorMessage.valueOfVarRequired", {
key: hasEmptyInput,
}),
});
return false;
}
if (fileIsUploading) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
return
}
if (fileIsUploading) {
notify({
type: "info",
message: t("appDebug.errorMessage.waitForFileUpload"),
});
return;
}
return true
}, [inputsForms, notify, t])
return true;
},
[inputsForms, notify, t]
);
const handleStartChat = useCallback(() => {
if (checkInputsRequired()) {
setShowConfigPanelBeforeChat(false)
setShowNewConversationItemInList(true)
setShowConfigPanelBeforeChat(false);
setShowNewConversationItemInList(true);
}
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired])
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
const handleChangeConversation = useCallback((conversationId: string) => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
handleConversationIdInfoChange(conversationId)
}, [
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])
if (conversationId === "" && !checkInputsRequired(true))
setShowConfigPanelBeforeChat(true);
else setShowConfigPanelBeforeChat(false);
},
[
handleConversationIdInfoChange,
setShowConfigPanelBeforeChat,
checkInputsRequired,
]
);
const handleNewConversation = useCallback(() => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
currentChatInstanceRef.current.handleStop();
setNewConversationId("");
if (showNewConversationItemInList) {
handleChangeConversation('')
handleChangeConversation("");
} else if (currentConversationId) {
handleConversationIdInfoChange("");
setShowConfigPanelBeforeChat(true);
setShowNewConversationItemInList(true);
handleNewConversationInputsChange({});
}
else if (currentConversationId) {
handleConversationIdInfoChange('')
setShowConfigPanelBeforeChat(true)
setShowNewConversationItemInList(true)
handleNewConversationInputsChange({})
}
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
}, [
handleChangeConversation,
currentConversationId,
handleConversationIdInfoChange,
setShowConfigPanelBeforeChat,
setShowNewConversationItemInList,
showNewConversationItemInList,
handleNewConversationInputsChange,
]);
const handleUpdateConversationList = useCallback(() => {
mutateAppConversationData()
mutateAppPinnedConversationData()
}, [mutateAppConversationData, mutateAppPinnedConversationData])
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 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 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
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)
}
try {
setConversationDeleting(true);
await delConversation(conversationId);
notify({ type: "success", message: t("common.api.success") });
onSuccess();
} finally {
setConversationDeleting(false);
}
if (conversationId === currentConversationId)
handleNewConversation()
if (conversationId === currentConversationId) handleNewConversation();
handleUpdateConversationList()
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting])
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
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
}
if (!newName.trim()) {
notify({
type: "error",
message: t("common.chat.conversationNameCanNotEmpty"),
});
return;
}
setConversationRenaming(true)
try {
await renameConversation(conversationId, newName)
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]
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])
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 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])
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,
@ -427,7 +614,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
currentConversationItem,
handleConversationIdInfoChange,
appData,
appParams: appParams || {} as ChatConfig,
appParams: appParams || ({} as ChatConfig),
appMeta,
appPinnedConversationData,
appConversationData,
@ -458,5 +645,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
chatShouldReloadKey,
handleFeedback,
currentChatInstanceRef,
}
}
feedbackModal: (
<FeedbackModal
open={isFeedbackModalVisible}
onOk={handleFeedbackSubmit}
onCancel={() => setIsFeedbackModalVisible(false)}
/>
),
};
};

View File

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

View File

@ -1,34 +1,32 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
devIndicators: false, // show dev tools
productionBrowserSourceMaps: false, // enable browser source map generation during the production build
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
experimental: {
// appDir: true,
reactStrictMode: true,
// 压缩优化Next.js 13+默认启用SWC无需手动配置
compress: 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
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
typescript: {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
ignoreBuildErrors: true,
// 修正后的开发指示器配置
devIndicators: {
position: "bottom-right", // 新版统一用position
},
// 页面扩展名
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
// 路由重写
async rewrites() {
return [
{
source: '/files/:path*',
destination: `${process.env.NEXT_PUBLIC_FILES_URL}/:path*`,
source: "/dev-api/: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 } })
}
export const fetchChatList = async (conversationId: string, limit = 20, last_id = null) => {
export const fetchChatList = async (conversationId: string, limit = 20) => {
return get('messages', {
params: { conversation_id: conversationId, limit, last_id }
params: { conversation_id: conversationId, limit}
})
}