diff --git a/nx.json b/nx.json index 06481552ec..a1c3bd7046 100644 --- a/nx.json +++ b/nx.json @@ -118,6 +118,7 @@ "outputs": ["{projectRoot}/coverage"], "options": { "jestConfig": "{projectRoot}/jest.config.mjs", + "silent": true, "coverage": true, "coverageReporters": ["text-summary"], "cacheDirectory": "../../.cache/jest/{projectRoot}" diff --git a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx index c128bfbe78..c5e3b52a5b 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx @@ -85,12 +85,15 @@ export const AIChatAssistantMessageRenderer = ({ }) => { // Filter out data-code-execution parts when tool-code_interpreter exists // (the tool part contains the final result, data-code-execution is for streaming updates) + // Also filter out data-thread-title (consumed by useAgentChat, not rendered) const hasCodeInterpreterTool = messageParts.some( (part) => part.type === 'tool-code_interpreter', ); - const filteredParts = hasCodeInterpreterTool - ? messageParts.filter((part) => part.type !== 'data-code-execution') - : messageParts; + const filteredParts = messageParts.filter( + (part) => + part.type !== 'data-thread-title' && + (!hasCodeInterpreterTool || part.type !== 'data-code-execution'), + ); const renderItems = groupContiguousThinkingStepParts(filteredParts); if (!renderItems.length && !hasError) { diff --git a/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx b/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx index 31ecd72740..7febd2a0c2 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx @@ -1,5 +1,6 @@ import { agentChatUsageState } from '@/ai/states/agentChatUsageState'; import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; +import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -78,11 +79,15 @@ export const AIChatThreadGroup = ({ const { t } = useLingui(); const theme = useTheme(); const [, setCurrentAIChatThread] = useRecoilState(currentAIChatThreadState); + const setCurrentAIChatThreadTitle = useSetRecoilState( + currentAIChatThreadTitleState, + ); const setAgentChatUsage = useSetRecoilState(agentChatUsageState); const { openAskAIPage } = useOpenAskAIPageInCommandMenu(); const handleThreadClick = (thread: AgentChatThread) => { setCurrentAIChatThread(thread.id); + setCurrentAIChatThreadTitle(thread.title ?? null); const hasUsageData = (thread.conversationSize ?? 0) > 0 && @@ -103,7 +108,6 @@ export const AIChatThreadGroup = ({ ); openAskAIPage({ - pageTitle: thread.title, resetNavigationStack: false, }); }; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts index 13112ecdc5..d23bb721e2 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts @@ -1,12 +1,12 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext'; +import { agentChatInputState } from '@/ai/states/agentChatInputState'; import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState'; import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState'; import { agentChatUsageState } from '@/ai/states/agentChatUsageState'; import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; - -import { agentChatInputState } from '@/ai/states/agentChatInputState'; +import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url'; import { getTokenPair } from '@/apollo/utils/getTokenPair'; import { renewToken } from '@/auth/services/AuthService'; @@ -23,6 +23,9 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => { const setAgentChatUsage = useSetRecoilState(agentChatUsageState); const { getBrowsingContext } = useGetBrowsingContext(); + const setCurrentAIChatThreadTitle = useSetRecoilState( + currentAIChatThreadTitleState, + ); const agentChatSelectedFiles = useRecoilValue(agentChatSelectedFilesState); @@ -150,6 +153,14 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => { outputCredits: (prev?.outputCredits ?? 0) + usage.outputCredits, })); } + + const titlePart = message.parts.find( + (part) => part.type === 'data-thread-title', + ); + + if (isDefined(titlePart) && titlePart.type === 'data-thread-title') { + setCurrentAIChatThreadTitle(titlePart.data.title); + } }, }); diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts index dee5e67582..efaefdd39d 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 { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; import { isCreatingChatThreadState } from '@/ai/states/isCreatingChatThreadState'; import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages'; import { @@ -46,6 +47,9 @@ export const useAgentChatData = () => { currentAIChatThreadState, ); const setAgentChatUsage = useSetRecoilState(agentChatUsageState); + const setCurrentAIChatThreadTitle = useSetRecoilState( + currentAIChatThreadTitleState, + ); const [isCreatingChatThread, setIsCreatingChatThread] = useRecoilState( isCreatingChatThreadState, ); @@ -56,6 +60,7 @@ export const useAgentChatData = () => { onCompleted: (data) => { setIsCreatingChatThread(false); setCurrentAIChatThread(data.createChatThread.id); + setCurrentAIChatThreadTitle(null); setAgentChatUsage(null); }, onError: () => { @@ -70,6 +75,7 @@ export const useAgentChatData = () => { const firstThread = data.chatThreads[0]; setCurrentAIChatThread(firstThread.id); + setCurrentAIChatThreadTitle(firstThread.title ?? null); setUsageFromThread(firstThread, setAgentChatUsage); } else if (!isCreatingChatThread) { setIsCreatingChatThread(true); diff --git a/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts b/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts index a872e5f98d..15415c9ebb 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts @@ -1,5 +1,6 @@ import { agentChatUsageState } from '@/ai/states/agentChatUsageState'; import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; +import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { useCreateChatThreadMutation } from '~/generated-metadata/graphql'; @@ -7,11 +8,15 @@ import { useCreateChatThreadMutation } from '~/generated-metadata/graphql'; export const useCreateNewAIChatThread = () => { const [, setCurrentAIChatThread] = useRecoilState(currentAIChatThreadState); const setAgentChatUsage = useSetRecoilState(agentChatUsageState); + const setCurrentAIChatThreadTitle = useSetRecoilState( + currentAIChatThreadTitleState, + ); const { openAskAIPage } = useOpenAskAIPageInCommandMenu(); const [createChatThread] = useCreateChatThreadMutation({ onCompleted: (data) => { setCurrentAIChatThread(data.createChatThread.id); + setCurrentAIChatThreadTitle(null); setAgentChatUsage(null); openAskAIPage({ resetNavigationStack: false }); }, diff --git a/packages/twenty-front/src/modules/ai/states/currentAIChatThreadTitleState.ts b/packages/twenty-front/src/modules/ai/states/currentAIChatThreadTitleState.ts new file mode 100644 index 0000000000..af58c57ed1 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/states/currentAIChatThreadTitleState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const currentAIChatThreadTitleState = atom({ + key: 'ai/currentAIChatThreadTitleState', + default: null, +}); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuAskAIInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuAskAIInfo.tsx new file mode 100644 index 0000000000..43b1d5ef45 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuAskAIInfo.tsx @@ -0,0 +1,25 @@ +import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useRecoilValue } from 'recoil'; +import { OverflowingTextWithTooltip } from 'twenty-ui/display'; + +const StyledPageTitle = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; +`; + +export const CommandMenuAskAIInfo = () => { + const currentAIChatThreadTitle = useRecoilValue( + currentAIChatThreadTitleState, + ); + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx index a446b32850..2acf46d629 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { OverflowingTextWithTooltip } from 'twenty-ui/display'; +import { CommandMenuAskAIInfo } from '@/command-menu/components/CommandMenuAskAIInfo'; import { CommandMenuFolderInfo } from '@/command-menu/components/CommandMenuFolderInfo'; import { CommandMenuLinkInfo } from '@/command-menu/components/CommandMenuLinkInfo'; import { CommandMenuMultipleRecordsInfo } from '@/command-menu/components/CommandMenuMultipleRecordsInfo'; @@ -114,6 +115,12 @@ export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => { ); } + const isAskAIPage = pageChip.page?.page === CommandMenuPages.AskAI; + + if (isAskAIPage) { + return ; + } + return ( diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenAskAIPageInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenAskAIPageInCommandMenu.test.tsx new file mode 100644 index 0000000000..08633d48ce --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenAskAIPageInCommandMenu.test.tsx @@ -0,0 +1,81 @@ +import { renderHook, act } from '@testing-library/react'; +import { type MutableSnapshot, RecoilRoot } from 'recoil'; + +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu'; +import { IconSparkles } from 'twenty-ui/display'; + +const navigateCommandMenuMock = jest.fn(); + +jest.mock('@/command-menu/hooks/useCommandMenu', () => ({ + useCommandMenu: () => ({ + navigateCommandMenu: navigateCommandMenuMock, + openCommandMenu: jest.fn(), + closeCommandMenu: jest.fn(), + toggleCommandMenu: jest.fn(), + }), +})); + +const renderWithRecoil = ( + initializeState?: (snapshot: MutableSnapshot) => void, +) => + renderHook(() => useOpenAskAIPageInCommandMenu(), { + wrapper: ({ children }) => ( + {children} + ), + }); + +describe('useOpenAskAIPageInCommandMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should navigate to AskAI page with correct defaults', () => { + const { result } = renderWithRecoil(); + + act(() => { + result.current.openAskAIPage(); + }); + + expect(navigateCommandMenuMock).toHaveBeenCalledWith( + expect.objectContaining({ + page: CommandMenuPages.AskAI, + pageTitle: 'Ask AI', + pageIcon: IconSparkles, + }), + ); + }); + + it('should use resetNavigationStack from argument when provided', () => { + const { result } = renderWithRecoil((snapshot) => { + snapshot.set(isCommandMenuOpenedState, true); + }); + + act(() => { + result.current.openAskAIPage({ resetNavigationStack: false }); + }); + + expect(navigateCommandMenuMock).toHaveBeenCalledWith( + expect.objectContaining({ + resetNavigationStack: false, + }), + ); + }); + + it('should default resetNavigationStack to isCommandMenuOpened', () => { + const { result } = renderWithRecoil((snapshot) => { + snapshot.set(isCommandMenuOpenedState, true); + }); + + act(() => { + result.current.openAskAIPage(); + }); + + expect(navigateCommandMenuMock).toHaveBeenCalledWith( + expect.objectContaining({ + resetNavigationStack: true, + }), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts index bb16ee47b1..de0d653ea7 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts @@ -2,34 +2,39 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { t } from '@lingui/core/macro'; -import { useRecoilValue } from 'recoil'; +import { useRecoilCallback } from 'recoil'; import { IconSparkles } from 'twenty-ui/display'; import { v4 } from 'uuid'; export const useOpenAskAIPageInCommandMenu = () => { const { navigateCommandMenu } = useCommandMenu(); - const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); - const openAskAIPage = ({ - pageTitle, - resetNavigationStack, - }: { - pageTitle?: string | null; - resetNavigationStack?: boolean; - } = {}) => { - const shouldReset = - resetNavigationStack !== undefined - ? resetNavigationStack - : isCommandMenuOpened; + const openAskAIPage = useRecoilCallback( + ({ snapshot }) => + ({ + resetNavigationStack, + }: { + resetNavigationStack?: boolean; + } = {}) => { + const isCommandMenuOpened = snapshot + .getLoadable(isCommandMenuOpenedState) + .getValue(); - navigateCommandMenu({ - page: CommandMenuPages.AskAI, - pageTitle: pageTitle ?? t`Ask AI`, - pageIcon: IconSparkles, - pageId: v4(), - resetNavigationStack: shouldReset, - }); - }; + const shouldReset = + resetNavigationStack !== undefined + ? resetNavigationStack + : isCommandMenuOpened; + + navigateCommandMenu({ + page: CommandMenuPages.AskAI, + pageTitle: t`Ask AI`, + pageIcon: IconSparkles, + pageId: v4(), + resetNavigationStack: shouldReset, + }); + }, + [navigateCommandMenu], + ); return { openAskAIPage, diff --git a/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts b/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts index 4cb8bdea44..e467fc5727 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts @@ -86,6 +86,6 @@ describe('processSingleDrag', () => { recordsWithPosition: [], isDroppedAfterList: false, }), - ).toThrowError('Cannot find item to move for id : record-1'); + ).toThrow('Cannot find item to move for id : record-1'); }); }); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx index 68d5f4a719..f931075098 100644 --- a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx @@ -80,15 +80,13 @@ describe('useCreateSSOIdentityProvider', () => { const OTHERParams = { type: 'OTHER' as const, }; - renderHook( - async () => { - const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); - await expect( - // @ts-expect-error - It's expected to throw an error - createSSOIdentityProvider(OTHERParams), - ).rejects.toThrowError(); - }, - { wrapper: Wrapper }, - ); + const { result } = renderHook(() => useCreateSSOIdentityProvider(), { + wrapper: Wrapper, + }); + + await expect( + // @ts-expect-error - It's expected to throw an error + result.current.createSSOIdentityProvider(OTHERParams), + ).rejects.toThrow('Invalid IdpType'); }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts index 11904a54fd..39454ac411 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getCronTriggerDefaultSettings.test.ts @@ -40,7 +40,7 @@ describe('getCronTriggerDefaultSettings', () => { it('throws an error for an invalid interval', () => { // @ts-expect-error Testing invalid input - expect(() => getCronTriggerDefaultSettings('INVALID')).toThrowError( + expect(() => getCronTriggerDefaultSettings('INVALID')).toThrow( 'Invalid cron trigger interval', ); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getWebhookTriggerDefaultSettings.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getWebhookTriggerDefaultSettings.test.ts index a19c942efb..8246c55c3c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getWebhookTriggerDefaultSettings.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getWebhookTriggerDefaultSettings.test.ts @@ -32,7 +32,7 @@ describe('getWebhookTriggerDefaultSettings', () => { it('throws an error for an invalid http method', () => { // @ts-expect-error Testing invalid input - expect(() => getWebhookTriggerDefaultSettings('INVALID')).toThrowError( + expect(() => getWebhookTriggerDefaultSettings('INVALID')).toThrow( 'Invalid webhook http method', ); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts index d9074746f6..339e2b9ac3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts @@ -66,6 +66,9 @@ export const mapUIMessagePartsToDBParts = ( // Code execution parts are streamed during execution but don't need // to be persisted - the final result is captured in the tool part return null; + case 'data-thread-title': + // Thread title is a transient notification for the client + return null; default: { if (isToolPart(part)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts index 4e36d43444..a460e9d2b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts @@ -82,6 +82,14 @@ export class AgentChatStreamingService { // surfaces when awaited in onFinish. userMessagePromise.catch(() => {}); + // Title generation runs in parallel with AI streaming so it's + // typically ready by the time onFinish fires + const titlePromise = thread.title + ? Promise.resolve(null) + : this.agentChatService + .generateTitleIfNeeded(thread.id, lastUserText) + .catch(() => null); + try { const uiStream = createUIMessageStream({ execute: async ({ writer }) => { @@ -214,6 +222,16 @@ export class AgentChatStreamingService { contextWindowTokens: modelConfig.contextWindowTokens, conversationSize: lastStepConversationSize, }); + + const generatedTitle = await titlePromise; + + if (generatedTitle) { + writer.write({ + type: 'data-thread-title' as const, + id: `thread-title-${thread.id}`, + data: { title: generatedTitle }, + }); + } }, sendReasoning: true, }), diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts index 758b17b1ab..92e409047a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts @@ -113,16 +113,6 @@ export class AgentChatService { await this.messagePartRepository.save(dbParts); } - if (uiMessage.role === AgentMessageRole.USER) { - const messageContent = uiMessage.parts.find( - (part) => part.type === 'text', - )?.text; - - if (messageContent) { - this.generateTitleIfNeeded(threadId, messageContent); - } - } - return savedMessage; } @@ -148,22 +138,24 @@ export class AgentChatService { }); } - private async generateTitleIfNeeded( + async generateTitleIfNeeded( threadId: string, messageContent: string, - ) { + ): Promise { const thread = await this.threadRepository.findOne({ where: { id: threadId }, select: ['id', 'title'], }); if (!thread || thread.title || !messageContent) { - return; + return null; } const title = await this.titleGenerationService.generateThreadTitle(messageContent); await this.threadRepository.update(threadId, { title }); + + return title; } } diff --git a/packages/twenty-shared/src/ai/types/DataMessagePart.ts b/packages/twenty-shared/src/ai/types/DataMessagePart.ts index 360d2666ca..d8ca40444b 100644 --- a/packages/twenty-shared/src/ai/types/DataMessagePart.ts +++ b/packages/twenty-shared/src/ai/types/DataMessagePart.ts @@ -65,4 +65,5 @@ export type DataMessagePart = { }; }; 'code-execution': CodeExecutionData; + 'thread-title': { title: string }; };