V4.8.14 dev (#3234)
* feat: rewrite chat context (#3176) * feat: add app auto execute (#3115) * feat: add app auto execute * auto exec configtion * chatting animation * change icon * fix * fix * fix link * feat: add chat context to all chatbox * perf: loading ui --------- Co-authored-by: heheer <heheer@sealos.io> * app auto exec (#3179) * add chat records loaded state (#3184) * perf: chat store reset storage (#3186) * perf: chat store reset storage * perf: auto exec code * chore: workflow ui (#3175) * chore: workflow ui * fix * change icon color config * change popover to mymenu * 4.8.14 test (#3189) * update doc * fix: token check * perf: icon button * update doc * feat: share page support configuration Whether to allow the original view (#3194) * update doc * perf: fix index (#3206) * perf: i18n * perf: Add service entry (#3226) * 4.8.14 test (#3228) * fix: ai log * fix: text splitter * fix: reference unselect & user form description & simple to advance (#3229) * fix: reference unselect & user form description & simple to advance * change abort position * perf * perf: code (#3232) * perf: code * update doc * fix: create btn permission (#3233) * update doc * fix: refresh chatbox listener * perf: check invalid reference * perf: check invalid reference * update doc * fix: ui props --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -109,11 +109,10 @@ function responseError(err: any) {
|
||||
}
|
||||
// 有报错响应
|
||||
if (err?.code in TOKEN_ERROR_CODE) {
|
||||
clearToken();
|
||||
|
||||
if (
|
||||
!(window.location.pathname === '/chat/share' || window.location.pathname === '/chat/team')
|
||||
) {
|
||||
clearToken();
|
||||
window.location.replace(
|
||||
getWebReqUrl(`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`)
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UploadImgProps } from '@fastgpt/global/common/file/api';
|
||||
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
|
||||
import { preUploadImgProps } from '@fastgpt/global/common/file/api';
|
||||
import { compressBase64Img, type CompressImgProps } from '@fastgpt/web/common/file/img';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import type { UploadChatFileProps, UploadDatasetFileProps } from '@/pages/api/common/file/upload';
|
||||
|
||||
/**
|
||||
* upload file to mongo gridfs
|
||||
@@ -11,13 +11,13 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
export const uploadFile2DB = ({
|
||||
file,
|
||||
bucketName,
|
||||
outLinkAuthData,
|
||||
data,
|
||||
metadata = {},
|
||||
percentListen
|
||||
}: {
|
||||
file: File;
|
||||
bucketName: `${BucketNameEnum}`;
|
||||
outLinkAuthData?: OutLinkChatAuthProps;
|
||||
data: UploadChatFileProps | UploadDatasetFileProps;
|
||||
metadata?: Record<string, any>;
|
||||
percentListen?: (percent: number) => void;
|
||||
}) => {
|
||||
@@ -25,13 +25,7 @@ export const uploadFile2DB = ({
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
form.append('bucketName', bucketName);
|
||||
form.append('file', file, encodeURIComponent(file.name));
|
||||
|
||||
if (outLinkAuthData) {
|
||||
for (const key in outLinkAuthData) {
|
||||
// @ts-ignore
|
||||
outLinkAuthData[key] && form.append(key, outLinkAuthData[key]);
|
||||
}
|
||||
}
|
||||
form.append('data', JSON.stringify(data));
|
||||
|
||||
return postUploadFiles(form, (e) => {
|
||||
if (!e.total) return;
|
||||
|
||||
@@ -2,18 +2,21 @@ import { I18nNsType } from '@fastgpt/web/types/i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
export enum LangEnum {
|
||||
'zh' = 'zh',
|
||||
'zh_CN' = 'zh-CN',
|
||||
'zh_TW' = 'zh-TW',
|
||||
'en' = 'en'
|
||||
}
|
||||
export const langMap = {
|
||||
[LangEnum.en]: {
|
||||
label: 'English(US)',
|
||||
icon: 'common/language/en',
|
||||
avatar: 'common/language/America'
|
||||
},
|
||||
[LangEnum.zh]: {
|
||||
[LangEnum.zh_CN]: {
|
||||
label: '简体中文',
|
||||
icon: 'common/language/zh',
|
||||
avatar: 'common/language/China'
|
||||
},
|
||||
[LangEnum.zh_TW]: {
|
||||
label: '中文(台湾)',
|
||||
avatar: 'common/language/China'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -532,12 +532,14 @@ export const workflowSystemVariables: EditorVariablePickerType[] = [
|
||||
{
|
||||
key: 'chatId',
|
||||
label: i18nT('common:core.module.http.ChatId'),
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'responseChatItemId',
|
||||
label: i18nT('common:core.module.http.ResponseChatItemId'),
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'histories',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
|
||||
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import {
|
||||
delClearChatHistories,
|
||||
@@ -9,20 +9,25 @@ import {
|
||||
getChatHistories
|
||||
} from '../api';
|
||||
import { ChatHistoryItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ClearHistoriesProps, DelHistoryProps, UpdateHistoryProps } from '@/global/core/chat/api';
|
||||
import { UpdateHistoryProps } from '@/global/core/chat/api';
|
||||
import { BoxProps, useDisclosure } from '@chakra-ui/react';
|
||||
import { useChatStore } from './storeChat';
|
||||
import { useChatStore } from './useChatStore';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
|
||||
type UpdateHistoryParams = {
|
||||
chatId: UpdateHistoryProps['chatId'];
|
||||
customTitle?: UpdateHistoryProps['customTitle'];
|
||||
top?: UpdateHistoryProps['top'];
|
||||
};
|
||||
|
||||
type ChatContextValueType = {
|
||||
params: Record<string, string | number | boolean>;
|
||||
};
|
||||
type ChatContextType = {
|
||||
chatId: string;
|
||||
onUpdateHistory: (data: UpdateHistoryProps) => void;
|
||||
onDelHistory: (data: DelHistoryProps) => Promise<undefined>;
|
||||
onClearHistories: (data: ClearHistoriesProps) => Promise<undefined>;
|
||||
onUpdateHistory: (data: UpdateHistoryParams) => void;
|
||||
onDelHistory: (chatId: string) => Promise<undefined>;
|
||||
onClearHistories: () => Promise<undefined>;
|
||||
isOpenSlider: boolean;
|
||||
onCloseSlider: () => void;
|
||||
onOpenSlider: () => void;
|
||||
@@ -46,8 +51,11 @@ type ChatContextType = {
|
||||
onUpdateHistoryTitle: ({ chatId, newTitle }: { chatId: string; newTitle: string }) => void;
|
||||
};
|
||||
|
||||
/*
|
||||
主要存放历史记录数据。
|
||||
同时还存放外部链接鉴权信息,不会在 chatTest 下使用
|
||||
*/
|
||||
export const ChatContext = createContext<ChatContextType>({
|
||||
chatId: '',
|
||||
// forbidLoadChat: undefined,
|
||||
histories: [],
|
||||
onUpdateHistoryTitle: function (): void {
|
||||
@@ -62,13 +70,13 @@ export const ChatContext = createContext<ChatContextType>({
|
||||
setHistories: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onUpdateHistory: function (data: UpdateHistoryProps): void {
|
||||
onUpdateHistory: function (data: UpdateHistoryParams): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onDelHistory: function (data: DelHistoryProps): Promise<undefined> {
|
||||
onDelHistory: function (data: string): Promise<undefined> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onClearHistories: function (data: ClearHistoriesProps): Promise<undefined> {
|
||||
onClearHistories: function (): Promise<undefined> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isOpenSlider: false,
|
||||
@@ -93,9 +101,9 @@ const ChatContextProvider = ({
|
||||
params
|
||||
}: ChatContextValueType & { children: ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { chatId = '' } = router.query as { chatId: string };
|
||||
|
||||
const forbidLoadChat = useRef(false);
|
||||
const { chatId, appId, setChatId, outLinkAuthData } = useChatStore();
|
||||
|
||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||
|
||||
@@ -108,38 +116,26 @@ const ChatContextProvider = ({
|
||||
} = useScrollPagination(getChatHistories, {
|
||||
pageSize: 20,
|
||||
params,
|
||||
refreshDeps: [params]
|
||||
refreshDeps: [params],
|
||||
showErrorToast: false
|
||||
});
|
||||
|
||||
const { setLastChatId } = useChatStore();
|
||||
const onChangeChatId = useCallback(
|
||||
(changeChatId = getNanoid(), forbid = false) => {
|
||||
(changeChatId = getNanoid(24), forbid = false) => {
|
||||
if (chatId !== changeChatId) {
|
||||
forbidLoadChat.current = forbid;
|
||||
setLastChatId(changeChatId);
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
chatId: changeChatId || ''
|
||||
}
|
||||
});
|
||||
setChatId(changeChatId);
|
||||
}
|
||||
onCloseSlider();
|
||||
},
|
||||
[chatId, onCloseSlider, router, setLastChatId]
|
||||
[chatId, onCloseSlider, setChatId]
|
||||
);
|
||||
|
||||
// Refresh lastChatId
|
||||
useEffect(() => {
|
||||
setLastChatId(chatId);
|
||||
}, [chatId, setLastChatId]);
|
||||
|
||||
const onChangeAppId = useCallback(
|
||||
(appId: string) => {
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
chatId: '',
|
||||
appId
|
||||
}
|
||||
});
|
||||
@@ -148,40 +144,63 @@ const ChatContextProvider = ({
|
||||
[onCloseSlider, router]
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateHistory, loading: isUpdatingHistory } = useRequest2(putChatHistory, {
|
||||
onSuccess(data, params) {
|
||||
const { chatId, top, customTitle } = params[0];
|
||||
const { runAsync: onUpdateHistory } = useRequest2(
|
||||
(data: UpdateHistoryParams) =>
|
||||
putChatHistory({
|
||||
appId,
|
||||
...data,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
onBefore(params) {
|
||||
const { chatId, top, customTitle } = params[0];
|
||||
|
||||
setHistories((histories) => {
|
||||
const updatedHistories = histories.map((history) => {
|
||||
if (history.chatId === chatId) {
|
||||
return {
|
||||
...history,
|
||||
customTitle: customTitle || history.customTitle,
|
||||
top: top !== undefined ? top : history.top
|
||||
};
|
||||
}
|
||||
return history;
|
||||
setHistories((histories) => {
|
||||
const updatedHistories = histories.map((history) => {
|
||||
if (history.chatId === chatId) {
|
||||
return {
|
||||
...history,
|
||||
customTitle: customTitle || history.customTitle,
|
||||
top: top !== undefined ? top : history.top
|
||||
};
|
||||
}
|
||||
return history;
|
||||
});
|
||||
|
||||
return top !== undefined
|
||||
? updatedHistories.sort((a, b) => (b.top ? 1 : 0) - (a.top ? 1 : 0))
|
||||
: updatedHistories;
|
||||
});
|
||||
|
||||
return top !== undefined
|
||||
? updatedHistories.sort((a, b) => (b.top ? 1 : 0) - (a.top ? 1 : 0))
|
||||
: updatedHistories;
|
||||
});
|
||||
},
|
||||
errorToast: undefined
|
||||
});
|
||||
|
||||
const { runAsync: onDelHistory, loading: isDeletingHistory } = useRequest2(delChatHistoryById, {
|
||||
onSuccess(data, params) {
|
||||
const { chatId } = params[0];
|
||||
setHistories((old) => old.filter((i) => i.chatId !== chatId));
|
||||
},
|
||||
refreshDeps: [outLinkAuthData, appId],
|
||||
errorToast: undefined
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const { runAsync: onDelHistory, loading: isDeletingHistory } = useRequest2(
|
||||
(chatId: string) =>
|
||||
delChatHistoryById({
|
||||
appId: appId,
|
||||
chatId,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
onSuccess(data, params) {
|
||||
const chatId = params[0];
|
||||
setHistories((old) => old.filter((i) => i.chatId !== chatId));
|
||||
},
|
||||
refreshDeps: [outLinkAuthData, appId]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onClearHistories, loading: isClearingHistory } = useRequest2(
|
||||
delClearChatHistories,
|
||||
() =>
|
||||
delClearChatHistories({
|
||||
appId: appId,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
refreshDeps: [outLinkAuthData, appId],
|
||||
onSuccess() {
|
||||
setHistories([]);
|
||||
},
|
||||
@@ -206,27 +225,43 @@ const ChatContextProvider = ({
|
||||
[histories, loadHistories, setHistories]
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
isUpdatingHistory || isDeletingHistory || isClearingHistory || isPaginationLoading;
|
||||
const isLoading = isDeletingHistory || isClearingHistory || isPaginationLoading;
|
||||
|
||||
const contextValue = {
|
||||
chatId,
|
||||
onUpdateHistory,
|
||||
onDelHistory,
|
||||
onClearHistories,
|
||||
isOpenSlider,
|
||||
onCloseSlider,
|
||||
onOpenSlider,
|
||||
forbidLoadChat,
|
||||
onChangeChatId,
|
||||
onChangeAppId,
|
||||
isLoading,
|
||||
setHistories,
|
||||
ScrollData,
|
||||
loadHistories,
|
||||
histories,
|
||||
onUpdateHistoryTitle
|
||||
};
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
onUpdateHistory,
|
||||
onDelHistory,
|
||||
onClearHistories,
|
||||
isOpenSlider,
|
||||
onCloseSlider,
|
||||
onOpenSlider,
|
||||
forbidLoadChat,
|
||||
onChangeChatId,
|
||||
onChangeAppId,
|
||||
isLoading,
|
||||
setHistories,
|
||||
ScrollData,
|
||||
loadHistories,
|
||||
histories,
|
||||
onUpdateHistoryTitle
|
||||
}),
|
||||
[
|
||||
ScrollData,
|
||||
histories,
|
||||
isLoading,
|
||||
isOpenSlider,
|
||||
loadHistories,
|
||||
onChangeAppId,
|
||||
onChangeChatId,
|
||||
onClearHistories,
|
||||
onCloseSlider,
|
||||
onDelHistory,
|
||||
onOpenSlider,
|
||||
onUpdateHistory,
|
||||
onUpdateHistoryTitle,
|
||||
setHistories
|
||||
]
|
||||
);
|
||||
return <ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider>;
|
||||
};
|
||||
|
||||
|
||||
125
projects/app/src/web/core/chat/context/chatItemContext.tsx
Normal file
125
projects/app/src/web/core/chat/context/chatItemContext.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ChatBoxInputFormType } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
|
||||
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { ComponentRef as ChatComponentRef } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import { useForm, UseFormReturn } from 'react-hook-form';
|
||||
import { defaultChatData } from '@/global/core/chat/constants';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
|
||||
type ChatBoxDataType = {
|
||||
userAvatar?: string;
|
||||
app: {
|
||||
chatConfig?: AppChatConfigType;
|
||||
name: string;
|
||||
avatar: string;
|
||||
type: `${AppTypeEnum}`;
|
||||
pluginInputs: FlowNodeInputItemType[];
|
||||
};
|
||||
};
|
||||
|
||||
type ChatItemContextType = {
|
||||
ChatBoxRef: React.RefObject<ChatComponentRef> | null;
|
||||
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
|
||||
pluginRunTab: PluginRunBoxTabEnum;
|
||||
setPluginRunTab: React.Dispatch<React.SetStateAction<PluginRunBoxTabEnum>>;
|
||||
resetVariables: (props?: { variables?: Record<string, any> }) => void;
|
||||
clearChatRecords: () => void;
|
||||
chatBoxData: ChatBoxDataType;
|
||||
setChatBoxData: React.Dispatch<React.SetStateAction<ChatBoxDataType>>;
|
||||
isPlugin: boolean;
|
||||
};
|
||||
|
||||
export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
ChatBoxRef: null,
|
||||
// @ts-ignore
|
||||
variablesForm: undefined,
|
||||
pluginRunTab: PluginRunBoxTabEnum.input,
|
||||
setPluginRunTab: function (value: React.SetStateAction<PluginRunBoxTabEnum>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
resetVariables: function (props?: { variables?: Record<string, any> }): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
clearChatRecords: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Chat 对象的上下文
|
||||
*/
|
||||
const ChatItemContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const ChatBoxRef = useRef<ChatComponentRef>(null);
|
||||
const variablesForm = useForm<ChatBoxInputFormType>();
|
||||
|
||||
const [chatBoxData, setChatBoxData] = useState<ChatBoxDataType>(defaultChatData);
|
||||
|
||||
const isPlugin = chatBoxData.app.type === AppTypeEnum.plugin;
|
||||
|
||||
// plugin
|
||||
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);
|
||||
|
||||
const resetVariables = useCallback(
|
||||
(props?: { variables?: Record<string, any> }) => {
|
||||
const { variables = {} } = props || {};
|
||||
|
||||
// Reset to empty input
|
||||
const data = variablesForm.getValues();
|
||||
|
||||
// Reset the old variables to empty
|
||||
const resetVariables: Record<string, any> = {};
|
||||
for (const key in data.variables) {
|
||||
resetVariables[key] = (() => {
|
||||
if (Array.isArray(data.variables[key])) {
|
||||
return [];
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
}
|
||||
|
||||
variablesForm.setValue('variables', {
|
||||
...resetVariables,
|
||||
...variables
|
||||
});
|
||||
},
|
||||
[variablesForm]
|
||||
);
|
||||
|
||||
const clearChatRecords = useCallback(() => {
|
||||
const data = variablesForm.getValues();
|
||||
for (const key in data.variables) {
|
||||
variablesForm.setValue(`variables.${key}`, '');
|
||||
}
|
||||
|
||||
ChatBoxRef.current?.restartChat?.();
|
||||
}, [variablesForm]);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return {
|
||||
chatBoxData,
|
||||
setChatBoxData,
|
||||
isPlugin,
|
||||
ChatBoxRef,
|
||||
variablesForm,
|
||||
pluginRunTab,
|
||||
setPluginRunTab,
|
||||
resetVariables,
|
||||
clearChatRecords
|
||||
};
|
||||
}, [
|
||||
chatBoxData,
|
||||
setChatBoxData,
|
||||
clearChatRecords,
|
||||
isPlugin,
|
||||
pluginRunTab,
|
||||
resetVariables,
|
||||
variablesForm
|
||||
]);
|
||||
|
||||
return <ChatItemContext.Provider value={contextValue}>{children}</ChatItemContext.Provider>;
|
||||
};
|
||||
|
||||
export default ChatItemContextProvider;
|
||||
121
projects/app/src/web/core/chat/context/chatRecordContext.tsx
Normal file
121
projects/app/src/web/core/chat/context/chatRecordContext.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { getPaginationRecordsBody } from '@/pages/api/core/chat/getPaginationRecords';
|
||||
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import React, { ReactNode, useMemo, useState } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import { ChatItemContext } from './chatItemContext';
|
||||
import { getChatRecords } from '../api';
|
||||
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
type ChatRecordContextType = {
|
||||
chatRecords: ChatSiteItemType[];
|
||||
setChatRecords: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
|
||||
isChatRecordsLoaded: boolean;
|
||||
setIsChatRecordsLoaded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
totalRecordsCount: number;
|
||||
ScrollData: ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
ScrollContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
} & BoxProps) => React.JSX.Element;
|
||||
};
|
||||
|
||||
export const ChatRecordContext = createContext<ChatRecordContextType>({
|
||||
chatRecords: [],
|
||||
setChatRecords: function (value: React.SetStateAction<ChatSiteItemType[]>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isChatRecordsLoaded: false,
|
||||
setIsChatRecordsLoaded: function (value: React.SetStateAction<boolean>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
totalRecordsCount: 0,
|
||||
ScrollData: function ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
ScrollContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
} & BoxProps): React.JSX.Element {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
具体对话记录的上下文
|
||||
*/
|
||||
const ChatRecordContextProvider = ({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: ReactNode;
|
||||
params: Record<string, any>;
|
||||
}) => {
|
||||
const ChatBoxRef = useContextSelector(ChatItemContext, (v) => v.ChatBoxRef);
|
||||
const [isChatRecordsLoaded, setIsChatRecordsLoaded] = useState(false);
|
||||
|
||||
const {
|
||||
data: chatRecords,
|
||||
ScrollData,
|
||||
setData: setChatRecords,
|
||||
total: totalRecordsCount
|
||||
} = useScrollPagination(
|
||||
async (data: getPaginationRecordsBody): Promise<PaginationResponse<ChatSiteItemType>> => {
|
||||
const res = await getChatRecords(data);
|
||||
|
||||
// First load scroll to bottom
|
||||
if (data.offset === 0) {
|
||||
function scrollToBottom() {
|
||||
requestAnimationFrame(
|
||||
ChatBoxRef?.current ? () => ChatBoxRef?.current?.scrollToBottom?.() : scrollToBottom
|
||||
);
|
||||
}
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
setIsChatRecordsLoaded(true);
|
||||
|
||||
return {
|
||||
...res,
|
||||
list: res.list.map((item) => ({
|
||||
...item,
|
||||
dataId: item.dataId || getNanoid(),
|
||||
status: ChatStatusEnum.finish
|
||||
}))
|
||||
};
|
||||
},
|
||||
{
|
||||
pageSize: 10,
|
||||
refreshDeps: [params],
|
||||
params,
|
||||
scrollLoadType: 'top',
|
||||
showErrorToast: false
|
||||
}
|
||||
);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return {
|
||||
chatRecords,
|
||||
setChatRecords,
|
||||
totalRecordsCount,
|
||||
ScrollData,
|
||||
isChatRecordsLoaded,
|
||||
setIsChatRecordsLoaded
|
||||
};
|
||||
}, [
|
||||
ScrollData,
|
||||
chatRecords,
|
||||
setChatRecords,
|
||||
totalRecordsCount,
|
||||
isChatRecordsLoaded,
|
||||
setIsChatRecordsLoaded
|
||||
]);
|
||||
return <ChatRecordContext.Provider value={contextValue}>{children}</ChatRecordContext.Provider>;
|
||||
};
|
||||
|
||||
export default ChatRecordContextProvider;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import type { DeleteChatItemProps } from '@/global/core/chat/api';
|
||||
|
||||
type State = {
|
||||
lastChatAppId: string;
|
||||
setLastChatAppId: (id: string) => void;
|
||||
lastChatId: string;
|
||||
setLastChatId: (id: string) => void;
|
||||
};
|
||||
|
||||
export const useChatStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
lastChatAppId: '',
|
||||
setLastChatAppId(id: string) {
|
||||
set((state) => {
|
||||
state.lastChatAppId = id;
|
||||
});
|
||||
},
|
||||
lastChatId: '',
|
||||
setLastChatId(id: string) {
|
||||
set((state) => {
|
||||
state.lastChatId = id;
|
||||
});
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'chatStore',
|
||||
partialize: (state) => ({
|
||||
lastChatAppId: state.lastChatAppId,
|
||||
lastChatId: state.lastChatId
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
129
projects/app/src/web/core/chat/context/useChatStore.ts
Normal file
129
projects/app/src/web/core/chat/context/useChatStore.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
type State = {
|
||||
source?: `${ChatSourceEnum}`;
|
||||
setSource: (e: `${ChatSourceEnum}`) => any;
|
||||
|
||||
appId: string;
|
||||
setAppId: (e: string) => any;
|
||||
lastChatAppId: string;
|
||||
setLastChatAppId: (e: string) => any;
|
||||
|
||||
lastChatId: string;
|
||||
chatId: string;
|
||||
setChatId: (e?: string) => any;
|
||||
|
||||
outLinkAuthData: OutLinkChatAuthProps;
|
||||
setOutLinkAuthData: (e: OutLinkChatAuthProps) => any;
|
||||
};
|
||||
|
||||
const createCustomStorage = () => {
|
||||
const sessionKeys = ['source', 'chatId', 'appId'];
|
||||
|
||||
return {
|
||||
getItem: (name: string) => {
|
||||
const sessionData = JSON.parse(sessionStorage.getItem(name) || '{}');
|
||||
const localData = JSON.parse(localStorage.getItem(name) || '{}');
|
||||
return JSON.stringify({ ...localData, ...sessionData });
|
||||
},
|
||||
setItem: (name: string, value: string) => {
|
||||
const data = JSON.parse(value);
|
||||
|
||||
// 分离 session 和 local 数据
|
||||
const sessionData = Object.fromEntries(
|
||||
Object.entries(data.state).filter(([key]) => sessionKeys.includes(key))
|
||||
);
|
||||
const localData = Object.fromEntries(
|
||||
Object.entries(data.state).filter(([key]) => !sessionKeys.includes(key))
|
||||
);
|
||||
|
||||
// 分别存储
|
||||
if (Object.keys(sessionData).length > 0) {
|
||||
sessionStorage.setItem(name, JSON.stringify({ state: sessionData, version: 0 }));
|
||||
}
|
||||
if (Object.keys(localData).length > 0) {
|
||||
localStorage.setItem(name, JSON.stringify({ state: localData, version: 0 }));
|
||||
}
|
||||
},
|
||||
removeItem: (name: string) => {
|
||||
sessionStorage.removeItem(name);
|
||||
localStorage.removeItem(name);
|
||||
}
|
||||
};
|
||||
};
|
||||
/*
|
||||
appId chatId source 存在当前 tab 中,刷新浏览器不会丢失。
|
||||
lastChatId 和 lastChatAppId 全局存储,切换 tab 或浏览器也不会丢失。用于首次 tab 进入对话时,恢复上一次的 chat。(只恢复相同来源的)
|
||||
*/
|
||||
export const useChatStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
source: undefined,
|
||||
setSource(e) {
|
||||
set((state) => {
|
||||
// 首次进入 chat 页面,如果相同的 source,则恢复上一次的 chatId
|
||||
if (!state.chatId && state.lastChatId && state.lastChatId.startsWith(e)) {
|
||||
state.chatId = state.lastChatId.split('-')[1];
|
||||
} else if (e !== get().source) {
|
||||
// 来源改变,强制重置 chatId
|
||||
state.chatId = getNanoid(24);
|
||||
}
|
||||
|
||||
if (!state.appId && state.lastChatAppId) {
|
||||
state.appId = state.lastChatAppId;
|
||||
}
|
||||
|
||||
state.source = e;
|
||||
});
|
||||
},
|
||||
appId: '',
|
||||
setAppId(e) {
|
||||
if (!e) return;
|
||||
|
||||
set((state) => {
|
||||
state.appId = e;
|
||||
state.lastChatAppId = e;
|
||||
});
|
||||
},
|
||||
lastChatId: '',
|
||||
chatId: '',
|
||||
setChatId(e) {
|
||||
const id = e || getNanoid(24);
|
||||
set((state) => {
|
||||
state.chatId = id;
|
||||
state.lastChatId = `${state.source}-${id}`;
|
||||
});
|
||||
},
|
||||
lastChatAppId: '',
|
||||
setLastChatAppId(e) {
|
||||
set((state) => {
|
||||
state.lastChatAppId = e;
|
||||
});
|
||||
},
|
||||
outLinkAuthData: {},
|
||||
setOutLinkAuthData(e) {
|
||||
set((state) => {
|
||||
state.outLinkAuthData = e;
|
||||
});
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'chatStore',
|
||||
storage: createJSONStorage(createCustomStorage),
|
||||
partialize: (state) => ({
|
||||
source: state.source,
|
||||
chatId: state.chatId,
|
||||
appId: state.appId,
|
||||
lastChatId: state.lastChatId,
|
||||
lastChatAppId: state.lastChatAppId
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Dispatch, ReactNode, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import {
|
||||
|
||||
@@ -3,15 +3,11 @@ import { getCollectionSource } from '@/web/core/dataset/api';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import type { readCollectionSourceBody } from '@/pages/api/core/dataset/collection/read';
|
||||
|
||||
export function getCollectionSourceAndOpen({
|
||||
collectionId,
|
||||
shareId,
|
||||
outLinkUid
|
||||
}: {
|
||||
collectionId: string;
|
||||
} & ShareChatAuthProps) {
|
||||
export function getCollectionSourceAndOpen(
|
||||
props: { collectionId: string } & readCollectionSourceBody
|
||||
) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { setLoading } = useSystemStore();
|
||||
@@ -20,7 +16,7 @@ export function getCollectionSourceAndOpen({
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const { value: url } = await getCollectionSource({ collectionId, shareId, outLinkUid });
|
||||
const { value: url } = await getCollectionSource(props);
|
||||
|
||||
if (!url) {
|
||||
throw new Error('No file found');
|
||||
@@ -33,7 +29,7 @@ export function getCollectionSourceAndOpen({
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: getErrText(error, t('common:error.fileNotFound')),
|
||||
title: t(getErrText(error, t('common:error.fileNotFound'))),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,7 +340,6 @@ export const checkWorkflowNodeAndConnection = ({
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[];
|
||||
edges: Edge<any>[];
|
||||
}): string[] | undefined => {
|
||||
const nodeIds: string[] = nodes.map((node) => node.data.nodeId);
|
||||
// 1. reference check. Required value
|
||||
for (const node of nodes) {
|
||||
const data = node.data;
|
||||
@@ -382,6 +381,26 @@ export const checkWorkflowNodeAndConnection = ({
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (data.flowNodeType === FlowNodeTypeEnum.userSelect) {
|
||||
const configValue = data.inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.userSelectOptions
|
||||
)?.value;
|
||||
if (
|
||||
!configValue ||
|
||||
configValue.length === 0 ||
|
||||
configValue.some((item: any) => !item.value)
|
||||
) {
|
||||
return [data.nodeId];
|
||||
}
|
||||
}
|
||||
if (data.flowNodeType === FlowNodeTypeEnum.formInput) {
|
||||
const value = data.inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.userInputForms
|
||||
)?.value;
|
||||
if (!value || value.length === 0) {
|
||||
return [data.nodeId];
|
||||
}
|
||||
}
|
||||
|
||||
// check node input
|
||||
if (
|
||||
@@ -392,32 +411,37 @@ export const checkWorkflowNodeAndConnection = ({
|
||||
}
|
||||
|
||||
if (input.required) {
|
||||
if (Array.isArray(input.value) && input.value.length === 0) return true;
|
||||
if (input.value === undefined) return true;
|
||||
if (Array.isArray(input.value) && input.value.length === 0) return true;
|
||||
}
|
||||
|
||||
// Check plugin output
|
||||
// if (
|
||||
// node.data.flowNodeType === FlowNodeTypeEnum.pluginOutput &&
|
||||
// (input.value?.length === 0 ||
|
||||
// (isValidReferenceValue(input.value, nodeIds) && !input.value?.[1]))
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// check reference invalid
|
||||
const renderType = input.renderTypeList[input.selectedTypeIndex || 0];
|
||||
if (renderType === FlowNodeInputTypeEnum.reference) {
|
||||
// 无效引用时,返回 true
|
||||
const checkValueValid = (value: ReferenceItemValueType) => {
|
||||
const nodeId = value?.[0];
|
||||
const outputId = value?.[1];
|
||||
|
||||
if (!nodeId || !outputId) return false;
|
||||
|
||||
return !!nodes
|
||||
.find((node) => node.data.nodeId === nodeId)
|
||||
?.data.outputs.find((output) => output.id === outputId);
|
||||
};
|
||||
|
||||
if (input.valueType?.startsWith('array')) {
|
||||
if (input.required && (!input.value || input.value.length === 0)) {
|
||||
input.value = input.value ?? [];
|
||||
// 如果内容为空,则报错
|
||||
if (input.required && input.value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isValidArrayReferenceValue(input.value, nodeIds);
|
||||
} else {
|
||||
// Single reference
|
||||
if (input.required) {
|
||||
return !checkValueValid(input.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Single reference
|
||||
return input.required && !isValidReferenceValue(input.value, nodeIds);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
@@ -566,7 +590,8 @@ export const compareSnapshot = (
|
||||
scheduledTriggerConfig: clone1.chatConfig?.scheduledTriggerConfig || undefined,
|
||||
chatInputGuide: clone1.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: clone1.chatConfig?.fileSelectConfig || undefined,
|
||||
instruction: clone1.chatConfig?.instruction || ''
|
||||
instruction: clone1.chatConfig?.instruction || '',
|
||||
autoExecute: clone1.chatConfig?.autoExecute || undefined
|
||||
},
|
||||
{
|
||||
welcomeText: clone2.chatConfig?.welcomeText || '',
|
||||
@@ -577,7 +602,8 @@ export const compareSnapshot = (
|
||||
scheduledTriggerConfig: clone2.chatConfig?.scheduledTriggerConfig || undefined,
|
||||
chatInputGuide: clone2.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: clone2.chatConfig?.fileSelectConfig || undefined,
|
||||
instruction: clone2.chatConfig?.instruction || ''
|
||||
instruction: clone2.chatConfig?.instruction || '',
|
||||
autoExecute: clone2.chatConfig?.autoExecute || undefined
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user