From 81eaee81a8f3f72647dda79de74c827310dddc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 28 Jan 2026 17:36:36 +0100 Subject: [PATCH] Fix AI chat infinite loading shimmer on empty workspace (#17521) ## Summary Fixes an issue where the AI chat would show loading shimmers indefinitely when opened on a workspace with no conversation history. **Root cause:** The `isLoading` state in `useAgentChat` included `!currentAIChatThread`. On workspaces with no chat threads, `currentAIChatThread` remained `null`, causing `isLoading` to be permanently `true`. **Changes:** - Remove `!currentAIChatThread` from `isLoading` calculation in `useAgentChat` - this state should only reflect streaming/file selection status - Auto-create a chat thread in `useAgentChatData` when the threads query returns empty, ensuring a valid thread exists for the `useChat` hook to initialize properly - Add primary font color to empty state title for better visibility ## Test plan 1. Create a new workspace or use a workspace with no AI chat history 2. Open the AI chat 3. Verify the empty state shows (not infinite loading shimmer) 4. Send a message and verify it works correctly Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor --- .../ai/components/AIChatEmptyState.tsx | 1 + .../ai/components/LazyMarkdownRenderer.tsx | 26 +++++++++++++++++-- .../src/modules/ai/hooks/useAgentChat.ts | 5 ++-- .../src/modules/ai/hooks/useAgentChatData.ts | 19 ++++++++++++++ .../ai/states/isCreatingChatThreadState.ts | 6 +++++ 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/states/isCreatingChatThreadState.ts diff --git a/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx b/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx index 87f51f4aaa..db1d63e7e7 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx @@ -24,6 +24,7 @@ const StyledSparkleIcon = styled.div` const StyledTitle = styled.div` font-size: ${({ theme }) => theme.font.size.lg}; + color: ${({ theme }) => theme.font.color.primary}; font-weight: 600; `; diff --git a/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx b/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx index 44be49ea8c..dac9beb99d 100644 --- a/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/LazyMarkdownRenderer.tsx @@ -72,9 +72,11 @@ const MarkdownRenderer = lazy(async () => { default: ({ children, TableScrollContainer, + StyledParagraph, }: { children: string; TableScrollContainer: React.ComponentType<{ children: React.ReactNode }>; + StyledParagraph: React.ComponentType<{ children: React.ReactNode }>; }) => ( { {children}
), - p: ({ children }) =>

{processChildrenForRecordLinks(children)}

, + p: ({ children }) => ( + + {processChildrenForRecordLinks(children)} + + ), li: ({ children }) => (
  • {processChildrenForRecordLinks(children)}
  • ), @@ -116,6 +122,19 @@ const StyledTableScrollContainer = styled.div` } `; +// Using div instead of p to allow RecordLink (which contains div elements) as children +const StyledParagraph = styled.div` + margin-block: 1em; + + &:first-child { + margin-block-start: 0; + } + + &:last-child { + margin-block-end: 0; + } +`; + const StyledSkeletonContainer = styled.div` display: flex; flex-direction: column; @@ -161,7 +180,10 @@ const LoadingSkeleton = () => { export const LazyMarkdownRenderer = ({ text }: { text: string }) => { return ( }> - + {text} diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts index 105abc3c0a..197dba7a65 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts @@ -147,11 +147,10 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => { const isStreaming = status === 'streaming'; - const isLoading = - !currentAIChatThread || isStreaming || agentChatSelectedFiles.length > 0; + const isLoading = isStreaming || agentChatSelectedFiles.length > 0; const handleSendMessage = async () => { - if (agentChatInput.trim() === '' || isLoading === true) { + if (agentChatInput.trim() === '' || isLoading || !currentAIChatThread) { return; } diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts index 6daf814b6e..046e8154d4 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts @@ -4,6 +4,7 @@ import { type AgentChatUsageState, } from '@/ai/states/agentChatUsageState'; import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; +import { isCreatingChatThreadState } from '@/ai/states/isCreatingChatThreadState'; import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages'; import { type SetterOrUpdater, @@ -13,6 +14,7 @@ import { import { isDefined } from 'twenty-shared/utils'; import { type AgentChatThread, + useCreateChatThreadMutation, useGetChatMessagesQuery, useGetChatThreadsQuery, } from '~/generated-metadata/graphql'; @@ -43,9 +45,23 @@ export const useAgentChatData = () => { currentAIChatThreadState, ); const setAgentChatUsage = useSetRecoilState(agentChatUsageState); + const [isCreatingChatThread, setIsCreatingChatThread] = useRecoilState( + isCreatingChatThreadState, + ); const { scrollToBottom } = useAgentChatScrollToBottom(); + const [createChatThread] = useCreateChatThreadMutation({ + onCompleted: (data) => { + setIsCreatingChatThread(false); + setCurrentAIChatThread(data.createChatThread.id); + setAgentChatUsage(null); + }, + onError: () => { + setIsCreatingChatThread(false); + }, + }); + const { loading: threadsLoading } = useGetChatThreadsQuery({ skip: isDefined(currentAIChatThread), onCompleted: (data) => { @@ -54,6 +70,9 @@ export const useAgentChatData = () => { setCurrentAIChatThread(firstThread.id); setUsageFromThread(firstThread, setAgentChatUsage); + } else if (!isCreatingChatThread) { + setIsCreatingChatThread(true); + createChatThread(); } }, }); diff --git a/packages/twenty-front/src/modules/ai/states/isCreatingChatThreadState.ts b/packages/twenty-front/src/modules/ai/states/isCreatingChatThreadState.ts new file mode 100644 index 0000000000..1005e99b04 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/states/isCreatingChatThreadState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isCreatingChatThreadState = atom({ + key: 'ai/isCreatingChatThreadState', + default: false, +});