Commit 5cd1c2af by Archer Committed by GitHub

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