-
-
-
+
Edit Assistant
diff --git a/web/src/app/assistants/gallery/AssistantsGallery.tsx b/web/src/app/assistants/gallery/AssistantsGallery.tsx
new file mode 100644
index 00000000000..4b9eaff718b
--- /dev/null
+++ b/web/src/app/assistants/gallery/AssistantsGallery.tsx
@@ -0,0 +1,205 @@
+"use client";
+
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { AssistantIcon } from "@/components/assistants/AssistantIcon";
+import { User } from "@/lib/types";
+import { Button } from "@tremor/react";
+import Link from "next/link";
+import { useState } from "react";
+import { FiMinus, FiPlus, FiX } from "react-icons/fi";
+import { NavigationButton } from "../NavigationButton";
+import { AssistantsPageTitle } from "../AssistantsPageTitle";
+import {
+ addAssistantToList,
+ removeAssistantFromList,
+} from "@/lib/assistants/updateAssistantPreferences";
+import { usePopup } from "@/components/admin/connectors/Popup";
+import { useRouter } from "next/navigation";
+import { ToolsDisplay } from "../ToolsDisplay";
+
+export function AssistantsGallery({
+ assistants,
+ user,
+}: {
+ assistants: Persona[];
+ user: User | null;
+}) {
+ function filterAssistants(assistants: Persona[], query: string): Persona[] {
+ return assistants.filter(
+ (assistant) =>
+ assistant.name.toLowerCase().includes(query.toLowerCase()) ||
+ assistant.description.toLowerCase().includes(query.toLowerCase())
+ );
+ }
+
+ const router = useRouter();
+
+ const [searchQuery, setSearchQuery] = useState("");
+ const { popup, setPopup } = usePopup();
+
+ const allAssistantIds = assistants.map((assistant) => assistant.id);
+ const filteredAssistants = filterAssistants(assistants, searchQuery);
+
+ return (
+ <>
+ {popup}
+
+
Assistant Gallery
+
+
+ View Your Assistants
+
+
+
+
+ Discover and create custom assistants that combine instructions, extra
+ knowledge, and any combination of tools.
+
+
+
+ setSearchQuery(e.target.value)}
+ className="
+ w-full
+ p-2
+ border
+ border-gray-300
+ rounded
+ focus:outline-none
+ focus:ring-2
+ focus:ring-blue-500
+ "
+ />
+
+
+ {filteredAssistants.map((assistant) => (
+
+
+
+
+ {assistant.name}
+
+ {user && (
+
+ {!user.preferences?.chosen_assistants ||
+ user.preferences?.chosen_assistants?.includes(
+ assistant.id
+ ) ? (
+ {
+ if (
+ user.preferences?.chosen_assistants &&
+ user.preferences?.chosen_assistants.length === 1
+ ) {
+ setPopup({
+ message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
+ type: "error",
+ });
+ return;
+ }
+
+ const success = await removeAssistantFromList(
+ assistant.id,
+ user.preferences?.chosen_assistants ||
+ allAssistantIds
+ );
+ if (success) {
+ setPopup({
+ message: `"${assistant.name}" has been removed from your list.`,
+ type: "success",
+ });
+ router.refresh();
+ } else {
+ setPopup({
+ message: `"${assistant.name}" could not be removed from your list.`,
+ type: "error",
+ });
+ }
+ }}
+ size="xs"
+ color="red"
+ >
+ Remove
+
+ ) : (
+ {
+ const success = await addAssistantToList(
+ assistant.id,
+ user.preferences?.chosen_assistants ||
+ allAssistantIds
+ );
+ if (success) {
+ setPopup({
+ message: `"${assistant.name}" has been added to your list.`,
+ type: "success",
+ });
+ router.refresh();
+ } else {
+ setPopup({
+ message: `"${assistant.name}" could not be added to your list.`,
+ type: "error",
+ });
+ }
+ }}
+ size="xs"
+ color="green"
+ >
+ Add
+
+ )}
+
+ )}
+
+ {assistant.tools.length > 0 && (
+
+ )}
+
{assistant.description}
+
+ Author: {assistant.owner?.email || "Danswer"}
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/web/src/app/assistants/gallery/page.tsx b/web/src/app/assistants/gallery/page.tsx
new file mode 100644
index 00000000000..c4b9f46f6d0
--- /dev/null
+++ b/web/src/app/assistants/gallery/page.tsx
@@ -0,0 +1,81 @@
+import { ChatSidebar } from "@/app/chat/sessionSidebar/ChatSidebar";
+import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
+import { UserDropdown } from "@/components/UserDropdown";
+import { ChatProvider } from "@/components/context/ChatContext";
+import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
+import { fetchChatData } from "@/lib/chat/fetchChatData";
+import { unstable_noStore as noStore } from "next/cache";
+import { redirect } from "next/navigation";
+import { AssistantsGallery } from "./AssistantsGallery";
+
+export default async function GalleryPage({
+ searchParams,
+}: {
+ searchParams: { [key: string]: string };
+}) {
+ noStore();
+
+ const data = await fetchChatData(searchParams);
+
+ if ("redirect" in data) {
+ redirect(data.redirect);
+ }
+
+ const {
+ user,
+ chatSessions,
+ availableSources,
+ documentSets,
+ personas,
+ tags,
+ llmProviders,
+ folders,
+ openedFolders,
+ shouldShowWelcomeModal,
+ } = data;
+
+ return (
+ <>
+
+
+ {shouldShowWelcomeModal &&
}
+
+
+
+
+ >
+ );
+}
diff --git a/web/src/app/assistants/mine/AssistantSharingModal.tsx b/web/src/app/assistants/mine/AssistantSharingModal.tsx
new file mode 100644
index 00000000000..0fa55fea424
--- /dev/null
+++ b/web/src/app/assistants/mine/AssistantSharingModal.tsx
@@ -0,0 +1,227 @@
+import { useState } from "react";
+import { Modal } from "@/components/Modal";
+import { MinimalUserSnapshot, User } from "@/lib/types";
+import { Button, Divider, Text } from "@tremor/react";
+import { FiPlus, FiX } from "react-icons/fi";
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { SearchMultiSelectDropdown } from "@/components/Dropdown";
+import { UsersIcon } from "@/components/icons/icons";
+import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
+import {
+ addUsersToAssistantSharedList,
+ removeUsersFromAssistantSharedList,
+} from "@/lib/assistants/shareAssistant";
+import { usePopup } from "@/components/admin/connectors/Popup";
+import { Bubble } from "@/components/Bubble";
+import { useRouter } from "next/navigation";
+import { AssistantIcon } from "@/components/assistants/AssistantIcon";
+import { Spinner } from "@/components/Spinner";
+
+interface AssistantSharingModalProps {
+ assistant: Persona;
+ user: User | null;
+ allUsers: MinimalUserSnapshot[];
+ show: boolean;
+ onClose: () => void;
+}
+
+export function AssistantSharingModal({
+ assistant,
+ user,
+ allUsers,
+ show,
+ onClose,
+}: AssistantSharingModalProps) {
+ const router = useRouter();
+ const { popup, setPopup } = usePopup();
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [selectedUsers, setSelectedUsers] = useState
([]);
+
+ const assistantName = assistant.name;
+ const sharedUsersWithoutOwner = assistant.users.filter(
+ (u) => u.id !== assistant.owner?.id
+ );
+
+ if (!show) {
+ return null;
+ }
+
+ const handleShare = async () => {
+ setIsUpdating(true);
+ const startTime = Date.now();
+
+ const error = await addUsersToAssistantSharedList(
+ assistant,
+ selectedUsers.map((user) => user.id)
+ );
+ router.refresh();
+
+ const elapsedTime = Date.now() - startTime;
+ const remainingTime = Math.max(0, 1000 - elapsedTime);
+
+ setTimeout(() => {
+ setIsUpdating(false);
+ if (error) {
+ setPopup({
+ message: `Failed to share assistant - ${error}`,
+ type: "error",
+ });
+ }
+ }, remainingTime);
+ };
+
+ let sharedStatus = null;
+ if (assistant.is_public || !sharedUsersWithoutOwner.length) {
+ sharedStatus = (
+
+ );
+ } else {
+ sharedStatus = (
+
+ Shared with:{" "}
+
+ {sharedUsersWithoutOwner.map((u) => (
+
{
+ setIsUpdating(true);
+ const startTime = Date.now();
+
+ const error = await removeUsersFromAssistantSharedList(
+ assistant,
+ [u.id]
+ );
+ router.refresh();
+
+ const elapsedTime = Date.now() - startTime;
+ const remainingTime = Math.max(0, 1000 - elapsedTime);
+
+ setTimeout(() => {
+ setIsUpdating(false);
+ if (error) {
+ setPopup({
+ message: `Failed to remove assistant - ${error}`,
+ type: "error",
+ });
+ }
+ }, remainingTime);
+ }}
+ >
+
+ {u.email}
+
+
+ ))}
+
+
+ );
+ }
+
+ return (
+ <>
+ {popup}
+
+ {" "}
+ {assistantName}
+
+ }
+ onOutsideClick={onClose}
+ >
+
+ {isUpdating &&
}
+
+ Control which other users should have access to this assistant.
+
+
+
+
Current status:
+ {sharedStatus}
+
+
+
Share Assistant:
+
+
+ !selectedUsers.map((u2) => u2.id).includes(u1.id) &&
+ !sharedUsersWithoutOwner
+ .map((u2) => u2.id)
+ .includes(u1.id) &&
+ u1.id !== user?.id
+ )
+ .map((user) => {
+ return {
+ name: user.email,
+ value: user.id,
+ };
+ })}
+ onSelect={(option) => {
+ setSelectedUsers([
+ ...Array.from(
+ new Set([
+ ...selectedUsers,
+ { id: option.value as string, email: option.name },
+ ])
+ ),
+ ]);
+ }}
+ itemComponent={({ option }) => (
+
+
+ {option.name}
+
+
+
+
+ )}
+ />
+
+ {selectedUsers.length > 0 &&
+ selectedUsers.map((selectedUser) => (
+
{
+ setSelectedUsers(
+ selectedUsers.filter(
+ (user) => user.id !== selectedUser.id
+ )
+ );
+ }}
+ className={`
+ flex
+ rounded-lg
+ px-2
+ py-1
+ border
+ border-border
+ hover:bg-hover-light
+ cursor-pointer`}
+ >
+ {selectedUser.email}
+
+ ))}
+
+
+ {selectedUsers.length > 0 && (
+ {
+ handleShare();
+ setSelectedUsers([]);
+ }}
+ size="xs"
+ color="blue"
+ >
+ Add
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/web/src/app/assistants/mine/AssistantsList.tsx b/web/src/app/assistants/mine/AssistantsList.tsx
new file mode 100644
index 00000000000..34e792c1ed2
--- /dev/null
+++ b/web/src/app/assistants/mine/AssistantsList.tsx
@@ -0,0 +1,367 @@
+"use client";
+
+import { useState } from "react";
+import { MinimalUserSnapshot, User } from "@/lib/types";
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { Divider, Text } from "@tremor/react";
+import {
+ FiArrowDown,
+ FiArrowUp,
+ FiEdit2,
+ FiMoreHorizontal,
+ FiPlus,
+ FiSearch,
+ FiX,
+ FiShare2,
+} from "react-icons/fi";
+import Link from "next/link";
+import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
+import {
+ addAssistantToList,
+ moveAssistantDown,
+ moveAssistantUp,
+ removeAssistantFromList,
+} from "@/lib/assistants/updateAssistantPreferences";
+import { AssistantIcon } from "@/components/assistants/AssistantIcon";
+import { DefaultPopover } from "@/components/popover/DefaultPopover";
+import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
+import { useRouter } from "next/navigation";
+import { NavigationButton } from "../NavigationButton";
+import { AssistantsPageTitle } from "../AssistantsPageTitle";
+import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
+import { AssistantSharingModal } from "./AssistantSharingModal";
+import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
+import useSWR from "swr";
+import { errorHandlingFetcher } from "@/lib/fetcher";
+import { ToolsDisplay } from "../ToolsDisplay";
+
+function AssistantListItem({
+ assistant,
+ user,
+ allAssistantIds,
+ allUsers,
+ isFirst,
+ isLast,
+ isVisible,
+ setPopup,
+}: {
+ assistant: Persona;
+ user: User | null;
+ allUsers: MinimalUserSnapshot[];
+ allAssistantIds: number[];
+ isFirst: boolean;
+ isLast: boolean;
+ isVisible: boolean;
+ setPopup: (popupSpec: PopupSpec | null) => void;
+}) {
+ const router = useRouter();
+ const [showSharingModal, setShowSharingModal] = useState(false);
+
+ const currentChosenAssistants = user?.preferences?.chosen_assistants;
+ const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
+
+ return (
+ <>
+
{
+ setShowSharingModal(false);
+ router.refresh();
+ }}
+ show={showSharingModal}
+ />
+
+
+
+
+
+ {assistant.name}
+
+
+ {assistant.tools.length > 0 && (
+
+ )}
+
{assistant.description}
+
+
+ {isOwnedByUser && (
+
+ {!assistant.is_public && (
+
setShowSharingModal(true)}
+ >
+
+
+ )}
+
+
+
+
+ )}
+
+
+
+ }
+ side="bottom"
+ align="start"
+ sideOffset={5}
+ >
+ {[
+ ...(!isFirst
+ ? [
+ {
+ const success = await moveAssistantUp(
+ assistant.id,
+ currentChosenAssistants || allAssistantIds
+ );
+ if (success) {
+ setPopup({
+ message: `"${assistant.name}" has been moved up.`,
+ type: "success",
+ });
+ router.refresh();
+ } else {
+ setPopup({
+ message: `"${assistant.name}" could not be moved up.`,
+ type: "error",
+ });
+ }
+ }}
+ >
+ Move Up
+
,
+ ]
+ : []),
+ ...(!isLast
+ ? [
+ {
+ const success = await moveAssistantDown(
+ assistant.id,
+ currentChosenAssistants || allAssistantIds
+ );
+ if (success) {
+ setPopup({
+ message: `"${assistant.name}" has been moved down.`,
+ type: "success",
+ });
+ router.refresh();
+ } else {
+ setPopup({
+ message: `"${assistant.name}" could not be moved down.`,
+ type: "error",
+ });
+ }
+ }}
+ >
+ Move Down
+
,
+ ]
+ : []),
+ isVisible ? (
+ {
+ if (
+ currentChosenAssistants &&
+ currentChosenAssistants.length === 1
+ ) {
+ setPopup({
+ message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
+ type: "error",
+ });
+ return;
+ }
+
+ const success = await removeAssistantFromList(
+ assistant.id,
+ currentChosenAssistants || allAssistantIds
+ );
+ if (success) {
+ setPopup({
+ message: `"${assistant.name}" has been removed from your list.`,
+ type: "success",
+ });
+ router.refresh();
+ } else {
+ setPopup({
+ message: `"${assistant.name}" could not be removed from your list.`,
+ type: "error",
+ });
+ }
+ }}
+ >
+ {isOwnedByUser ? "Hide" : "Remove"}
+
+ ) : (
+ {
+ const success = await addAssistantToList(
+ assistant.id,
+ currentChosenAssistants || allAssistantIds
+ );
+ if (success) {
+ setPopup({
+ message: `"${assistant.name}" has been added to your list.`,
+ type: "success",
+ });
+ router.refresh();
+ } else {
+ setPopup({
+ message: `"${assistant.name}" could not be added to your list.`,
+ type: "error",
+ });
+ }
+ }}
+ >
+ Add
+
+ ),
+ ]}
+
+
+ >
+ );
+}
+
+interface AssistantsListProps {
+ user: User | null;
+ assistants: Persona[];
+}
+
+export function AssistantsList({ user, assistants }: AssistantsListProps) {
+ const filteredAssistants = orderAssistantsForUser(assistants, user);
+ const ownedButHiddenAssistants = assistants.filter(
+ (assistant) =>
+ checkUserOwnsAssistant(user, assistant) &&
+ user?.preferences?.chosen_assistants &&
+ !user?.preferences?.chosen_assistants?.includes(assistant.id)
+ );
+ const allAssistantIds = assistants.map((assistant) => assistant.id);
+
+ const { popup, setPopup } = usePopup();
+
+ const { data: users } = useSWR