From 8580cd6f27ec7ec67b8f6d787ec4aaa76d0bbd16 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 6 Jul 2026 17:38:28 +0200 Subject: [PATCH] feat(ai): open Ask AI side panel with a preprompt in two modes (#22582) Add the ability to open the Ask AI side panel pre-filled with a prompt from any frontend component, with a mode to control whether the message is sent automatically or left for the user to review. - agentChatPrepromptState: holds the pending preprompt and its mode (PREFILL = fill only, SEND = fill and auto-submit) - useOpenAskAiPageWithPreprompt: seeds the new-thread draft, opens a fresh Ask AI thread and stores the preprompt intent - AgentChatPrepromptEffect: applies the intent once the chat editor and send listener are mounted, either restoring the editor content or dispatching the send event and clearing the editor Review in cubic --- .../components/AgentChatPrepromptEffect.tsx | 41 +++++++++++++++++++ .../ai/components/AgentChatRuntimeEffects.tsx | 2 + .../ai/hooks/useOpenAskAiPageWithPreprompt.ts | 35 ++++++++++++++++ .../ai/states/agentChatPrepromptState.ts | 14 +++++++ .../useFrontComponentExecutionContext.ts | 19 +++++++++ .../frontComponentHostCommunicationApi.ts | 11 +++++ 6 files changed, 122 insertions(+) create mode 100644 packages/twenty-front/src/modules/ai/components/AgentChatPrepromptEffect.tsx create mode 100644 packages/twenty-front/src/modules/ai/hooks/useOpenAskAiPageWithPreprompt.ts create mode 100644 packages/twenty-front/src/modules/ai/states/agentChatPrepromptState.ts diff --git a/packages/twenty-front/src/modules/ai/components/AgentChatPrepromptEffect.tsx b/packages/twenty-front/src/modules/ai/components/AgentChatPrepromptEffect.tsx new file mode 100644 index 0000000000..88060d153d --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/AgentChatPrepromptEffect.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +import { AGENT_CHAT_RESTORE_EDITOR_CONTENT_EVENT_NAME } from '@/ai/constants/AgentChatRestoreEditorContentEventName'; +import { agentChatPrepromptState } from '@/ai/states/agentChatPrepromptState'; +import { dispatchAgentChatSendMessageEvent } from '@/ai/utils/dispatchAgentChatSendMessageEvent'; +import { dispatchBrowserEvent } from '@/browser-event/utils/dispatchBrowserEvent'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; + +export const AgentChatPrepromptEffect = () => { + const [agentChatPreprompt, setAgentChatPreprompt] = useAtomState( + agentChatPrepromptState, + ); + + useEffect(() => { + if (!isDefined(agentChatPreprompt)) { + return; + } + + const { text, mode } = agentChatPreprompt; + + const timeoutId = setTimeout(() => { + if (mode === 'SEND') { + dispatchAgentChatSendMessageEvent(); + dispatchBrowserEvent(AGENT_CHAT_RESTORE_EDITOR_CONTENT_EVENT_NAME, { + content: '', + }); + } else { + dispatchBrowserEvent(AGENT_CHAT_RESTORE_EDITOR_CONTENT_EVENT_NAME, { + content: text, + }); + } + + setAgentChatPreprompt(null); + }, 0); + + return () => clearTimeout(timeoutId); + }, [agentChatPreprompt, setAgentChatPreprompt]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/ai/components/AgentChatRuntimeEffects.tsx b/packages/twenty-front/src/modules/ai/components/AgentChatRuntimeEffects.tsx index dc351447c1..a74d632ef9 100644 --- a/packages/twenty-front/src/modules/ai/components/AgentChatRuntimeEffects.tsx +++ b/packages/twenty-front/src/modules/ai/components/AgentChatRuntimeEffects.tsx @@ -1,4 +1,5 @@ import { AgentChatMessagesFetchEffect } from '@/ai/components/AgentChatMessagesFetchEffect'; +import { AgentChatPrepromptEffect } from '@/ai/components/AgentChatPrepromptEffect'; import { AgentChatSessionStartTimeEffect } from '@/ai/components/AgentChatSessionStartTimeEffect'; import { AgentChatStreamKeepAliveEffect } from '@/ai/components/AgentChatStreamKeepAliveEffect'; import { AgentChatStreamSubscriptionEffect } from '@/ai/components/AgentChatStreamSubscriptionEffect'; @@ -37,6 +38,7 @@ export const AgentChatRuntimeEffects = () => { <> + {isAgentChatOpen && ( diff --git a/packages/twenty-front/src/modules/ai/hooks/useOpenAskAiPageWithPreprompt.ts b/packages/twenty-front/src/modules/ai/hooks/useOpenAskAiPageWithPreprompt.ts new file mode 100644 index 0000000000..1d0ed5790e --- /dev/null +++ b/packages/twenty-front/src/modules/ai/hooks/useOpenAskAiPageWithPreprompt.ts @@ -0,0 +1,35 @@ +import { useSwitchToNewAiChat } from '@/ai/hooks/useSwitchToNewAiChat'; +import { + AGENT_CHAT_NEW_THREAD_DRAFT_KEY, + agentChatDraftsByThreadIdState, +} from '@/ai/states/agentChatDraftsByThreadIdState'; +import { + type AgentChatPrepromptMode, + agentChatPrepromptState, +} from '@/ai/states/agentChatPrepromptState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; + +export const useOpenAskAiPageWithPreprompt = () => { + const { switchToNewChat } = useSwitchToNewAiChat(); + const setAgentChatDraftsByThreadId = useSetAtomState( + agentChatDraftsByThreadIdState, + ); + const setAgentChatPreprompt = useSetAtomState(agentChatPrepromptState); + + const openAskAiPageWithPreprompt = ({ + text, + mode = 'PREFILL', + }: { + text: string; + mode?: AgentChatPrepromptMode; + }) => { + switchToNewChat(); + setAgentChatDraftsByThreadId((prev) => ({ + ...prev, + [AGENT_CHAT_NEW_THREAD_DRAFT_KEY]: text, + })); + setAgentChatPreprompt({ text, mode }); + }; + + return { openAskAiPageWithPreprompt }; +}; diff --git a/packages/twenty-front/src/modules/ai/states/agentChatPrepromptState.ts b/packages/twenty-front/src/modules/ai/states/agentChatPrepromptState.ts new file mode 100644 index 0000000000..8edfa3297a --- /dev/null +++ b/packages/twenty-front/src/modules/ai/states/agentChatPrepromptState.ts @@ -0,0 +1,14 @@ +import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; + +export type AgentChatPrepromptMode = 'PREFILL' | 'SEND'; + +export type AgentChatPreprompt = { + text: string; + mode: AgentChatPrepromptMode; +}; + +export const agentChatPrepromptState = + createAtomState({ + key: 'ai/agentChatPrepromptState', + defaultValue: null, + }); diff --git a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts index 56c3712ad5..8b2c4fdb76 100644 --- a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts +++ b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts @@ -13,6 +13,7 @@ import { } from 'twenty-shared/types'; import { type AppLocale } from 'twenty-shared/translations'; +import { useOpenAskAiPageWithPreprompt } from '@/ai/hooks/useOpenAskAiPageWithPreprompt'; import { currentUserState } from '@/auth/states/currentUserState'; import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal'; import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand'; @@ -63,6 +64,7 @@ export const useFrontComponentExecutionContext = ({ frontComponentId, }); const { openConfirmationModal } = useCommandMenuConfirmationModal(); + const { openAskAiPageWithPreprompt } = useOpenAskAiPageWithPreprompt(); const { navigateSidePanel } = useNavigateSidePanel(); const { openRecordInSidePanel: openRecordInSidePanelInternal } = useOpenRecordInSidePanel(); @@ -193,6 +195,23 @@ export const useFrontComponentExecutionContext = ({ return; } + if ( + params.page === SidePanelPages.AskAI && + isDefined(params.preprompt) && + isNonEmptyString(params.preprompt.text) + ) { + openAskAiPageWithPreprompt({ + text: params.preprompt.text, + mode: params.preprompt.mode, + }); + + if (params.shouldResetSearchState === true) { + setSidePanelSearch(''); + } + + return; + } + navigateSidePanel({ page: params.page, pageTitle: params.pageTitle, diff --git a/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts b/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts index 7472752e00..15189a50a2 100644 --- a/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts +++ b/packages/twenty-sdk/src/sdk/front-component/globals/frontComponentHostCommunicationApi.ts @@ -45,6 +45,16 @@ export type OpenSidePanelPageParams = pageIcon?: string; resetNavigationStack?: boolean; } + | { + page: SidePanelPages.AskAI; + pageTitle: string; + pageIcon?: string; + shouldResetSearchState?: boolean; + preprompt?: { + text: string; + mode?: 'PREFILL' | 'SEND'; + }; + } | { page: Exclude< SidePanelPages, @@ -52,6 +62,7 @@ export type OpenSidePanelPageParams = | SidePanelPages.EditRichText | SidePanelPages.ComposeEmail | SidePanelPages.ViewFrontComponent + | SidePanelPages.AskAI >; pageTitle: string; pageIcon?: string;