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:
Archer
2025-08-15 11:03:38 +08:00
committed by GitHub
parent eadf2fd54c
commit 5cd1c2af14
24 changed files with 188 additions and 136 deletions

View File

@@ -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',

View File

@@ -12,9 +12,12 @@ description: 'FastGPT V4.12.1 更新说明'
1. 工作流响应优化,主动指定响应值进入历史记录,而不是根据 key 决定。
2. 避免工作流中,变量替换导致的死循环或深度递归风险。
3. 对话日志导出,固定导出对话详情。
## 🐛 修复
1. 工具密钥输入boolean 值无法通过 form 校验。
2. 对话页pane切换可能导致数据异常。
3. 对话日志看板数据表索引不正确。
## 🔨 工具更新

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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 });

View File

@@ -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 }
},
{

View File

@@ -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 });

View File

@@ -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(() => {

View File

@@ -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 (

View File

@@ -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);

View File

@@ -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>

View File

@@ -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();
};

View File

@@ -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);
}
}}
>

View File

@@ -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() {

View File

@@ -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>
)
) : (

View File

@@ -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'} />

View File

@@ -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'
}
/**

View File

@@ -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 {

View File

@@ -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'
);
}
}
]

View File

@@ -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 }
);

View File

@@ -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') } },

View File

@@ -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
},

View File

@@ -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(

View File

@@ -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 };