mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
@@ -6,6 +6,7 @@ import { createStoreUpdater } from 'zustand-utils';
|
||||
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
||||
import { useQueryState } from '@/hooks/useQueryParam';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { PortalViewType } from '@/store/chat/slices/portal/initialState';
|
||||
|
||||
// sync outside state to useChatStore
|
||||
const ThreadHydration = memo(() => {
|
||||
@@ -31,7 +32,7 @@ const ThreadHydration = memo(() => {
|
||||
// should open portal automatically when portalThread is set
|
||||
useEffect(() => {
|
||||
if (!!portalThread && !useChatStore.getState().showPortal) {
|
||||
useChatStore.getState().togglePortal(true);
|
||||
useChatStore.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
|
||||
}
|
||||
}, [portalThread]);
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
}));
|
||||
|
||||
const Layout = () => {
|
||||
const [showMobilePortal, isPortalThread, togglePortal] = useChatStore((s) => [
|
||||
const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
|
||||
s.showPortal,
|
||||
portalThreadSelectors.showThread(s),
|
||||
s.togglePortal,
|
||||
s.clearPortalStack,
|
||||
]);
|
||||
const { t } = useTranslation('portal');
|
||||
|
||||
@@ -42,7 +42,7 @@ const Layout = () => {
|
||||
destroyOnHidden
|
||||
footer={null}
|
||||
height={'95%'}
|
||||
onCancel={() => togglePortal(false)}
|
||||
onCancel={() => clearPortalStack()}
|
||||
open={showMobilePortal}
|
||||
styles={{
|
||||
body: { padding: 0 },
|
||||
|
||||
@@ -11,6 +11,7 @@ import UserAvatar from '@/features/User/UserAvatar';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { PortalViewType } from '@/store/chat/slices/portal/initialState';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
|
||||
|
||||
@@ -36,7 +37,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
|
||||
const addAgentsToGroup = useAgentGroupStore((s) => s.addAgentsToGroup);
|
||||
const removeAgentFromGroup = useAgentGroupStore((s) => s.removeAgentFromGroup);
|
||||
const toggleThread = useAgentGroupStore((s) => s.toggleThread);
|
||||
const togglePortal = useChatStore((s) => s.togglePortal);
|
||||
const pushPortalView = useChatStore((s) => s.pushPortalView);
|
||||
|
||||
// Get members from store (excluding supervisor)
|
||||
const groupMembers = useAgentGroupStore(agentGroupSelectors.getGroupMembers(groupId || ''));
|
||||
@@ -76,7 +77,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
|
||||
|
||||
const handleMemberClick = (agentId: string) => {
|
||||
toggleThread(agentId);
|
||||
togglePortal(true);
|
||||
pushPortalView({ agentId, type: PortalViewType.GroupThread });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStoreUpdater } from 'zustand-utils';
|
||||
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
||||
import { useQueryState } from '@/hooks/useQueryParam';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { PortalViewType } from '@/store/chat/slices/portal/initialState';
|
||||
|
||||
// sync outside state to useChatStore
|
||||
const ThreadHydration = memo(() => {
|
||||
@@ -31,7 +32,7 @@ const ThreadHydration = memo(() => {
|
||||
// should open portal automatically when portalThread is set
|
||||
useEffect(() => {
|
||||
if (!!portalThread && !useChatStore.getState().showPortal) {
|
||||
useChatStore.getState().togglePortal(true);
|
||||
useChatStore.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
|
||||
}
|
||||
}, [portalThread]);
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
}));
|
||||
|
||||
const Layout = () => {
|
||||
const [showMobilePortal, isPortalThread, togglePortal] = useChatStore((s) => [
|
||||
const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
|
||||
s.showPortal,
|
||||
portalThreadSelectors.showThread(s),
|
||||
s.togglePortal,
|
||||
s.clearPortalStack,
|
||||
]);
|
||||
const { t } = useTranslation('portal');
|
||||
|
||||
@@ -42,7 +42,7 @@ const Layout = () => {
|
||||
destroyOnHidden
|
||||
footer={null}
|
||||
height={'95%'}
|
||||
onCancel={() => togglePortal(false)}
|
||||
onCancel={() => clearPortalStack()}
|
||||
open={showMobilePortal}
|
||||
styles={{
|
||||
body: { padding: 0 },
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles, useResponsive } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { Activity, type PropsWithChildren, memo, useState } from 'react';
|
||||
|
||||
import {
|
||||
CHAT_PORTAL_MAX_WIDTH,
|
||||
CHAT_PORTAL_TOOL_UI_WIDTH,
|
||||
CHAT_PORTAL_WIDTH,
|
||||
} from '@/const/layoutTokens';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatPortalSelectors, portalThreadSelectors } from '@/store/chat/selectors';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
content: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100% !important;
|
||||
`,
|
||||
drawer: css`
|
||||
z-index: 10;
|
||||
height: 100%;
|
||||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
panel: css`
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
|
||||
const PortalPanel = memo(({ children }: PropsWithChildren) => {
|
||||
const { md = true } = useResponsive();
|
||||
|
||||
const [showPortal, showToolUI, showArtifactUI, showThread] = useChatStore((s) => [
|
||||
chatPortalSelectors.showPortal(s),
|
||||
chatPortalSelectors.showPluginUI(s),
|
||||
chatPortalSelectors.showArtifactUI(s),
|
||||
portalThreadSelectors.showThread(s),
|
||||
]);
|
||||
|
||||
const [portalWidth, updateSystemStatus] = useGlobalStore((s) => [
|
||||
systemStatusSelectors.portalWidth(s),
|
||||
s.updateSystemStatus,
|
||||
]);
|
||||
|
||||
const [tmpWidth, setWidth] = useState(portalWidth);
|
||||
if (tmpWidth !== portalWidth) setWidth(portalWidth);
|
||||
|
||||
const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => {
|
||||
if (!size) return;
|
||||
const nextWidth = typeof size.width === 'string' ? Number.parseInt(size.width) : size.width;
|
||||
if (!nextWidth) return;
|
||||
|
||||
if (isEqual(nextWidth, portalWidth)) return;
|
||||
setWidth(nextWidth);
|
||||
updateSystemStatus({ portalWidth: nextWidth });
|
||||
};
|
||||
|
||||
return (
|
||||
<DraggablePanel
|
||||
className={styles.drawer}
|
||||
classNames={{
|
||||
content: styles.content,
|
||||
}}
|
||||
defaultSize={{ width: tmpWidth }}
|
||||
expand={showPortal}
|
||||
maxWidth={CHAT_PORTAL_MAX_WIDTH}
|
||||
minWidth={
|
||||
(showArtifactUI || showToolUI || showThread) && md
|
||||
? CHAT_PORTAL_TOOL_UI_WIDTH
|
||||
: CHAT_PORTAL_WIDTH
|
||||
}
|
||||
mode={md ? 'fixed' : 'float'}
|
||||
onSizeChange={handleSizeChange}
|
||||
placement={'right'}
|
||||
showHandleWhenCollapsed={false}
|
||||
showHandleWideArea={false}
|
||||
size={{ height: '100%', width: portalWidth }}
|
||||
styles={{
|
||||
handle: { display: 'none' },
|
||||
}}
|
||||
>
|
||||
<DraggablePanelContainer
|
||||
style={{
|
||||
flex: 'none',
|
||||
height: '100%',
|
||||
maxHeight: '100vh',
|
||||
minWidth: CHAT_PORTAL_WIDTH,
|
||||
}}
|
||||
>
|
||||
<Activity mode={showPortal ? 'visible' : 'hidden'} name="GroupPortal">
|
||||
<Flexbox className={styles.panel}>{children}</Flexbox>
|
||||
</Activity>
|
||||
</DraggablePanelContainer>
|
||||
</DraggablePanel>
|
||||
);
|
||||
});
|
||||
|
||||
export default PortalPanel;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Suspense, memo } from 'react';
|
||||
|
||||
import DesktopLayout from '@/app/[variants]/(main)/group/features/Portal/_layout/Desktop';
|
||||
import MobileLayout from '@/app/[variants]/(main)/group/features/Portal/_layout/Mobile';
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
|
||||
interface PortalPanelProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const PortalPanel = memo<PortalPanelProps>(({ mobile }) => {
|
||||
const Layout = mobile ? MobileLayout : DesktopLayout;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loading debugId="PortalPanel" />}>
|
||||
<Layout />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
|
||||
PortalPanel.displayName = 'PortalPanel';
|
||||
|
||||
export default PortalPanel;
|
||||
@@ -12,10 +12,10 @@ import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
|
||||
const Header = memo(() => {
|
||||
const togglePortal = useChatStore((s) => s.togglePortal);
|
||||
const clearPortalStack = useChatStore((s) => s.clearPortalStack);
|
||||
const close = () => {
|
||||
useAgentGroupStore.setState({ activeThreadAgentId: '' });
|
||||
togglePortal(false);
|
||||
clearPortalStack();
|
||||
};
|
||||
const activeThreadAgentId = useAgentGroupStore((s) => s.activeThreadAgentId);
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ const md = css`
|
||||
`;
|
||||
|
||||
const MessageDetailBody = () => {
|
||||
const [messageDetailId, togglePortal] = useChatStore((s) => [
|
||||
const [messageDetailId, clearPortalStack] = useChatStore((s) => [
|
||||
chatPortalSelectors.messageDetailId(s),
|
||||
s.togglePortal,
|
||||
s.clearPortalStack,
|
||||
]);
|
||||
|
||||
const message = useChatStore(dbMessageSelectors.getDbMessageById(messageDetailId || ''), isEqual);
|
||||
@@ -26,7 +26,7 @@ const MessageDetailBody = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) {
|
||||
togglePortal(false);
|
||||
clearPortalStack();
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import { useChatStore } from '@/store/chat';
|
||||
import { chatPortalSelectors } from '@/store/chat/selectors';
|
||||
|
||||
const Header = memo<{ title: ReactNode }>(({ title }) => {
|
||||
const [canGoBack, goBack, togglePortal] = useChatStore((s) => [
|
||||
const [canGoBack, goBack, clearPortalStack] = useChatStore((s) => [
|
||||
chatPortalSelectors.canGoBack(s),
|
||||
s.goBack,
|
||||
s.togglePortal,
|
||||
s.clearPortalStack,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -30,7 +30,7 @@ const Header = memo<{ title: ReactNode }>(({ title }) => {
|
||||
<ActionIcon
|
||||
icon={PanelRightCloseIcon}
|
||||
onClick={() => {
|
||||
togglePortal(false);
|
||||
clearPortalStack();
|
||||
}}
|
||||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
/>
|
||||
|
||||
@@ -5,15 +5,15 @@ import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
export const useNavigateToAgent = () => {
|
||||
const togglePortal = useChatStore((s) => s.togglePortal);
|
||||
const clearPortalStack = useChatStore((s) => s.clearPortalStack);
|
||||
const router = useQueryRoute();
|
||||
|
||||
return useCallback(
|
||||
(agentId: string) => {
|
||||
togglePortal(false);
|
||||
clearPortalStack();
|
||||
|
||||
router.push(SESSION_CHAT_URL(agentId, false));
|
||||
},
|
||||
[togglePortal, router],
|
||||
[clearPortalStack, router],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -268,45 +268,4 @@ describe('chatDockSlice', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleDock', () => {
|
||||
it('should toggle dock state when no argument is provided', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
expect(result.current.showPortal).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.togglePortal();
|
||||
});
|
||||
|
||||
expect(result.current.showPortal).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.togglePortal();
|
||||
});
|
||||
|
||||
expect(result.current.showPortal).toBe(false);
|
||||
});
|
||||
|
||||
it('should set dock state to the provided value', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
act(() => {
|
||||
result.current.togglePortal(true);
|
||||
});
|
||||
|
||||
expect(result.current.showPortal).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.togglePortal(false);
|
||||
});
|
||||
|
||||
expect(result.current.showPortal).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.togglePortal(true);
|
||||
});
|
||||
|
||||
expect(result.current.showPortal).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface ChatPortalAction {
|
||||
pushPortalView: (view: PortalViewData) => void;
|
||||
replacePortalView: (view: PortalViewData) => void;
|
||||
toggleNotebook: (open?: boolean) => void;
|
||||
togglePortal: (open?: boolean) => void;
|
||||
}
|
||||
|
||||
// Helper to get current view type from stack
|
||||
@@ -222,28 +221,4 @@ pushPortalView: (view) => {
|
||||
get().closeNotebook();
|
||||
}
|
||||
},
|
||||
|
||||
togglePortal: (open) => {
|
||||
const nextOpen = open === undefined ? !get().showPortal : open;
|
||||
|
||||
if (!nextOpen) {
|
||||
// When closing, clear the stack
|
||||
set({ portalStack: [], showPortal: false }, false, 'togglePortal/close');
|
||||
} else {
|
||||
// When opening, if stack is empty, push Home view
|
||||
const { portalStack } = get();
|
||||
if (portalStack.length === 0) {
|
||||
set(
|
||||
{
|
||||
portalStack: [{ type: PortalViewType.Home }],
|
||||
showPortal: true,
|
||||
},
|
||||
false,
|
||||
'togglePortal/openHome',
|
||||
);
|
||||
} else {
|
||||
set({ showPortal: true }, false, 'togglePortal/open');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('thread action', () => {
|
||||
describe('openThreadCreator', () => {
|
||||
it('should set thread creator state and open portal', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const togglePortalSpy = vi.spyOn(result.current, 'togglePortal');
|
||||
const pushPortalViewSpy = vi.spyOn(result.current, 'pushPortalView');
|
||||
|
||||
act(() => {
|
||||
result.current.openThreadCreator('message-id');
|
||||
@@ -150,14 +150,14 @@ describe('thread action', () => {
|
||||
expect(result.current.threadStartMessageId).toBe('message-id');
|
||||
expect(result.current.portalThreadId).toBeUndefined();
|
||||
expect(result.current.startToForkThread).toBe(true);
|
||||
expect(togglePortalSpy).toHaveBeenCalledWith(true);
|
||||
expect(pushPortalViewSpy).toHaveBeenCalledWith({ type: 'thread', startMessageId: 'message-id' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('openThreadInPortal', () => {
|
||||
it('should set portal thread state and open portal', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const togglePortalSpy = vi.spyOn(result.current, 'togglePortal');
|
||||
const pushPortalViewSpy = vi.spyOn(result.current, 'pushPortalView');
|
||||
|
||||
act(() => {
|
||||
result.current.openThreadInPortal('thread-id', 'source-message-id');
|
||||
@@ -166,7 +166,11 @@ describe('thread action', () => {
|
||||
expect(result.current.portalThreadId).toBe('thread-id');
|
||||
expect(result.current.threadStartMessageId).toBe('source-message-id');
|
||||
expect(result.current.startToForkThread).toBe(false);
|
||||
expect(togglePortalSpy).toHaveBeenCalledWith(true);
|
||||
expect(pushPortalViewSpy).toHaveBeenCalledWith({
|
||||
type: 'thread',
|
||||
threadId: 'thread-id',
|
||||
startMessageId: 'source-message-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,7 +186,7 @@ describe('thread action', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const togglePortalSpy = vi.spyOn(result.current, 'togglePortal');
|
||||
const clearPortalStackSpy = vi.spyOn(result.current, 'clearPortalStack');
|
||||
|
||||
act(() => {
|
||||
result.current.closeThreadPortal();
|
||||
@@ -191,7 +195,7 @@ describe('thread action', () => {
|
||||
expect(result.current.portalThreadId).toBeUndefined();
|
||||
expect(result.current.threadStartMessageId).toBeUndefined();
|
||||
expect(result.current.startToForkThread).toBeUndefined();
|
||||
expect(togglePortalSpy).toHaveBeenCalledWith(false);
|
||||
expect(clearPortalStackSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { merge } from '@/utils/merge';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { displayMessageSelectors } from '../message/selectors';
|
||||
import { PortalViewType } from '../portal/initialState';
|
||||
import { type ThreadDispatch, threadReducer } from './reducer';
|
||||
import { genParentMessages } from './selectors';
|
||||
|
||||
@@ -88,7 +89,8 @@ export const chatThreadMessage: StateCreator<
|
||||
false,
|
||||
'openThreadCreator',
|
||||
);
|
||||
get().togglePortal(true);
|
||||
// Push Thread view to portal stack instead of togglePortal
|
||||
get().pushPortalView({ type: PortalViewType.Thread, startMessageId: messageId });
|
||||
},
|
||||
openThreadInPortal: (threadId, sourceMessageId) => {
|
||||
set(
|
||||
@@ -96,7 +98,12 @@ export const chatThreadMessage: StateCreator<
|
||||
false,
|
||||
'openThreadInPortal',
|
||||
);
|
||||
get().togglePortal(true);
|
||||
// Push Thread view to portal stack with threadId
|
||||
get().pushPortalView({
|
||||
type: PortalViewType.Thread,
|
||||
threadId,
|
||||
startMessageId: sourceMessageId ?? undefined,
|
||||
});
|
||||
},
|
||||
|
||||
closeThreadPortal: () => {
|
||||
@@ -105,7 +112,7 @@ export const chatThreadMessage: StateCreator<
|
||||
false,
|
||||
'closeThreadPortal',
|
||||
);
|
||||
get().togglePortal(false);
|
||||
get().clearPortalStack();
|
||||
},
|
||||
createThread: async ({ message, sourceMessageId, topicId, type }) => {
|
||||
set({ isCreatingThread: true }, false, n('creatingThread/start'));
|
||||
|
||||
Reference in New Issue
Block a user