Navbar with AI chats (#18161)

## Summary
Add Home/Chat tabs and a dedicated threads list in the navigation
drawer.

## Changes
- **Navbar tabs:** Tabs in the drawer to switch between Home and Chat
(with “New chat” button). Shown on desktop when expanded and on mobile
below the workspace selector.
- **Navbar threads list:** New `NavigationDrawerAIChatThreadsList` for
the Chat tab with date groups (Today / Yesterday / Older), thread rows
as `NavigationDrawerItem` (IconComment, title, timestamp). Shared
`useAIChatThreadClick` hook used by navbar and command menu; navbar
passes `resetNavigationStack: true`.
- **NavigationDrawerItem:** New `alwaysShowRightOptions` prop so the
timestamp is always visible (no hover-only).

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Abdul Rahman
2026-02-28 04:07:14 +05:30
committed by GitHub
parent 9f5a8735c9
commit e806d36099
34 changed files with 970 additions and 291 deletions
@@ -2,13 +2,11 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
import { useAuth } from '@/auth/hooks/useAuth';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
import { supportChatState } from '@/client-config/states/supportChatState';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { supportChatState } from '@/client-config/states/supportChatState';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -19,22 +17,27 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { type ApolloError } from '@apollo/client';
import styled from '@emotion/styled';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isNonEmptyString } from '@sniptt/guards';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useLocation } from 'react-router-dom';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
Avatar,
IconDotsVertical,
IconHelpCircle,
IconLogout,
IconMessage,
IconPlus,
IconSettings,
IconSwitchHorizontal,
IconUserPlus,
} from 'twenty-ui/display';
@@ -57,7 +60,6 @@ const StyledDescription = styled.div`
export const MultiWorkspaceDropdownDefaultComponents = () => {
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const { t } = useLingui();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const availableWorkspaces = useAtomStateValue(availableWorkspacesState);
@@ -79,19 +81,21 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
multiWorkspaceDropdownState,
);
const location = useLocation();
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useAtomState(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetAtomState(
navigationDrawerExpandedMemorizedState,
);
const setNavigationMemorizedUrl = useSetAtomState(
navigationMemorizedUrlState,
);
const handleSupport = () => {
window.FrontChat?.('show');
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
};
const handleDocumentation = () => {
window.open(
getDocumentationUrl({ locale: currentWorkspaceMember?.locale }),
'_blank',
);
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
};
const handleChange = async (availableWorkspace: AvailableWorkspace) => {
redirectToWorkspaceDomain(
getWorkspaceUrl(availableWorkspace.workspaceUrls),
@@ -149,6 +153,11 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
text={t`Create Workspace`}
onClick={createWorkspace}
/>
<MenuItem
LeftIcon={IconLogout}
text={t`Log out`}
onClick={signOut}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
@@ -223,6 +232,17 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
>
<MenuItem LeftIcon={IconUserPlus} text={t`Invite user`} />
</UndecoratedLink>
<UndecoratedLink
to={getSettingsPath(SettingsPath.ProfilePage)}
onClick={() => {
setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded);
setIsNavigationDrawerExpanded(true);
setNavigationMemorizedUrl(location.pathname + location.search);
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
}}
>
<MenuItem LeftIcon={IconSettings} text={t`Settings`} />
</UndecoratedLink>
{isSupportChatConfigured && (
<MenuItem
LeftIcon={IconMessage}
@@ -230,12 +250,6 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
onClick={handleSupport}
/>
)}
<MenuItem
LeftIcon={IconHelpCircle}
text={t`Documentation`}
onClick={handleDocumentation}
/>
<MenuItem LeftIcon={IconLogout} text={t`Log out`} onClick={signOut} />
</DropdownMenuItemsContainer>
</DropdownContent>
);
@@ -3,19 +3,22 @@ import { type ReactNode, useState } from 'react';
import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { ResizablePanelEdge } from '@/ui/layout/resizable-panel/components/ResizablePanelEdge';
import { NAVIGATION_DRAWER_COLLAPSED_WIDTH } from '@/ui/layout/resizable-panel/constants/NavigationDrawerCollapsedWidth';
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { NavigationDrawerWidthEffect } from '@/ui/navigation/components/NavigationDrawerWidthEffect';
import { NAVIGATION_DRAWER_CLICK_OUTSIDE_ID } from '@/ui/navigation/navigation-drawer/constants/NavigationDrawerClickOutsideId';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
import { NAVIGATION_DRAWER_TABS } from '@/ui/navigation/states/navigationDrawerTabs';
import {
NAVIGATION_DRAWER_WIDTH_VAR,
navigationDrawerWidthState,
} from '@/ui/navigation/states/navigationDrawerWidthState';
import { NavigationDrawerWidthEffect } from '@/ui/navigation/components/NavigationDrawerWidthEffect';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { NavigationDrawerBackButton } from './NavigationDrawerBackButton';
import { NavigationDrawerHeader } from './NavigationDrawerHeader';
@@ -47,11 +50,13 @@ const StyledAnimatedContainer = styled.div<{
const StyledContainer = styled.div<{
isSettings?: boolean;
isMobile?: boolean;
isExpanded?: boolean;
}>`
box-sizing: border-box;
display: flex;
flex-direction: column;
width: var(${NAVIGATION_DRAWER_WIDTH_VAR});
width: ${({ isExpanded }) =>
isExpanded ? `var(${NAVIGATION_DRAWER_WIDTH_VAR})` : '100%'};
gap: ${({ theme }) => theme.spacing(3)};
height: 100%;
padding: ${({ theme, isSettings, isMobile }) =>
@@ -72,7 +77,6 @@ export const NavigationDrawer = ({
className,
title,
}: NavigationDrawerProps) => {
const [isHovered, setIsHovered] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const isMobile = useIsMobile();
const isSettingsDrawer = useIsSettingsDrawer();
@@ -82,20 +86,16 @@ export const NavigationDrawer = ({
const [navigationDrawerWidth, setNavigationDrawerWidth] = useAtomState(
navigationDrawerWidthState,
);
const setNavigationDrawerActiveTab = useSetAtomState(
navigationDrawerActiveTabState,
);
const setTableWidthResizeIsActive = useSetAtomState(
tableWidthResizeIsActiveState,
);
const handleHover = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const handleCollapse = () => {
setIsNavigationDrawerExpanded(false);
setNavigationDrawerActiveTab(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU);
setIsResizing(false);
setTableWidthResizeIsActive(true);
};
@@ -116,21 +116,20 @@ export const NavigationDrawer = ({
<NavigationDrawerWidthEffect />
<StyledAnimatedContainer
className={className}
data-click-outside-id={NAVIGATION_DRAWER_CLICK_OUTSIDE_ID}
isExpanded={isNavigationDrawerExpanded}
isResizing={isResizing}
>
<StyledContainer
isSettings={isSettingsDrawer}
isMobile={isMobile}
onMouseEnter={handleHover}
onMouseLeave={handleMouseLeave}
isExpanded={isNavigationDrawerExpanded}
>
{isSettingsDrawer && title ? (
!isMobile && <NavigationDrawerBackButton title={title} />
) : (
<NavigationDrawerHeader showCollapseButton={isHovered} />
<NavigationDrawerHeader showCollapseButton />
)}
{children}
</StyledContainer>
@@ -1,4 +1,7 @@
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
import { NAVIGATION_DRAWER_TABS } from '@/ui/navigation/states/navigationDrawerTabs';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import styled from '@emotion/styled';
import {
@@ -26,26 +29,28 @@ export const NavigationDrawerCollapseButton = ({
className,
direction = 'left',
}: NavigationDrawerCollapseButtonProps) => {
const setIsNavigationDrawerExpanded = useSetAtomState(
isNavigationDrawerExpandedState,
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useAtomState(isNavigationDrawerExpandedState);
const setNavigationDrawerActiveTab = useSetAtomState(
navigationDrawerActiveTabState,
);
const handleClick = () => {
if (isNavigationDrawerExpanded) {
setNavigationDrawerActiveTab(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU);
}
setIsNavigationDrawerExpanded((previousIsExpanded) => !previousIsExpanded);
};
return (
<StyledCollapseButton
className={className}
onClick={() =>
setIsNavigationDrawerExpanded(
(previousIsExpanded) => !previousIsExpanded,
)
}
>
<StyledCollapseButton className={className} onClick={handleClick}>
<LightIconButton
Icon={
direction === 'left'
? IconLayoutSidebarLeftCollapse
: IconLayoutSidebarRightCollapse
}
accent="tertiary"
accent="secondary"
size="small"
/>
</StyledCollapseButton>
@@ -1,30 +1,42 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { IconSearch } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/MultiWorkspaceDropdownButton';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/MultiWorkspaceDropdownButton';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
const StyledContainer = styled.div`
const StyledContainer = styled.div<{ isExpanded: boolean }>`
align-items: center;
display: flex;
flex-direction: ${({ isExpanded }) => (isExpanded ? 'row' : 'column')};
gap: ${({ theme, isExpanded }) => (isExpanded ? 0 : theme.spacing(4))};
user-select: none;
padding-right: ${({ theme }) => theme.spacing(2)};
min-height: ${PAGE_BAR_MIN_HEIGHT}px;
transition: gap ${({ theme }) => theme.animation.duration.normal}s ease;
`;
const StyledRightActions = styled.div<{ isExpanded: boolean }>`
align-items: center;
display: flex;
flex-direction: ${({ isExpanded }) => (isExpanded ? 'row' : 'column')};
gap: ${({ theme, isExpanded }) => (isExpanded ? 0 : theme.spacing(1))};
margin-left: ${({ isExpanded }) => (isExpanded ? 'auto' : 0)};
transition: gap ${({ theme }) => theme.animation.duration.normal}s ease;
`;
const StyledNavigationDrawerCollapseButton = styled(
NavigationDrawerCollapseButton,
)<{ show?: boolean }>`
height: ${({ theme }) => theme.spacing(4)};
margin-left: auto;
opacity: ${({ show }) => (show ? 1 : 0)};
)`
height: ${({ theme }) => theme.spacing(6)};
padding-right: ${({ theme }) => theme.spacing(1)};
transition: opacity ${({ theme }) => theme.animation.duration.normal}s;
width: ${({ theme }) => theme.spacing(4)};
width: ${({ theme }) => theme.spacing(6)};
`;
type NavigationDrawerHeaderProps = {
@@ -35,19 +47,27 @@ export const NavigationDrawerHeader = ({
showCollapseButton,
}: NavigationDrawerHeaderProps) => {
const isMobile = useIsMobile();
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
const isNavigationDrawerExpanded = useAtomStateValue(
isNavigationDrawerExpandedState,
);
return (
<StyledContainer>
<StyledContainer isExpanded={isNavigationDrawerExpanded}>
<MultiWorkspaceDropdownButton />
{!isMobile && isNavigationDrawerExpanded && (
<StyledNavigationDrawerCollapseButton
direction="left"
show={showCollapseButton}
/>
{!isMobile && (
<StyledRightActions isExpanded={isNavigationDrawerExpanded}>
<LightIconButton
Icon={IconSearch}
accent="secondary"
size="small"
onClick={openRecordsSearchPage}
aria-label={t`Search`}
/>
{isNavigationDrawerExpanded && showCollapseButton && (
<StyledNavigationDrawerCollapseButton direction="left" />
)}
</StyledRightActions>
)}
</StyledContainer>
);
@@ -51,6 +51,7 @@ export type NavigationDrawerItemProps = {
count?: number;
keyboard?: string[];
rightOptions?: ReactNode;
alwaysShowRightOptions?: boolean;
isDragging?: boolean;
isRightOptionsDropdownOpen?: boolean;
triggerEvent?: TriggerEventType;
@@ -257,6 +258,7 @@ const visibleStateStyles = css`
const StyledRightOptionsVisbility = styled.div<{
isMobile: boolean;
isRightOptionsDropdownOpen?: boolean;
alwaysVisible?: boolean;
}>`
display: block;
opacity: 0;
@@ -269,8 +271,9 @@ const StyledRightOptionsVisbility = styled.div<{
height: 1px;
width: 1px;
${({ isMobile, isRightOptionsDropdownOpen }) =>
(isMobile || isRightOptionsDropdownOpen) && visibleStateStyles}
${({ isMobile, isRightOptionsDropdownOpen, alwaysVisible }) =>
(isMobile || isRightOptionsDropdownOpen || alwaysVisible) &&
visibleStateStyles}
.navigation-drawer-item:hover & {
${visibleStateStyles}
@@ -294,6 +297,7 @@ export const NavigationDrawerItem = ({
keyboard,
subItemState,
rightOptions,
alwaysShowRightOptions = false,
isDragging,
isRightOptionsDropdownOpen,
triggerEvent,
@@ -456,6 +460,7 @@ export const NavigationDrawerItem = ({
}}
>
<StyledRightOptionsVisbility
alwaysVisible={alwaysShowRightOptions}
isMobile={isMobile}
isRightOptionsDropdownOpen={
isRightOptionsDropdownOpen || false
@@ -0,0 +1 @@
export const NAVIGATION_DRAWER_CLICK_OUTSIDE_ID = 'navigation-drawer';
@@ -0,0 +1,12 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import {
type NavigationDrawerActiveTab,
NAVIGATION_DRAWER_TABS,
} from '@/ui/navigation/states/navigationDrawerTabs';
export const navigationDrawerActiveTabState =
createAtomState<NavigationDrawerActiveTab>({
key: 'navigationDrawerActiveTab',
defaultValue: NAVIGATION_DRAWER_TABS.NAVIGATION_MENU,
});
@@ -0,0 +1,7 @@
export const NAVIGATION_DRAWER_TABS = {
NAVIGATION_MENU: 'home',
AI_CHAT_HISTORY: 'chat',
} as const;
export type NavigationDrawerActiveTab =
(typeof NAVIGATION_DRAWER_TABS)[keyof typeof NAVIGATION_DRAWER_TABS];