mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-15 15:41:05 +00:00
perf: chat pane (#5462)
* fix: sync pane with URL appId vs Home appId to avoid cross-tab interference (#5456) * perf: chat pane * perf: markdown render * update app chat logs index * doc * doc redirect --------- Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
const exactMap: Record<string, string> = {
|
||||
'/docs': '/docs/introduction',
|
||||
'/docs/intro': '/docs/introduction',
|
||||
'/docs/guide/dashboard/workflow/coreferenceresolution':
|
||||
'/docs/introduction/guide/dashboard/workflow/coreferenceResolution',
|
||||
|
@@ -12,9 +12,12 @@ description: 'FastGPT V4.12.1 更新说明'
|
||||
|
||||
1. 工作流响应优化,主动指定响应值进入历史记录,而不是根据 key 决定。
|
||||
2. 避免工作流中,变量替换导致的死循环或深度递归风险。
|
||||
3. 对话日志导出,固定导出对话详情。
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
1. 工具密钥输入,boolean 值无法通过 form 校验。
|
||||
2. 对话页,pane切换可能导致数据异常。
|
||||
3. 对话日志看板数据表索引不正确。
|
||||
|
||||
## 🔨 工具更新
|
||||
|
@@ -30,9 +30,9 @@
|
||||
"document/content/docs/introduction/development/modelConfig/one-api.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-14T16:11:54+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-14T16:11:54+08:00",
|
||||
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-08-14T16:11:54+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-14T18:54:47+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-14T18:54:47+08:00",
|
||||
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-08-14T18:54:47+08:00",
|
||||
"document/content/docs/introduction/development/openapi/share.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/development/proxy/http_proxy.mdx": "2025-07-23T21:35:03+08:00",
|
||||
@@ -103,7 +103,7 @@
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00",
|
||||
"document/content/docs/upgrading/4-12/4120.mdx": "2025-08-12T22:45:19+08:00",
|
||||
"document/content/docs/upgrading/4-12/4121.mdx": "2025-08-14T15:48:22+08:00",
|
||||
"document/content/docs/upgrading/4-12/4121.mdx": "2025-08-14T22:01:36+08:00",
|
||||
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
|
||||
@@ -182,6 +182,6 @@
|
||||
"document/content/docs/use-cases/external-integration/dingtalk.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00",
|
||||
"document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-14T16:11:54+08:00",
|
||||
"document/content/docs/use-cases/external-integration/openapi.mdx": "2025-08-14T18:54:47+08:00",
|
||||
"document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00"
|
||||
}
|
@@ -5,15 +5,15 @@ import { AppCollectionName } from '../schema';
|
||||
export const ChatLogCollectionName = 'app_chat_logs';
|
||||
|
||||
const ChatLogSchema = new Schema({
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true
|
||||
},
|
||||
appId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: AppCollectionName,
|
||||
required: true
|
||||
},
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true
|
||||
},
|
||||
chatId: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -68,8 +68,15 @@ const ChatLogSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
// Get chart data
|
||||
ChatLogSchema.index({ teamId: 1, appId: 1, source: 1, updateTime: -1 });
|
||||
ChatLogSchema.index({ userId: 1, appId: 1, source: 1, createTime: -1 });
|
||||
// Get chart data isFirstChat
|
||||
ChatLogSchema.index({ isFirstChat: 1, teamId: 1, appId: 1, source: 1, createTime: -1 });
|
||||
// Get userStats
|
||||
ChatLogSchema.index({ teamId: 1, appId: 1, userId: 1 });
|
||||
|
||||
// Init shell
|
||||
ChatLogSchema.index({ teamId: 1, appId: 1, chatId: 1 });
|
||||
|
||||
export const MongoAppChatLog = getMongoLogModel<AppChatLogSchema>(
|
||||
ChatLogCollectionName,
|
||||
|
@@ -90,7 +90,7 @@ const ChatSchema = new Schema({
|
||||
|
||||
try {
|
||||
// Tmp
|
||||
ChatSchema.index({ initStatistics: 1 });
|
||||
ChatSchema.index({ initStatistics: 1, _id: -1 });
|
||||
ChatSchema.index({ appId: 1, tmbId: 1, outLinkUid: 1 });
|
||||
|
||||
ChatSchema.index({ chatId: 1 });
|
||||
@@ -100,7 +100,7 @@ try {
|
||||
ChatSchema.index({ appId: 1, chatId: 1 });
|
||||
|
||||
// get chat logs;
|
||||
ChatSchema.index({ teamId: 1, appId: 1, updateTime: -1, sources: 1 });
|
||||
ChatSchema.index({ teamId: 1, appId: 1, sources: 1, tmbId: 1, updateTime: -1 });
|
||||
// get share chat history
|
||||
ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 });
|
||||
|
||||
|
@@ -178,6 +178,7 @@ export async function saveChat({
|
||||
) || 0;
|
||||
|
||||
const hasHistoryChat = await MongoAppChatLog.exists({
|
||||
teamId,
|
||||
appId,
|
||||
userId,
|
||||
createTime: { $lt: now }
|
||||
@@ -185,8 +186,9 @@ export async function saveChat({
|
||||
|
||||
await MongoAppChatLog.updateOne(
|
||||
{
|
||||
chatId,
|
||||
teamId,
|
||||
appId,
|
||||
chatId,
|
||||
updateTime: { $gte: fifteenMinutesAgo }
|
||||
},
|
||||
{
|
||||
|
@@ -61,7 +61,7 @@ const UsageSchema = new Schema({
|
||||
});
|
||||
|
||||
try {
|
||||
UsageSchema.index({ teamId: 1, tmbId: 1, source: 1, time: 1, appName: 1 });
|
||||
UsageSchema.index({ teamId: 1, tmbId: 1, source: 1, time: 1, appName: 1, _id: -1 });
|
||||
// timer task. clear dead team
|
||||
// UsageSchema.index({ teamId: 1, time: -1 });
|
||||
|
||||
|
@@ -40,7 +40,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo } = useUserStore();
|
||||
const { gitStar, feConfigs } = useSystemStore();
|
||||
const { lastChatAppId } = useChatStore();
|
||||
const { lastChatAppId, lastPane } = useChatStore();
|
||||
|
||||
const navbarList = useMemo(
|
||||
() => [
|
||||
@@ -48,7 +48,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
label: t('common:navbar.Chat'),
|
||||
icon: 'core/chat/chatLight',
|
||||
activeIcon: 'core/chat/chatFill',
|
||||
link: `/chat?appId=${lastChatAppId}`,
|
||||
link: `/chat?appId=${lastChatAppId}&pane=${lastPane}`,
|
||||
activeLink: ['/chat']
|
||||
},
|
||||
{
|
||||
@@ -92,7 +92,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
]
|
||||
}
|
||||
],
|
||||
[lastChatAppId, t]
|
||||
[lastChatAppId, lastPane, t]
|
||||
);
|
||||
|
||||
const isSecondNavbarPage = useMemo(() => {
|
||||
|
@@ -9,14 +9,15 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
const NavbarPhone = ({ unread }: { unread: number }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { lastChatAppId } = useChatStore();
|
||||
const { lastChatAppId, lastPane } = useChatStore();
|
||||
|
||||
const navbarList = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('common:navbar.Chat'),
|
||||
icon: 'core/chat/chatLight',
|
||||
activeIcon: 'core/chat/chatFill',
|
||||
link: `/chat?appId=${lastChatAppId}`,
|
||||
link: `/chat?appId=${lastChatAppId}&pane=${lastPane}`,
|
||||
activeLink: ['/chat'],
|
||||
unread: 0
|
||||
},
|
||||
@@ -63,7 +64,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
|
||||
unread
|
||||
}
|
||||
],
|
||||
[t, lastChatAppId, unread]
|
||||
[t, lastChatAppId, lastPane, unread]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -181,7 +181,7 @@ const A = ({
|
||||
showAnimation: boolean;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const content = useCreation(() => String(children), [children]);
|
||||
const content = useMemo(() => (children === undefined ? '' : String(children)), [children]);
|
||||
|
||||
// empty href link
|
||||
if (!props.href && typeof children?.[0] === 'string') {
|
||||
@@ -203,7 +203,7 @@ const A = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <Link {...props}>{children}</Link>;
|
||||
return <Link {...props}>{content || props?.href}</Link>;
|
||||
};
|
||||
|
||||
export default React.memo(A);
|
||||
|
@@ -25,6 +25,7 @@ import { postTransition2Workflow } from '@/web/core/app/api/app';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { type SimpleAppSnapshotType } from './useSnapshots';
|
||||
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
|
||||
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
|
||||
|
||||
const AppCard = ({
|
||||
appForm,
|
||||
@@ -103,7 +104,9 @@ const AppCard = ({
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
onClick={() =>
|
||||
router.push(`/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`)
|
||||
}
|
||||
>
|
||||
{t('common:core.Chat')}
|
||||
</Button>
|
||||
|
@@ -131,13 +131,11 @@ const MobileDrawer = ({
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
const { onChangeAppId } = useContextSelector(ChatContext, (v) => v);
|
||||
|
||||
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange);
|
||||
|
||||
const onclickApp = (id: string) => {
|
||||
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS);
|
||||
onChangeAppId(id);
|
||||
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id);
|
||||
onCloseDrawer();
|
||||
setChatId();
|
||||
};
|
||||
|
@@ -95,13 +95,7 @@ const ListItem = ({ appType }: { appType: AppTypeEnum | 'all' }) => {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
appId: app._id
|
||||
}
|
||||
});
|
||||
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS);
|
||||
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, app._id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@@ -22,6 +22,8 @@ import { getInitChatInfo } from '@/web/core/chat/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRouter } from 'next/router';
|
||||
import NextHead from '@/components/common/NextHead';
|
||||
import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext';
|
||||
import { ChatSidebarPaneEnum } from '../constants';
|
||||
|
||||
type Props = {
|
||||
myApps: AppListItemType[];
|
||||
@@ -35,6 +37,7 @@ const AppChatWindow = ({ myApps }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange);
|
||||
const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider);
|
||||
const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
|
||||
const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider);
|
||||
@@ -68,12 +71,7 @@ const AppChatWindow = ({ myApps }: Props) => {
|
||||
errorToast: '',
|
||||
onError(e: any) {
|
||||
if (e?.code && e.code >= 502000) {
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
appId: myApps[0]?._id
|
||||
}
|
||||
});
|
||||
handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS);
|
||||
}
|
||||
},
|
||||
onFinally() {
|
||||
|
@@ -77,7 +77,7 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
const { llmModelList, defaultModels, feConfigs } = useSystemStore();
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
|
||||
const onHomeClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange);
|
||||
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange);
|
||||
|
||||
const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider);
|
||||
const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
|
||||
@@ -105,6 +105,10 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
const modelData = getModelFromList(llmModelList, selectedModel || '');
|
||||
return modelData?.avatar || HUGGING_FACE_ICON;
|
||||
}, [selectedModel, llmModelList]);
|
||||
const selectedModelButtonLabel = useMemo(() => {
|
||||
const modelData = availableModels.find((model) => model.value === selectedModel);
|
||||
return modelData?.label || selectedModel;
|
||||
}, [selectedModel, availableModels]);
|
||||
|
||||
const availableTools = useMemo(
|
||||
() => chatSettings?.selectedTools || [],
|
||||
@@ -121,16 +125,16 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
}, [availableTools, selectedToolIds]);
|
||||
// If selected ToolIds not in availableTools, Remove it
|
||||
useEffect(() => {
|
||||
if (availableTools.length === 0) return;
|
||||
if (!chatSettings?.selectedTools) return;
|
||||
setSelectedToolIds(
|
||||
selectedToolIds.filter((id) => availableTools.some((tool) => tool.pluginId === id))
|
||||
);
|
||||
}, [availableTools]);
|
||||
}, [availableTools, chatSettings?.selectedTools]);
|
||||
|
||||
// 初始化聊天数据
|
||||
const { loading } = useRequest2(
|
||||
async () => {
|
||||
if (!appId || forbidLoadChat.current) return;
|
||||
if (!appId || forbidLoadChat.current || !feConfigs?.isPlus) return;
|
||||
|
||||
const modelData = getWebLLMModel(selectedModel);
|
||||
const res = await getInitChatInfo({ appId, chatId });
|
||||
@@ -167,13 +171,20 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
errorToast: '',
|
||||
onFinally() {
|
||||
forbidLoadChat.current = false;
|
||||
},
|
||||
onError() {
|
||||
if (feConfigs.isPlus) {
|
||||
handlePaneChange(ChatSidebarPaneEnum.HOME);
|
||||
} else {
|
||||
handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
if (!feConfigs?.isPlus) {
|
||||
onHomeClick(ChatSidebarPaneEnum.RECENTLY_USED_APPS);
|
||||
handlePaneChange(ChatSidebarPaneEnum.TEAM_APPS);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -255,7 +266,7 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
valueLabel={
|
||||
<Flex className="textEllipsis" maxW={['74px', '100%']} alignItems={'center'} gap={1}>
|
||||
{isPc && <Avatar src={selectedModelAvatar} w={4} h={4} />}
|
||||
<Box>{selectedModel}</Box>
|
||||
<Box>{selectedModelButtonLabel}</Box>
|
||||
</Flex>
|
||||
}
|
||||
onChange={async (model) => {
|
||||
@@ -351,7 +362,8 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
setSelectedToolIds,
|
||||
setChatBoxData,
|
||||
isPc,
|
||||
selectedModelAvatar
|
||||
selectedModelAvatar,
|
||||
selectedModelButtonLabel
|
||||
]
|
||||
);
|
||||
|
||||
@@ -395,35 +407,17 @@ const HomeChatWindow = ({ myApps }: Props) => {
|
||||
flexDirection={'column'}
|
||||
>
|
||||
{isPc ? (
|
||||
chatRecords.length > 0 && (
|
||||
chatBoxData?.title && (
|
||||
<Flex
|
||||
py={4}
|
||||
py={3}
|
||||
bg="white"
|
||||
fontWeight={500}
|
||||
color="myGray.900"
|
||||
color="myGray.600"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
h="56px"
|
||||
borderBottom="sm"
|
||||
>
|
||||
<MyPopover
|
||||
trigger="hover"
|
||||
placement="bottom"
|
||||
Trigger={
|
||||
<Flex
|
||||
flex="1"
|
||||
textAlign="center"
|
||||
cursor="pointer"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{chatBoxData?.title}
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
{() => `${t('chat:home.chat_id')}:${chatBoxData?.chatId}`}
|
||||
</MyPopover>
|
||||
{chatBoxData?.title}
|
||||
</Flex>
|
||||
)
|
||||
) : (
|
||||
|
@@ -459,7 +459,6 @@ const BottomSection = () => {
|
||||
};
|
||||
|
||||
const SliderApps = ({ apps, activeAppId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1);
|
||||
@@ -467,32 +466,9 @@ const SliderApps = ({ apps, activeAppId }: Props) => {
|
||||
|
||||
const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange);
|
||||
|
||||
const getAppList = useCallback(async ({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({
|
||||
parentId,
|
||||
type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin]
|
||||
}).then((res) =>
|
||||
res.map<GetResourceListItemResponse>((item) => ({
|
||||
id: item._id,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
isFolder: item.type === AppTypeEnum.folder
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const isRecentlyUsedAppSelected = (id: string) =>
|
||||
pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && id === activeAppId;
|
||||
|
||||
const handleSelectRecentlyUsedApp = useCallback(
|
||||
(id: string) => {
|
||||
if (pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && id === activeAppId) return;
|
||||
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS);
|
||||
router.replace({ query: { ...router.query, appId: id } });
|
||||
},
|
||||
[pane, router, activeAppId, handlePaneChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<MotionFlex
|
||||
flexDirection={'column'}
|
||||
@@ -544,7 +520,8 @@ const SliderApps = ({ apps, activeAppId }: Props) => {
|
||||
? { bg: 'primary.100', color: 'primary.600' }
|
||||
: {
|
||||
_hover: { bg: 'primary.100', color: 'primary.600' },
|
||||
onClick: () => handleSelectRecentlyUsedApp(item._id)
|
||||
onClick: () =>
|
||||
handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, item._id)
|
||||
})}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'md'} />
|
||||
|
@@ -1,11 +1,11 @@
|
||||
export enum ChatSidebarPaneEnum {
|
||||
SETTING = 'setting',
|
||||
TEAM_APPS = 'team_apps',
|
||||
RECENTLY_USED_APPS = 'recently_used_apps',
|
||||
SETTING = 's',
|
||||
TEAM_APPS = 'ta',
|
||||
RECENTLY_USED_APPS = 'ra',
|
||||
|
||||
// these two features are only available in the commercial version
|
||||
HOME = 'home',
|
||||
FAVORITE_APPS = 'favorite_apps'
|
||||
HOME = 'h',
|
||||
FAVORITE_APPS = 'fa'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -21,6 +21,8 @@ export const useChat = (appId: string) => {
|
||||
|
||||
// initialize user info
|
||||
useMount(async () => {
|
||||
// ensure store has current appId before setting source (avoids fallback to lastChatAppId)
|
||||
if (appId) setAppId(appId);
|
||||
try {
|
||||
await initUserInfo();
|
||||
} catch (error) {
|
||||
@@ -31,10 +33,11 @@ export const useChat = (appId: string) => {
|
||||
}
|
||||
});
|
||||
|
||||
// watch appId
|
||||
// sync appId to store as soon as route/appId changes
|
||||
useEffect(() => {
|
||||
if (!userInfo || !appId) return;
|
||||
setAppId(appId);
|
||||
if (appId) {
|
||||
setAppId(appId);
|
||||
}
|
||||
}, [appId, setAppId, userInfo]);
|
||||
|
||||
return {
|
||||
|
@@ -37,6 +37,7 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { type RequireOnlyOne } from '@fastgpt/global/common/type/utils';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
import { type PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
|
||||
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
|
||||
|
||||
const ListItem = () => {
|
||||
@@ -193,7 +194,10 @@ const ListItem = () => {
|
||||
} else if (app.permission.hasWritePer || app.permission.hasReadChatLogPer) {
|
||||
router.push(`/app/detail?appId=${app._id}`);
|
||||
} else {
|
||||
window.open(`/chat?appId=${app._id}`, '_blank');
|
||||
window.open(
|
||||
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
}}
|
||||
{...getBoxProps({
|
||||
@@ -271,7 +275,10 @@ const ListItem = () => {
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('app:go_to_chat'),
|
||||
onClick: () => {
|
||||
window.open(`/chat?appId=${app._id}`, '_blank');
|
||||
window.open(
|
||||
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -287,7 +294,10 @@ const ListItem = () => {
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('app:go_to_run'),
|
||||
onClick: () => {
|
||||
window.open(`/chat?appId=${app._id}`, '_blank');
|
||||
window.open(
|
||||
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@@ -157,7 +157,7 @@ async function processChatRecord(chat: ChatSchemaType) {
|
||||
};
|
||||
|
||||
await MongoAppChatLog.updateOne(
|
||||
{ appId: chat.appId, chatId: chat.chatId },
|
||||
{ teamId: chat.teamId, appId: chat.appId, chatId: chat.chatId },
|
||||
{ $set: chatLogData },
|
||||
{ upsert: true }
|
||||
);
|
||||
|
@@ -52,8 +52,8 @@ async function handler(
|
||||
$gte: new Date(dateStart),
|
||||
$lte: new Date(dateEnd)
|
||||
},
|
||||
...(sources && { source: { $in: sources } }),
|
||||
...(tmbIds && { tmbId: { $in: tmbIds.map((item) => new Types.ObjectId(item)) } }),
|
||||
source: sources ? { $in: sources } : { $exists: true },
|
||||
tmbId: tmbIds ? { $in: tmbIds.map((item) => new Types.ObjectId(item)) } : { $exists: true },
|
||||
...(chatSearch && {
|
||||
$or: [
|
||||
{ chatId: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } },
|
||||
|
@@ -14,7 +14,7 @@ async function handler(req: ApiRequestProps<UpdateChatFeedbackProps>, res: NextA
|
||||
return Promise.reject('chatId or dataId is empty');
|
||||
}
|
||||
|
||||
await authChatCrud({
|
||||
const { teamId } = await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
@@ -60,8 +60,9 @@ async function handler(req: ApiRequestProps<UpdateChatFeedbackProps>, res: NextA
|
||||
return 0;
|
||||
})();
|
||||
|
||||
await MongoAppChatLog.findOneAndUpdate(
|
||||
await MongoAppChatLog.updateOne(
|
||||
{
|
||||
teamId,
|
||||
appId,
|
||||
chatId
|
||||
},
|
||||
|
@@ -16,7 +16,7 @@ type ChatSettingReturnType = ChatSettingSchema | undefined;
|
||||
|
||||
export type ChatSettingContextValue = {
|
||||
pane: ChatSidebarPaneEnum;
|
||||
handlePaneChange: (pane: ChatSidebarPaneEnum) => void;
|
||||
handlePaneChange: (pane: ChatSidebarPaneEnum, _id?: string) => void;
|
||||
collapse: CollapseStatusType;
|
||||
onTriggerCollapse: () => void;
|
||||
chatSettings: ChatSettingSchema | undefined;
|
||||
@@ -41,47 +41,60 @@ export const ChatSettingContext = createContext<ChatSettingContextValue>({
|
||||
|
||||
export const ChatSettingContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { appId, setLastPane, lastPane } = useChatStore();
|
||||
const { appId, setLastPane, setLastChatAppId, lastPane } = useChatStore();
|
||||
|
||||
const { pane = lastPane || ChatSidebarPaneEnum.HOME } = router.query as {
|
||||
pane: ChatSidebarPaneEnum;
|
||||
};
|
||||
|
||||
const [collapse, setCollapse] = useState<CollapseStatusType>(defaultCollapseStatus);
|
||||
|
||||
const { data: chatSettings, runAsync: refreshChatSetting } = useRequest2<
|
||||
ChatSettingReturnType,
|
||||
[]
|
||||
>(
|
||||
const { data: chatSettings, runAsync: refreshChatSetting } = useRequest2(
|
||||
async () => {
|
||||
if (!feConfigs.isPlus) return;
|
||||
const settings = await getChatSetting();
|
||||
return settings;
|
||||
return await getChatSetting();
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [feConfigs.isPlus]
|
||||
refreshDeps: [feConfigs.isPlus],
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
// Reset home page appId
|
||||
if (pane === ChatSidebarPaneEnum.HOME && appId !== data.appId) {
|
||||
handlePaneChange(ChatSidebarPaneEnum.HOME, data.appId);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [pane, setPane] = useState<ChatSidebarPaneEnum>(
|
||||
lastPane ??
|
||||
(feConfigs.isPlus ? ChatSidebarPaneEnum.HOME : ChatSidebarPaneEnum.RECENTLY_USED_APPS)
|
||||
);
|
||||
const handlePaneChange = useCallback(
|
||||
(newPane: ChatSidebarPaneEnum) => {
|
||||
// 如果切换到首页,且当前不是隐藏应用,则切换到隐藏应用
|
||||
const hiddenAppId = chatSettings?.appId;
|
||||
if (newPane === ChatSidebarPaneEnum.HOME && hiddenAppId && appId !== hiddenAppId) {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
appId: hiddenAppId
|
||||
}
|
||||
});
|
||||
}
|
||||
setPane(newPane);
|
||||
async (newPane: ChatSidebarPaneEnum, id?: string) => {
|
||||
if (newPane === pane && !id) return;
|
||||
|
||||
const _id = (() => {
|
||||
if (id) return id;
|
||||
|
||||
const hiddenAppId = chatSettings?.appId;
|
||||
if (newPane === ChatSidebarPaneEnum.HOME && hiddenAppId) {
|
||||
return hiddenAppId;
|
||||
}
|
||||
|
||||
return '';
|
||||
})();
|
||||
|
||||
await router.replace({
|
||||
query: {
|
||||
appId: _id,
|
||||
pane: newPane
|
||||
}
|
||||
});
|
||||
|
||||
setLastPane(newPane);
|
||||
setLastChatAppId(_id);
|
||||
},
|
||||
[setLastPane, chatSettings?.appId, appId, router]
|
||||
[setLastPane, chatSettings?.appId, appId, router, pane]
|
||||
);
|
||||
|
||||
const logos: Pick<ChatSettingSchema, 'wideLogoUrl' | 'squareLogoUrl'> = useMemo(
|
||||
|
@@ -65,6 +65,7 @@ const createCustomStorage = () => {
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
appId chatId source 存在当前 tab 中,刷新浏览器不会丢失。
|
||||
lastChatId 和 lastChatAppId 全局存储,切换 tab 或浏览器也不会丢失。用于首次 tab 进入对话时,恢复上一次的 chat。(只恢复相同来源的)
|
||||
@@ -144,4 +145,50 @@ export const useChatStore = create<State>()(
|
||||
)
|
||||
);
|
||||
|
||||
// Storage 事件监听器,用于跨 tab 同步
|
||||
const createStorageListener = (store: any) => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'chatStore' && e.newValue && e.storageArea === localStorage) {
|
||||
try {
|
||||
const newData = JSON.parse(e.newValue);
|
||||
const currentState = store.getState();
|
||||
|
||||
// 只同步 localStorage 中的数据(非 session 数据)
|
||||
const sessionKeys = ['source', 'chatId', 'appId'];
|
||||
const updatedState: Partial<State> = {};
|
||||
let hasChanges = false;
|
||||
|
||||
Object.entries(newData.state || {}).forEach(([key, value]) => {
|
||||
if (!sessionKeys.includes(key) && currentState[key] !== value) {
|
||||
(updatedState as any)[key] = value;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
store.setState(updatedState);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse storage event data:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 添加监听器
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
};
|
||||
// 初始化存储事件监听器
|
||||
if (typeof window !== 'undefined') {
|
||||
createStorageListener(useChatStore);
|
||||
}
|
||||
|
||||
export { createCustomStorage };
|
||||
|
Reference in New Issue
Block a user