Files
twenty/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx
T
Abdullah. 4cdb7d61b2 Redesign AI chat and add pre-existing prompts. (#17787)
Redesigned AI-chat based on the following provided design.

<p align="center">
<img
src="https://github.com/user-attachments/assets/f10ebbd2-9ee9-402f-b246-6e8f8cedbd53"
width="225" />
</p>

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-02-09 15:29:32 +01:00

203 lines
6.9 KiB
TypeScript

import { TextArea } from '@/ui/input/components/TextArea';
import styled from '@emotion/styled';
import { IconHistory } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { DropZone } from '@/activities/files/components/DropZone';
import { AgentChatFileUploadButton } from '@/ai/components/internal/AgentChatFileUploadButton';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { AIChatEmptyState } from '@/ai/components/AIChatEmptyState';
import { AIChatMessage } from '@/ai/components/AIChatMessage';
import { AIChatStandaloneError } from '@/ai/components/AIChatStandaloneError';
import { AIChatContextUsageButton } from '@/ai/components/internal/AIChatContextUsageButton';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { AgentChatContextPreview } from '@/ai/components/internal/AgentChatContextPreview';
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
import { AgentMessageRole } from '@/ai/constants/AgentMessageRole';
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
import { AI_CHAT_SCROLL_WRAPPER_ID } from '@/ai/constants/AiChatScrollWrapperId';
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
import { agentChatInputState } from '@/ai/states/agentChatInputState';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
background: ${({ theme }) => theme.background.primary};
height: ${({ isDraggingFile }) =>
isDraggingFile ? `calc(100% - 24px)` : '100%'};
padding: ${({ isDraggingFile, theme }) =>
isDraggingFile ? theme.spacing(3) : '0'};
display: flex;
flex-direction: column;
`;
const StyledInputArea = styled.div`
align-items: flex-end;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
background: ${({ theme }) => theme.background.primary};
`;
const StyledInputBox = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
min-height: 140px;
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
box-sizing: border-box;
&:focus-within {
border-color: ${({ theme }) => theme.color.blue};
box-shadow: 0px 0px 0px 3px ${({ theme }) => theme.color.transparent.blue2};
}
`;
const StyledTextAreaWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
`;
const StyledChatTextArea = styled(TextArea)`
&& {
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
padding: 0;
}
&&:focus {
border: none;
box-shadow: none;
}
`;
const StyledScrollWrapper = styled(ScrollWrapper)`
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(5)};
overflow-y: auto;
padding: ${({ theme }) => theme.spacing(3)};
width: calc(100% - 24px);
`;
const StyledButtonsContainer = styled.div`
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: flex-end;
`;
export const AIChatTab = () => {
const [isDraggingFile, setIsDraggingFile] = useState(false);
const { isLoading, messages, isStreaming, error } =
useAgentChatContextOrThrow();
const [agentChatInput, setAgentChatInput] =
useRecoilState(agentChatInputState);
const { uploadFiles } = useAIChatFileUpload();
const { navigateCommandMenu } = useCommandMenu();
return (
<StyledContainer
isDraggingFile={isDraggingFile}
onDragEnter={() => setIsDraggingFile(true)}
>
{isDraggingFile && (
<DropZone
setIsDraggingFile={setIsDraggingFile}
onUploadFiles={uploadFiles}
/>
)}
{!isDraggingFile && (
<>
{messages.length !== 0 && (
<StyledScrollWrapper
componentInstanceId={AI_CHAT_SCROLL_WRAPPER_ID}
>
{messages.map((message, index) => {
const isLastMessage = index === messages.length - 1;
const isLastMessageStreaming = isStreaming && isLastMessage;
const isLastAssistantMessage =
isLastMessage && message.role === AgentMessageRole.ASSISTANT;
const shouldShowError = error && isLastAssistantMessage;
return (
<AIChatMessage
isLastMessageStreaming={isLastMessageStreaming}
message={message}
key={message.id}
error={shouldShowError ? error : null}
/>
);
})}
{error &&
!isStreaming &&
messages.at(-1)?.role === AgentMessageRole.USER && (
<AIChatStandaloneError error={error} />
)}
</StyledScrollWrapper>
)}
{messages.length === 0 && !error && !isLoading && (
<AIChatEmptyState />
)}
{messages.length === 0 && error && !isLoading && (
<AIChatStandaloneError error={error} />
)}
{isLoading && messages.length === 0 && <AIChatSkeletonLoader />}
<StyledInputArea>
<AgentChatContextPreview />
<StyledInputBox>
<StyledTextAreaWrapper>
<StyledChatTextArea
textAreaId={AI_CHAT_INPUT_ID}
placeholder={t`Ask, search or make anything...`}
value={agentChatInput}
onChange={(value) => setAgentChatInput(value)}
minRows={3}
maxRows={20}
/>
</StyledTextAreaWrapper>
<StyledButtonsContainer>
<AIChatContextUsageButton />
<IconButton
Icon={IconHistory}
variant="tertiary"
size="small"
onClick={() =>
navigateCommandMenu({
page: CommandMenuPages.ViewPreviousAIChats,
pageTitle: t`View Previous AI Chats`,
pageIcon: IconHistory,
})
}
ariaLabel={t`View Previous AI Chats`}
/>
<AgentChatFileUploadButton />
<SendMessageButton />
</StyledButtonsContainer>
</StyledInputBox>
</StyledInputArea>
</>
)}
</StyledContainer>
);
};