diff --git a/front/components/assistant/conversation/Conversation.tsx b/front/components/assistant/conversation/Conversation.tsx index 8dc262181510..0ea13413ebee 100644 --- a/front/components/assistant/conversation/Conversation.tsx +++ b/front/components/assistant/conversation/Conversation.tsx @@ -10,17 +10,24 @@ import { } from "@app/lib/api/assistant/conversation"; import { useConversation, useConversations } from "@app/lib/swr"; import { + AgentMention, AgentMessageType, + isAgentMention, + isUserMessageType, UserMessageType, } from "@app/types/assistant/conversation"; -import { WorkspaceType } from "@app/types/user"; +import { UserType, WorkspaceType } from "@app/types/user"; export default function Conversation({ owner, + user, conversationId, + onStickyMentionsChange, }: { owner: WorkspaceType; + user: UserType; conversationId: string; + onStickyMentionsChange?: (mentions: AgentMention[]) => void; }) { const { conversation, @@ -42,6 +49,41 @@ export default function Conversation({ } }, [conversation?.content.length]); + useEffect(() => { + if (!onStickyMentionsChange) { + return; + } + const lastUserMessageContent = conversation?.content.findLast( + (versionedMessages) => + versionedMessages.some( + (message) => + isUserMessageType(message) && + message.visibility !== "deleted" && + message.user?.id === user.id + ) + ); + + if (!lastUserMessageContent) { + return; + } + + const lastUserMessage = + lastUserMessageContent[lastUserMessageContent.length - 1]; + + if (!lastUserMessage || !isUserMessageType(lastUserMessage)) { + return; + } + + const mentions = lastUserMessage.mentions; + const agentMentions = mentions.filter(isAgentMention); + onStickyMentionsChange(agentMentions); + }, [ + conversation?.content, + conversation?.content.length, + onStickyMentionsChange, + user.id, + ]); + const buildEventSourceURL = useCallback( (lastEvent: string | null) => { const esURL = `/api/w/${owner.sId}/assistant/conversations/${conversationId}/events`; diff --git a/front/components/assistant/conversation/InputBar.tsx b/front/components/assistant/conversation/InputBar.tsx index cc81dd607d1a..305db1c76699 100644 --- a/front/components/assistant/conversation/InputBar.tsx +++ b/front/components/assistant/conversation/InputBar.tsx @@ -18,7 +18,7 @@ import { compareAgentsForSort } from "@app/lib/assistant"; import { useAgentConfigurations } from "@app/lib/swr"; import { classNames } from "@app/lib/utils"; import { AgentConfigurationType } from "@app/types/assistant/agent"; -import { MentionType } from "@app/types/assistant/conversation"; +import { AgentMention, MentionType } from "@app/types/assistant/conversation"; import { WorkspaceType } from "@app/types/user"; // AGENT MENTION @@ -183,9 +183,11 @@ const AgentList = forwardRef(AgentListImpl); export function AssistantInputBar({ owner, onSubmit, + stickyMentions, }: { owner: WorkspaceType; onSubmit: (input: string, mentions: MentionType[]) => void; + stickyMentions?: AgentMention[]; }) { const [agentListVisible, setAgentListVisible] = useState(false); const [agentListFilter, setAgentListFilter] = useState(""); @@ -262,6 +264,69 @@ export function AssistantInputBar({ } }, [animate, isAnimating]); + const stickyMentionsTextContent = useRef(null); + + useEffect(() => { + if (!stickyMentions) { + return; + } + + const mentionedAgentConfigurationIds = new Set( + stickyMentions?.map((m) => m.configurationId) + ); + + const contentEditable = document.getElementById("dust-input-bar"); + if (contentEditable) { + const textContent = contentEditable.textContent?.trim(); + + if (textContent?.length && !stickyMentionsTextContent.current) { + return; + } + + if ( + textContent?.length && + textContent !== stickyMentionsTextContent.current + ) { + // content has changed, we don't clear it (we preserve whatever the user typed) + return; + } + + // we clear the content of the input bar -- at this point, it's either already empty, + // or contains only the sticky mentions added by this hook + contentEditable.innerHTML = ""; + let lastTextNode = null; + for (const configurationId of mentionedAgentConfigurationIds) { + const agentConfiguration = agentConfigurations.find( + (agent) => agent.sId === configurationId + ); + if (!agentConfiguration) { + continue; + } + const mentionNode = getAgentMentionNode(agentConfiguration); + if (!mentionNode) { + continue; + } + contentEditable.appendChild(mentionNode); + lastTextNode = document.createTextNode(" "); + contentEditable.appendChild(lastTextNode); + + stickyMentionsTextContent.current = + contentEditable.textContent?.trim() || null; + } + // move the cursor to the end of the input bar + if (lastTextNode) { + const selection = window.getSelection(); + if (selection) { + const range = document.createRange(); + range.setStart(lastTextNode, lastTextNode.length); + range.setEnd(lastTextNode, lastTextNode.length); + selection.removeAllRanges(); + selection.addRange(range); + } + } + } + }, [stickyMentions, agentConfigurations, stickyMentionsTextContent]); + return ( <> - ); - const wrapper = document.createElement("div"); - wrapper.innerHTML = htmlString.trim(); - const mentionNode = wrapper.firstChild; + const mentionNode = getAgentMentionNode(selected); // This is mainly to please TypeScript. if (!mentionNode || !mentionSelectNode.parentNode) { @@ -594,12 +654,7 @@ export function AssistantInputBar({ onItemClick={(c) => { // We construct the HTML for an AgentMention and inject it in the content // editable with an extra space after it. - const htmlString = ReactDOMServer.renderToStaticMarkup( - - ); - const wrapper = document.createElement("div"); - wrapper.innerHTML = htmlString.trim(); - const mentionNode = wrapper.firstChild; + const mentionNode = getAgentMentionNode(c); const contentEditable = document.getElementById("dust-input-bar"); if (contentEditable && mentionNode) { @@ -631,17 +686,34 @@ export function AssistantInputBar({ export function FixedAssistantInputBar({ owner, onSubmit, + stickyMentions, }: { owner: WorkspaceType; onSubmit: (input: string, mentions: MentionType[]) => void; + stickyMentions?: AgentMention[]; }) { return (
- +
); } export const InputBarContext = createContext({ animate: false }); + +function getAgentMentionNode( + agentConfiguration: AgentConfigurationType +): ChildNode | null { + const htmlString = ReactDOMServer.renderToStaticMarkup( + + ); + const wrapper = document.createElement("div"); + wrapper.innerHTML = htmlString.trim(); + return wrapper.firstChild; +} diff --git a/front/pages/w/[wId]/assistant/[cId]/index.tsx b/front/pages/w/[wId]/assistant/[cId]/index.tsx index 332f1eacefd1..dc42e2aaa115 100644 --- a/front/pages/w/[wId]/assistant/[cId]/index.tsx +++ b/front/pages/w/[wId]/assistant/[cId]/index.tsx @@ -1,5 +1,6 @@ import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; +import { useState } from "react"; import Conversation from "@app/components/assistant/conversation/Conversation"; import { ConversationTitle } from "@app/components/assistant/conversation/ConversationTitle"; @@ -7,7 +8,7 @@ import { FixedAssistantInputBar } from "@app/components/assistant/conversation/I import { AssistantSidebarMenu } from "@app/components/assistant/conversation/SidebarMenu"; import AppLayout from "@app/components/sparkle/AppLayout"; import { Authenticator, getSession, getUserFromSession } from "@app/lib/auth"; -import { MentionType } from "@app/types/assistant/conversation"; +import { AgentMention, MentionType } from "@app/types/assistant/conversation"; import { UserType, WorkspaceType } from "@app/types/user"; const { URL = "", GA_TRACKING_ID = "" } = process.env; @@ -55,6 +56,7 @@ export default function AssistantConversation({ conversationId, }: InferGetServerSidePropsType) { const router = useRouter(); + const [stickyMentions, setStickyMentions] = useState([]); const handleSubmit = async (input: string, mentions: MentionType[]) => { // Create a new user message. @@ -121,8 +123,17 @@ export default function AssistantConversation({ } > - - + + ); } diff --git a/front/pages/w/[wId]/assistant/new.tsx b/front/pages/w/[wId]/assistant/new.tsx index 7eb3df6e027c..b32a2b542505 100644 --- a/front/pages/w/[wId]/assistant/new.tsx +++ b/front/pages/w/[wId]/assistant/new.tsx @@ -344,7 +344,11 @@ export default function AssistantNew({ ) : ( - + )}