Skip to content

Commit

Permalink
[Ahuna] Test assistants in side modal from gallery
Browse files Browse the repository at this point in the history
  • Loading branch information
philipperolet committed Dec 22, 2023
1 parent 4d74500 commit cf8cc27
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 88 deletions.
26 changes: 24 additions & 2 deletions front/components/assistant/AssistantPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Chip,
DashIcon,
MoreIcon,
PlayIcon,
PlusIcon,
} from "@dust-tt/sparkle";
import {
Expand All @@ -28,6 +29,7 @@ interface AssistantPreviewProps {
onUpdate: () => void;
variant: AssistantPreviewVariant;
flow: AssistantPreviewFlow;
setTestModalAssistant?: (agentConfiguration: AgentConfigurationType) => void;
}

function getDescriptionClassName(variant: AssistantPreviewVariant): string {
Expand Down Expand Up @@ -55,6 +57,7 @@ export function AssistantPreview({
onUpdate,
variant,
flow,
setTestModalAssistant,
}: AssistantPreviewProps) {
const [isUpdatingList, setIsUpdatingList] = useState<boolean>(false);
// TODO(flav) Move notification logic to the caller. This maintains the purity of the component by
Expand Down Expand Up @@ -200,7 +203,24 @@ export function AssistantPreview({
default:
assertNever(flow);
}

let testButton = null;
if (
setTestModalAssistant &&
((flow === "workspace" && agentConfiguration.scope === "published") ||
(flow === "personal" &&
agentConfiguration.userListStatus === "not-in-list"))
) {
testButton = (
<Button
key="test"
variant="tertiary"
icon={PlayIcon}
size="xs"
label={"Test"}
onClick={() => setTestModalAssistant(agentConfiguration)}
/>
);
}
const showAssistantButton = (
<Button
key="show_details"
Expand Down Expand Up @@ -268,7 +288,9 @@ export function AssistantPreview({

// Define button groups with JSX elements, including default buttons
const buttonGroups: Record<AssistantPreviewVariant, JSX.Element[]> = {
gallery: [addButton, showAssistantButton].filter(Boolean) as JSX.Element[],
gallery: [addButton, testButton, showAssistantButton].filter(
Boolean
) as JSX.Element[],
home: defaultButtons,
};

Expand Down
166 changes: 166 additions & 0 deletions front/components/assistant/TryAssistantModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Modal } from "@dust-tt/sparkle";
import {
AgentConfigurationType,
AgentMention,
ConversationType,
InternalPostConversationsRequestBodySchema,
MentionType,
UserType,
} from "@dust-tt/types";
import { WorkspaceType } from "@dust-tt/types";
import * as t from "io-ts";
import { useContext, useEffect, useState } from "react";

import Conversation from "@app/components/assistant/conversation/Conversation";
import { GenerationContextProvider } from "@app/components/assistant/conversation/GenerationContextProvider";
import { FixedAssistantInputBar } from "@app/components/assistant/conversation/input_bar/InputBar";
import { SendNotificationsContext } from "@app/components/sparkle/Notification";
import { NotificationType } from "@app/components/sparkle/Notification";
import { deleteConversation, submitMessage } from "@app/lib/conversation";
import { PostConversationsResponseBody } from "@app/pages/api/w/[wId]/assistant/conversations";

export function TryAssistantModal({
owner,
user,
assistant,
onClose,
}: {
owner: WorkspaceType;
user: UserType;
assistant: AgentConfigurationType;
onClose: () => void;
}) {
const [stickyMentions, setStickyMentions] = useState<AgentMention[]>([
{ configurationId: assistant?.sId as string },
]);
const [conversation, setConversation] = useState<
ConversationType | null | { errorMessage: string }
>(null);
const sendNotification = useContext(SendNotificationsContext);

const handleSubmit = async (
input: string,
mentions: MentionType[],
contentFragment?: {
title: string;
content: string;
}
) => {
if (!conversation || "errorMessage" in conversation) return;
const messageData = { input, mentions, contentFragment };
const result = await submitMessage({
owner,
user,
conversationId: conversation.sId as string,
messageData,
});
if (result.isOk()) return;
sendNotification({
title: result.error.title,
description: result.error.message,
type: "error",
});
};

useEffect(() => {
setStickyMentions([{ configurationId: assistant?.sId as string }]);
if (assistant && !conversation) {
createEmptyConversation(owner.sId, assistant, sendNotification)
.then(async (conv) => {
await submitMessage({
owner,
user,
conversationId: conv.sId,
messageData: {
input: "Hi, I'd like to try you out!",
mentions: [{ configurationId: assistant.sId }],
},
});
setConversation(conv);
})
.catch((e) => setConversation({ errorMessage: e.message }));
}
}, [assistant, conversation, owner, sendNotification, user]);

return (
<Modal
isOpen={!!assistant}
title={`Trying @${assistant?.name}`}
onClose={async () => {
onClose();
if (conversation && "sId" in conversation) {
await deleteConversation({
workspaceId: owner.sId,
conversationId: conversation?.sId as string,
sendNotification,
});
setConversation(null);
}
}}
hasChanged={false}
variant="side-md"
>
{conversation && !("errorMessage" in conversation) && (
<GenerationContextProvider>
<Conversation
owner={owner}
user={user}
conversationId={conversation.sId}
onStickyMentionsChange={setStickyMentions}
/>
<FixedAssistantInputBar
owner={owner}
onSubmit={handleSubmit}
stickyMentions={stickyMentions}
conversationId={conversation.sId}
/>{" "}
</GenerationContextProvider>
)}
{conversation && "errorMessage" in conversation && (
<div className="flex h-full flex-col items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold">
Error creating conversation
</h2>
<p className="text-structure-500 text-lg">
{conversation.errorMessage}
</p>
</div>
</div>
)}
</Modal>
);
}

async function createEmptyConversation(
workspaceId: string,
agentConfiguration: AgentConfigurationType,
sendNotification: (notification: NotificationType) => void
): Promise<ConversationType> {
const body: t.TypeOf<typeof InternalPostConversationsRequestBodySchema> = {
title: `Trying out ${agentConfiguration.name}`,
visibility: "unlisted",
message: null,
contentFragment: undefined,
};

const cRes = await fetch(`/api/w/${workspaceId}/assistant/conversations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});

if (!cRes.ok) {
const data = await cRes.json();
sendNotification({
title: "Error creating conversation.",
description: data.error.message || "Please try again or contact us.",
type: "error",
});
throw new Error(data.error.message);
}

return ((await cRes.json()) as PostConversationsResponseBody).conversation;
}
2 changes: 1 addition & 1 deletion front/components/sparkle/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createPortal } from "react-dom";

import { classNames } from "@app/lib/utils";

type NotificationType = {
export type NotificationType = {
title?: string;
description?: string;
type: "success" | "error";
Expand Down
131 changes: 131 additions & 0 deletions front/lib/conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
Err,
MentionType,
Ok,
Result,
UserType,
WorkspaceType,
} from "@dust-tt/types";

import { NotificationType } from "@app/components/sparkle/Notification";

export type ConversationErrorType = {
type:
| "attachment_upload_error"
| "message_send_error"
| "plan_limit_reached_error";
title: string;
message: string;
};

export async function submitMessage({
owner,
user,
conversationId,
messageData,
}: {
owner: WorkspaceType;
user: UserType;
conversationId: string;
messageData: {
input: string;
mentions: MentionType[];
contentFragment?: {
title: string;
content: string;
};
};
}): Promise<Result<void, ConversationErrorType>> {
const { input, mentions, contentFragment } = messageData;
// Create a new content fragment.
if (contentFragment) {
const mcfRes = await fetch(
`/api/w/${owner.sId}/assistant/conversations/${conversationId}/content_fragment`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: contentFragment.title,
content: contentFragment.content,
url: null,
contentType: "file_attachment",
context: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
profilePictureUrl: user.image,
},
}),
}
);

if (!mcfRes.ok) {
const data = await mcfRes.json();
console.error("Error creating content fragment", data);
return new Err({
type: "attachment_upload_error",
title: "Error uploading file.",
message: data.error.message || "Please try again or contact us.",
});
}
}

// Create a new user message.
const mRes = await fetch(
`/api/w/${owner.sId}/assistant/conversations/${conversationId}/messages`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: input,
context: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
profilePictureUrl: user.image,
},
mentions,
}),
}
);

if (!mRes.ok) {
const data = await mRes.json();
return new Err({
type:
data.error.type === "test_plan_message_limit_reached"
? "plan_limit_reached_error"
: "message_send_error",
title: "Your message could not be sent.",
message: data.error.message || "Please try again or contact us.",
});
}
return new Ok(undefined);
}

export async function deleteConversation({
workspaceId,
conversationId,
sendNotification,
}: {
workspaceId: string;
conversationId: string;
sendNotification: (notification: NotificationType) => void;
}) {
const res = await fetch(
`/api/w/${workspaceId}/assistant/conversations/${conversationId}`,
{
method: "DELETE",
}
);

if (!res.ok) {
const data = await res.json();
sendNotification({
title: "Error deleting conversation.",
description: data.error.message || "Please try again or contact us.",
type: "error",
});
return;
}
}
Loading

0 comments on commit cf8cc27

Please sign in to comment.