Skip to content

Commit

Permalink
feat(conversations): assistant mentions are sticky (#1862)
Browse files Browse the repository at this point in the history
* feat(conversations): assistant mentions are sticky

* fix + no cross user

* use a ref instead of a regex

* dry out mention node
  • Loading branch information
fontanierh authored Sep 29, 2023
1 parent 82464ac commit 2c1442d
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 19 deletions.
44 changes: 43 additions & 1 deletion front/components/assistant/conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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`;
Expand Down
100 changes: 86 additions & 14 deletions front/components/assistant/conversation/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("");
Expand Down Expand Up @@ -262,6 +264,69 @@ export function AssistantInputBar({
}
}, [animate, isAnimating]);

const stickyMentionsTextContent = useRef<string | null>(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 (
<>
<AgentList
Expand Down Expand Up @@ -456,12 +521,7 @@ export function AssistantInputBar({
// contenteditable and inject an AgentMention component.
if (selected) {
// Construct an AgentMention component and inject it as HTML.
const htmlString = ReactDOMServer.renderToStaticMarkup(
<AgentMention agentConfiguration={selected} />
);
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) {
Expand Down Expand Up @@ -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(
<AgentMention agentConfiguration={c} />
);
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) {
Expand Down Expand Up @@ -631,17 +686,34 @@ export function AssistantInputBar({
export function FixedAssistantInputBar({
owner,
onSubmit,
stickyMentions,
}: {
owner: WorkspaceType;
onSubmit: (input: string, mentions: MentionType[]) => void;
stickyMentions?: AgentMention[];
}) {
return (
<div className="4xl:px-0 fixed bottom-0 left-0 right-0 z-20 flex-initial px-2 lg:left-80">
<div className="mx-auto max-w-4xl pb-12">
<AssistantInputBar owner={owner} onSubmit={onSubmit} />
<AssistantInputBar
owner={owner}
onSubmit={onSubmit}
stickyMentions={stickyMentions}
/>
</div>
</div>
);
}

export const InputBarContext = createContext({ animate: false });

function getAgentMentionNode(
agentConfiguration: AgentConfigurationType
): ChildNode | null {
const htmlString = ReactDOMServer.renderToStaticMarkup(
<AgentMention agentConfiguration={agentConfiguration} />
);
const wrapper = document.createElement("div");
wrapper.innerHTML = htmlString.trim();
return wrapper.firstChild;
}
17 changes: 14 additions & 3 deletions front/pages/w/[wId]/assistant/[cId]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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";
import { FixedAssistantInputBar } from "@app/components/assistant/conversation/InputBar";
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;
Expand Down Expand Up @@ -55,6 +56,7 @@ export default function AssistantConversation({
conversationId,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const router = useRouter();
const [stickyMentions, setStickyMentions] = useState<AgentMention[]>([]);

const handleSubmit = async (input: string, mentions: MentionType[]) => {
// Create a new user message.
Expand Down Expand Up @@ -121,8 +123,17 @@ export default function AssistantConversation({
<AssistantSidebarMenu owner={owner} triggerInputAnimation={null} />
}
>
<Conversation owner={owner} conversationId={conversationId} />
<FixedAssistantInputBar owner={owner} onSubmit={handleSubmit} />
<Conversation
owner={owner}
user={user}
conversationId={conversationId}
onStickyMentionsChange={setStickyMentions}
/>
<FixedAssistantInputBar
owner={owner}
onSubmit={handleSubmit}
stickyMentions={stickyMentions}
/>
</AppLayout>
);
}
6 changes: 5 additions & 1 deletion front/pages/w/[wId]/assistant/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ export default function AssistantNew({
</Page.Vertical>
</div>
) : (
<Conversation owner={owner} conversationId={conversation.sId} />
<Conversation
owner={owner}
user={user}
conversationId={conversation.sId}
/>
)}

<FixedAssistantInputBar owner={owner} onSubmit={handleSubmit} />
Expand Down

0 comments on commit 2c1442d

Please sign in to comment.