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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22582?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Weiko
2026-07-06 17:38:28 +02:00
committed by GitHub
parent 62c7e8f6b4
commit 8580cd6f27
6 changed files with 122 additions and 0 deletions
@@ -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;
};
@@ -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 = () => {
<>
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatPrepromptEffect />
<AgentChatStreamKeepAliveEffect />
<AgentChatSessionStartTimeEffect />
{isAgentChatOpen && (
@@ -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 };
};
@@ -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<AgentChatPreprompt | null>({
key: 'ai/agentChatPrepromptState',
defaultValue: null,
});
@@ -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,
@@ -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;