a4ed561e11
## Summary - Add side-panel-owned Escape and Backspace behavior for the side-panel search input. - Keep side-panel Escape scoped to side-panel focus and avoid left-content fallback behavior. - Add a Side Panel group to the keyboard shortcut menu. - Reuse the side-panel focus id for AI chat thread-list shortcuts. ## Demo https://github.com/user-attachments/assets/80a632d6-7ff7-496b-905f-a3f95f9cfc14 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
|
|
import { AiChatThreadDeleteConfirmationModal } from '@/ai/components/AiChatThreadDeleteConfirmationModal';
|
|
import { AiChatThreadFilterDropdown } from '@/ai/components/AiChatThreadFilterDropdown';
|
|
import { AiChatThreadGroup } from '@/ai/components/AiChatThreadGroup';
|
|
import { AiChatThreadListItem } from '@/ai/components/AiChatThreadListItem';
|
|
import { AiChatThreadsListFocusEffect } from '@/ai/components/AiChatThreadsListFocusEffect';
|
|
import { AiChatSkeletonLoader } from '@/ai/components/internal/AiChatSkeletonLoader';
|
|
import { AGENT_CHAT_THREAD_GROUP_BY } from '@/ai/constants/AgentChatThreadGroupBy';
|
|
import { AI_CHAT_THREADS_LIST_FOCUS_ID } from '@/ai/constants/AiChatThreadsListFocusId';
|
|
import { AI_CHAT_THREAD_ACTIONS_SURFACE } from '@/ai/constants/AiChatThreadActionsSurface';
|
|
import { useChatThreads } from '@/ai/hooks/useChatThreads';
|
|
import { useSwitchToNewAiChat } from '@/ai/hooks/useSwitchToNewAiChat';
|
|
import { agentChatThreadGroupByState } from '@/ai/states/agentChatThreadGroupByState';
|
|
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
|
|
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
|
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { t } from '@lingui/core/macro';
|
|
import { Key } from 'ts-key-enum';
|
|
import { Button } from 'twenty-ui/input';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
|
|
|
const StyledContainer = styled.div`
|
|
background: ${themeCssVariables.background.secondary};
|
|
border-right: 1px solid ${themeCssVariables.border.color.light};
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
`;
|
|
|
|
const StyledThreadsContainer = styled.div`
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
const StyledFlatThreadList = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const StyledButtonsContainer = styled.div`
|
|
border-top: 1px solid ${themeCssVariables.border.color.medium};
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
export const AiChatThreadsList = () => {
|
|
const { switchToNewChat } = useSwitchToNewAiChat();
|
|
|
|
useHotkeysOnFocusedElement({
|
|
keys: [`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
|
|
callback: () => switchToNewChat(),
|
|
focusId: AI_CHAT_THREADS_LIST_FOCUS_ID,
|
|
dependencies: [switchToNewChat],
|
|
});
|
|
|
|
const { threads, hasNextPage, loading, fetchMoreRef } = useChatThreads();
|
|
const agentChatThreadGroupBy = useAtomStateValue(agentChatThreadGroupByState);
|
|
|
|
if (loading && threads.length === 0) {
|
|
return <AiChatSkeletonLoader />;
|
|
}
|
|
|
|
const isGroupedByDate =
|
|
agentChatThreadGroupBy === AGENT_CHAT_THREAD_GROUP_BY.DATE;
|
|
const dateGroups = isGroupedByDate ? groupThreadsByDate(threads) : [];
|
|
const shouldRenderDateGroups = isGroupedByDate && dateGroups.length > 0;
|
|
const filterDropdown = (
|
|
<AiChatThreadFilterDropdown
|
|
surface={AI_CHAT_THREAD_ACTIONS_SURFACE.SIDE_PANEL}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<AiChatThreadsListFocusEffect focusId={AI_CHAT_THREADS_LIST_FOCUS_ID} />
|
|
<StyledContainer>
|
|
<StyledThreadsContainer>
|
|
{shouldRenderDateGroups ? (
|
|
dateGroups.map((dateGroup, index) => (
|
|
<AiChatThreadGroup
|
|
key={dateGroup.id}
|
|
title={dateGroup.title}
|
|
threads={dateGroup.threads}
|
|
rightIcon={index === 0 ? filterDropdown : undefined}
|
|
alwaysShowRightIcon={index === 0}
|
|
/>
|
|
))
|
|
) : (
|
|
<>
|
|
<NavigationDrawerSectionTitle
|
|
label={t`Recents`}
|
|
alwaysShowRightIcon
|
|
rightIcon={filterDropdown}
|
|
/>
|
|
<StyledFlatThreadList>
|
|
{threads.map((thread) => (
|
|
<AiChatThreadListItem key={thread.id} thread={thread} />
|
|
))}
|
|
</StyledFlatThreadList>
|
|
</>
|
|
)}
|
|
{hasNextPage ? (
|
|
<div ref={fetchMoreRef} style={{ minHeight: 1 }} />
|
|
) : null}
|
|
</StyledThreadsContainer>
|
|
<StyledButtonsContainer>
|
|
<Button
|
|
variant="primary"
|
|
accent="blue"
|
|
size="medium"
|
|
title={t`New chat`}
|
|
onClick={() => switchToNewChat()}
|
|
hotkeys={[getOsControlSymbol(), '⏎']}
|
|
/>
|
|
</StyledButtonsContainer>
|
|
</StyledContainer>
|
|
<AiChatThreadDeleteConfirmationModal
|
|
surface={AI_CHAT_THREAD_ACTIONS_SURFACE.SIDE_PANEL}
|
|
/>
|
|
</>
|
|
);
|
|
};
|