Assistants panel rework (#2509)

* update user model

* squash - update assistant gallery

* rework assistant display logic + ux

* update tool + assistant display

* update a couple function names

* update typing + some logic

* remove unnecessary comments

* finalize functionality

* updated logic

* fully functional

* remove logs + ports

* small update to logic

* update typing

* allow seeding of display priority

* reorder migrations

* update for alembic
This commit is contained in:
pablodanswer
2024-09-19 16:36:15 -07:00
committed by GitHub
parent 2274cab554
commit 8a8e2b310e
33 changed files with 917 additions and 556 deletions

View File

@@ -383,6 +383,7 @@ export function AssistantEditor({
} else {
[promptResponse, personaResponse] = await createPersona({
...values,
is_default_persona: admin!,
num_chunks: numChunks,
users:
user && !checkUserIsNoAuthUser(user.id) ? [user.id] : undefined,
@@ -414,10 +415,7 @@ export function AssistantEditor({
shouldAddAssistantToUserPreferences &&
user?.preferences?.chosen_assistants
) {
const success = await addAssistantToList(
assistantId,
user.preferences.chosen_assistants
);
const success = await addAssistantToList(assistantId);
if (success) {
setPopup({
message: `"${assistant.name}" has been added to your list.`,

View File

@@ -20,7 +20,7 @@ import { UserRole, User } from "@/lib/types";
import { useUser } from "@/components/user/UserProvider";
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
if (persona.default_persona) {
if (persona.is_default_persona) {
return <Text>Built-In</Text>;
}
@@ -119,7 +119,7 @@ export function PersonasTable({
id: persona.id.toString(),
cells: [
<div key="name" className="flex">
{!persona.default_persona && (
{!persona.is_default_persona && (
<FiEdit2
className="mr-1 my-auto cursor-pointer"
onClick={() =>
@@ -173,7 +173,7 @@ export function PersonasTable({
</div>,
<div key="edit" className="flex">
<div className="mx-auto my-auto">
{!persona.default_persona && isEditable ? (
{!persona.is_default_persona && isEditable ? (
<div
className="hover:bg-hover rounded p-1 cursor-pointer"
onClick={async () => {

View File

@@ -35,7 +35,8 @@ export interface Persona {
llm_model_provider_override?: string;
llm_model_version_override?: string;
starter_messages: StarterMessage[] | null;
default_persona: boolean;
builtin_persona: boolean;
is_default_persona: boolean;
users: MinimalUserSnapshot[];
groups: number[];
icon_shape?: number;

View File

@@ -21,6 +21,7 @@ interface PersonaCreationRequest {
icon_shape: number | null;
remove_image?: boolean;
uploaded_image: File | null;
is_default_persona: boolean;
}
interface PersonaUpdateRequest {
@@ -125,6 +126,11 @@ function buildPersonaAPIBody(
remove_image,
} = creationRequest;
const is_default_persona =
"is_default_persona" in creationRequest
? creationRequest.is_default_persona
: false;
return {
name,
description,
@@ -145,6 +151,7 @@ function buildPersonaAPIBody(
icon_shape,
uploaded_image_id,
remove_image,
is_default_persona,
};
}

View File

@@ -6,9 +6,11 @@ import { FiLock, FiUnlock } from "react-icons/fi";
export function AssistantSharedStatusDisplay({
assistant,
user,
size = "sm",
}: {
assistant: Persona;
user: User | null;
size?: "sm" | "md" | "lg";
}) {
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
@@ -18,7 +20,9 @@ export function AssistantSharedStatusDisplay({
if (assistant.is_public) {
return (
<div className="text-subtle text-sm flex items-center">
<div
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
>
<FiUnlock className="mr-1" />
Public
</div>
@@ -27,7 +31,9 @@ export function AssistantSharedStatusDisplay({
if (assistantSharedUsersWithoutOwner.length > 0) {
return (
<div className="text-subtle text-sm flex items-center">
<div
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
>
<FiUnlock className="mr-1" />
{isOwnedByUser ? (
`Shared with: ${
@@ -54,7 +60,9 @@ export function AssistantSharedStatusDisplay({
}
return (
<div className="text-subtle text-sm flex items-center">
<div
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
>
<FiLock className="mr-1" />
Private
</div>

View File

@@ -6,10 +6,11 @@ export function AssistantsPageTitle({
return (
<h1
className="
text-4xl
text-5xl
font-bold
mb-4
text-center
text-text-900
"
>
{children}

View File

@@ -1,41 +1,5 @@
import { Bubble } from "@/components/Bubble";
import { ToolSnapshot } from "@/lib/tools/interfaces";
import { FiImage, FiSearch, FiGlobe, FiMoreHorizontal } from "react-icons/fi";
import { FiImage, FiSearch } from "react-icons/fi";
import { Persona } from "../admin/assistants/interfaces";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
import { useState } from "react";
export function ToolsDisplay({ tools }: { tools: ToolSnapshot[] }) {
return (
<div className="text-xs text-subtle flex flex-wrap gap-1 mt-2">
<p className="text-sm text-default my-auto">Tools:</p>
{tools.map((tool) => {
let toolName = tool.name;
let toolIcon = null;
if (tool.name === "SearchTool") {
toolName = "Search";
toolIcon = <FiSearch className="mr-1 my-auto" />;
} else if (tool.name === "ImageGenerationTool") {
toolName = "Image Generation";
toolIcon = <FiImage className="mr-1 my-auto" />;
} else if (tool.name === "InternetSearchTool") {
toolName = "Internet Search";
toolIcon = <FiGlobe className="mr-1 my-auto" />;
}
return (
<Bubble key={tool.id} isSelected={false} notSelectable>
<div className="flex flex-row gap-0.5">
{toolIcon}
{toolName}
</div>
</Bubble>
);
})}
</div>
);
}
export function AssistantTools({
assistant,

View File

@@ -6,17 +6,137 @@ 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 { FiList, FiMinus, FiPlus } from "react-icons/fi";
import { AssistantsPageTitle } from "../AssistantsPageTitle";
import {
addAssistantToList,
removeAssistantFromList,
} from "@/lib/assistants/updateAssistantPreferences";
import { usePopup } from "@/components/admin/connectors/Popup";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { AssistantTools, ToolsDisplay } from "../ToolsDisplay";
import { AssistantTools } from "../ToolsDisplay";
import { classifyAssistants } from "@/lib/assistants/utils";
export function AssistantGalleryCard({
assistant,
user,
setPopup,
selectedAssistant,
}: {
assistant: Persona;
user: User | null;
setPopup: (popup: PopupSpec) => void;
selectedAssistant: boolean;
}) {
const router = useRouter();
return (
<div
key={assistant.id}
className="
bg-background-emphasis
rounded-lg
shadow-md
p-4
"
>
<div className="flex items-center">
<AssistantIcon assistant={assistant} />
<h2
className="
text-xl
font-semibold
my-auto
ml-2
text-strong
line-clamp-2
"
>
{assistant.name}
</h2>
{user && (
<div className="ml-auto">
{selectedAssistant ? (
<Button
className="
mr-2
my-auto
bg-background-700
hover:bg-background-600
"
icon={FiMinus}
onClick={async () => {
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);
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"
>
Deselect
</Button>
) : (
<Button
className="
mr-2
my-auto
bg-accent
hover:bg-accent-hover
"
icon={FiPlus}
onClick={async () => {
const success = await addAssistantToList(assistant.id);
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
</Button>
)}
</div>
)}
</div>
<p className="text-sm mt-2">{assistant.description}</p>
<p className="text-subtle text-sm my-2">
Author: {assistant.owner?.email || "Danswer"}
</p>
{assistant.tools.length > 0 && (
<AssistantTools list assistant={assistant} />
)}
</div>
);
}
export function AssistantsGallery({
assistants,
user,
@@ -25,182 +145,171 @@ export function AssistantsGallery({
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);
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
user,
assistants
);
const defaultAssistants = assistants
.filter((assistant) => assistant.is_default_persona)
.filter(
(assistant) =>
assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
assistant.description.toLowerCase().includes(searchQuery.toLowerCase())
);
const nonDefaultAssistants = assistants
.filter((assistant) => !assistant.is_default_persona)
.filter(
(assistant) =>
assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
assistant.description.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<>
{popup}
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<AssistantsPageTitle>Assistant Gallery</AssistantsPageTitle>
<div className="flex justify-center mb-6">
<Link href="/assistants/mine">
<NavigationButton>View Your Assistants</NavigationButton>
</Link>
<div className="grid grid-cols-2 gap-4 mt-4 mb-6">
<Button
onClick={() => router.push("/assistants/new")}
className="w-full py-3 text-lg rounded-full bg-background-800 text-white hover:bg-background-800 transition duration-300 ease-in-out"
icon={FiPlus}
>
Create New Assistant
</Button>
<Button
onClick={() => router.push("/assistants/mine")}
className="w-full hover:border-border-strong py-3 text-lg rounded-full bg-white border border-border shadow text-text-700 hover:bg-background-50 transition duration-300 ease-in-out"
icon={FiList}
>
Your Assistants
</Button>
</div>
<p className="text-center mb-6">
Discover and create custom assistants that combine instructions, extra
knowledge, and any combination of tools.
</p>
<div className="mb-6">
<input
type="text"
placeholder="Search assistants..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="
w-full
p-2
border
border-gray-300
rounded
focus:outline-none
focus:ring-2
focus:ring-blue-500
"
/>
</div>
<div
className="
w-full
grid
grid-cols-2
gap-4
py-2
"
>
{filteredAssistants.map((assistant) => (
<div
key={assistant.id}
<div className="mt-4 mb-12">
<div className="relative">
<input
type="text"
placeholder="Search assistants..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="
bg-background-emphasis
rounded-lg
shadow-md
p-4
"
>
<div className="flex items-center">
<AssistantIcon assistant={assistant} />
<h2
className="
text-xl
font-semibold
my-auto
ml-2
text-strong
line-clamp-2
"
>
{assistant.name}
</h2>
{user && (
<div className="ml-auto">
{!user.preferences?.chosen_assistants ||
user.preferences?.chosen_assistants?.includes(
assistant.id
) ? (
<Button
className="
mr-2
my-auto
"
icon={FiMinus}
onClick={async () => {
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="blue"
>
Deselect
</Button>
) : (
<Button
className="
mr-2
my-auto
"
icon={FiPlus}
onClick={async () => {
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
</Button>
)}
</div>
)}
</div>
<p className="text-sm mt-2">{assistant.description}</p>
<p className="text-subtle text-sm my-2">
Author: {assistant.owner?.email || "Danswer"}
</p>
{assistant.tools.length > 0 && (
<AssistantTools list assistant={assistant} />
)}
w-full
py-3
px-4
pl-10
text-lg
border-2
border-background-strong
rounded-full
bg-background-50
text-text-700
placeholder-text-400
focus:outline-none
focus:ring-2
focus:ring-primary-500
focus:border-transparent
transition duration-300 ease-in-out
"
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-text-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
/>
</svg>
</div>
))}
</div>
</div>
{defaultAssistants.length == 0 &&
nonDefaultAssistants.length == 0 &&
assistants.length != 0 && (
<div className="text-text-500">
No assistants found for this search
</div>
)}
{defaultAssistants.length > 0 && (
<>
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-2 text-text-900">
Default Assistants
</h2>
<h3 className="text-lg text-text-500">
These are assistant created by your admins are and preferred.
</h3>
</section>
<div
className="
w-full
grid
grid-cols-2
gap-4
py-2
"
>
{defaultAssistants.map((assistant) => (
<AssistantGalleryCard
selectedAssistant={visibleAssistants.includes(assistant)}
key={assistant.id}
assistant={assistant}
user={user}
setPopup={setPopup}
/>
))}
</div>
</>
)}
{nonDefaultAssistants.length > 0 && (
<section className="mt-12 mb-8 flex flex-col gap-y-2">
<div className="flex flex-col">
<h2 className="text-2xl font-semibold text-text-900">
Other Assistants
</h2>
<h3 className="text-lg text-text-500">
These are community-contributed assistants.
</h3>
</div>
<div
className="
w-full
grid
grid-cols-2
gap-4
py-2
"
>
{nonDefaultAssistants.map((assistant) => (
<AssistantGalleryCard
selectedAssistant={visibleAssistants.includes(assistant)}
key={assistant.id}
assistant={assistant}
user={user}
setPopup={setPopup}
/>
))}
</div>
</section>
)}
</div>
</>
);

View File

@@ -73,7 +73,11 @@ export function AssistantSharingModal({
let sharedStatus = null;
if (assistant.is_public || !sharedUsersWithoutOwner.length) {
sharedStatus = (
<AssistantSharedStatusDisplay assistant={assistant} user={user} />
<AssistantSharedStatusDisplay
size="md"
assistant={assistant}
user={user}
/>
);
} else {
sharedStatus = (
@@ -122,27 +126,30 @@ export function AssistantSharingModal({
<>
{popup}
<Modal
width="max-w-3xl w-full"
title={
<div className="flex">
<AssistantIcon assistant={assistant} />{" "}
<div className="ml-2 my-auto">{assistantName}</div>
<div className="flex items-end space-x-3">
<AssistantIcon size="large" assistant={assistant} />
<h2 className="text-3xl text-text-800 font-semibold">
{assistantName}
</h2>
</div>
}
onOutsideClick={onClose}
>
<div className="px-4">
<div>
{isUpdating && <Spinner />}
<Text className="mb-5">
Control which other users should have access to this assistant.
</Text>
<p className="text-text-600 text-lg mb-6">
Manage access to this assistant by sharing it with other users.
</p>
<div>
<p className="font-bold mb-2">Current status:</p>
{sharedStatus}
<div className="mb-8 flex flex-col gap-y-4">
<h3 className="text-lg font-semibold">Current Status</h3>
<div className="bg-gray-50 rounded-lg">{sharedStatus}</div>
</div>
<h3 className="text-default font-bold mb-4 mt-3">Share Assistant:</h3>
<div className="mt-4">
<div className="mb-8 flex flex-col gap-y-4">
<h3 className="text-lg font-semibold">Share Assistant</h3>
<SearchMultiSelectDropdown
options={allUsers
.filter(
@@ -153,12 +160,10 @@ export function AssistantSharingModal({
.includes(u1.id) &&
u1.id !== user?.id
)
.map((user) => {
return {
name: user.email,
value: user.id,
};
})}
.map((user) => ({
name: user.email,
value: user.id,
}))}
onSelect={(option) => {
setSelectedUsers([
...Array.from(
@@ -170,18 +175,22 @@ export function AssistantSharingModal({
]);
}}
itemComponent={({ option }) => (
<div className="flex px-4 py-2.5 cursor-pointer hover:bg-hover">
<UsersIcon className="mr-2 my-auto" />
{option.name}
<div className="ml-auto my-auto">
<FiPlus />
</div>
<div className="flex items-center px-4 py-2.5 cursor-pointer hover:bg-gray-100">
<UsersIcon className="mr-3 text-gray-500" />
<span className="flex-grow">{option.name}</span>
<FiPlus className="text-blue-500" />
</div>
)}
/>
<div className="mt-2 flex flex-wrap gap-x-2">
{selectedUsers.length > 0 &&
selectedUsers.map((selectedUser) => (
</div>
{selectedUsers.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-medium text-gray-700 mb-2">
Selected Users:
</h4>
<div className="flex flex-wrap gap-2">
{selectedUsers.map((selectedUser) => (
<div
key={selectedUser.id}
onClick={() => {
@@ -191,35 +200,29 @@ export function AssistantSharingModal({
)
);
}}
className={`
flex
rounded-lg
px-2
py-1
border
border-border
hover:bg-hover-light
cursor-pointer`}
className="flex items-center bg-blue-50 text-blue-700 rounded-full px-3 py-1 text-sm hover:bg-blue-100 transition-colors duration-200 cursor-pointer"
>
{selectedUser.email} <FiX className="ml-1 my-auto" />
{selectedUser.email}
<FiX className="ml-2 text-blue-500" />
</div>
))}
</div>
</div>
)}
{selectedUsers.length > 0 && (
<Button
className="mt-4"
onClick={() => {
handleShare();
setSelectedUsers([]);
}}
size="xs"
color="blue"
>
Add
</Button>
)}
</div>
{selectedUsers.length > 0 && (
<Button
onClick={() => {
handleShare();
setSelectedUsers([]);
}}
size="sm"
color="blue"
className="w-full"
>
Share with Selected Users
</Button>
)}
</div>
</Modal>
</>

View File

@@ -1,25 +1,26 @@
"use client";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import React, {
Dispatch,
ReactNode,
SetStateAction,
useEffect,
useState,
} from "react";
import { MinimalUserSnapshot, User } from "@/lib/types";
import { Persona } from "@/app/admin/assistants/interfaces";
import { Divider, Text } from "@tremor/react";
import { Button, Divider, Text } from "@tremor/react";
import {
FiEdit2,
FiFigma,
FiMenu,
FiList,
FiMinus,
FiMoreHorizontal,
FiPlus,
FiSearch,
FiShare,
FiShare2,
FiToggleLeft,
FiTrash,
FiX,
} from "react-icons/fi";
import Link from "next/link";
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
import {
addAssistantToList,
removeAssistantFromList,
@@ -29,14 +30,12 @@ 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 { AssistantTools } from "../ToolsDisplay";
import {
DndContext,
@@ -62,6 +61,12 @@ import {
} from "@/app/admin/assistants/lib";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantModal";
import {
classifyAssistants,
getUserCreatedAssistants,
orderAssistantsForUser,
} from "@/lib/assistants/utils";
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
function DraggableAssistantListItem(props: any) {
const {
@@ -83,12 +88,12 @@ function DraggableAssistantListItem(props: any) {
};
return (
<div ref={setNodeRef} style={style} className="flex items-center">
<div ref={setNodeRef} style={style} className="flex mt-2 items-center">
<div {...attributes} {...listeners} className="mr-2 cursor-grab">
<DragHandle />
</div>
<div className="flex-grow">
<AssistantListItem {...props} />
<AssistantListItem isDragging={isDragging} {...props} />
</div>
</div>
);
@@ -97,21 +102,21 @@ function DraggableAssistantListItem(props: any) {
function AssistantListItem({
assistant,
user,
allAssistantIds,
allUsers,
isVisible,
setPopup,
deleteAssistant,
shareAssistant,
isDragging,
}: {
assistant: Persona;
user: User | null;
allUsers: MinimalUserSnapshot[];
allAssistantIds: string[];
isVisible: boolean;
deleteAssistant: Dispatch<SetStateAction<Persona | null>>;
shareAssistant: Dispatch<SetStateAction<Persona | null>>;
setPopup: (popupSpec: PopupSpec | null) => void;
isDragging?: boolean;
}) {
const router = useRouter();
const [showSharingModal, setShowSharingModal] = useState(false);
@@ -133,155 +138,154 @@ function AssistantListItem({
show={showSharingModal}
/>
<div
className="flex bg-background-emphasis
rounded-lg
shadow-md
p-4
mb-4 flex-col"
className={`rounded-lg px-4 py-6 transition-all duration-900 hover:bg-background-125 ${isDragging && "bg-background-125"}`}
>
<div
className="
flex
justify-between
items-center
"
>
<div className="w-3/4">
<div className="flex items-center">
<AssistantIcon assistant={assistant} />
<h2 className="text-xl line-clamp-2 font-semibold my-auto ml-2">
{assistant.name}
</h2>
<div className="flex justify-between items-center">
<AssistantIcon assistant={assistant} />
<h2 className="ml-6 w-fit flex-grow space-y-3 text-start flex text-xl font-semibold line-clamp-2 text-gray-800">
{assistant.name}
</h2>
{/* {isOwnedByUser && ( */}
<div className="flex items-center space-x-4">
<div className="flex mr-20 flex-wrap items-center gap-x-4">
{assistant.tools.length > 0 && (
<p className="text-base flex w-fit text-subtle">
{assistant.tools.length} tool
{assistant.tools.length > 1 && "s"}
</p>
)}
<AssistantSharedStatusDisplay
size="md"
assistant={assistant}
user={user}
/>
</div>
<div className="text-sm mt-2">{assistant.description}</div>
<div className="mt-2 flex items-start gap-y-2 flex-col gap-x-3">
<AssistantSharedStatusDisplay assistant={assistant} user={user} />
{assistant.tools.length != 0 && (
<AssistantTools list assistant={assistant} />
)}
</div>
</div>
{isOwnedByUser && (
<div className="ml-auto flex items-center">
{!assistant.is_public && (
<div
className="mr-4 rounded p-2 cursor-pointer hover:bg-hover"
onClick={(e) => {
e.stopPropagation();
setShowSharingModal(true);
}}
>
<FiShare2 size={16} />
</div>
)}
{isOwnedByUser ? (
<Link
href={`/assistants/edit/${assistant.id}`}
className="mr-4 rounded p-2 cursor-pointer hover:bg-hover"
className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200"
title="Edit assistant"
>
<FiEdit2 size={16} />
<FiEdit2 size={20} className="text-text-900" />
</Link>
<DefaultPopover
content={
<div className="hover:bg-hover rounded p-2 cursor-pointer">
<FiMoreHorizontal size={16} />
</div>
}
side="bottom"
align="start"
sideOffset={5}
) : (
<CustomTooltip
showTick
content="You don't have permission to edit this assistant"
>
{[
isVisible ? (
<div
key="remove"
className="flex items-center gap-x-2"
onClick={async () => {
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",
});
}
}}
>
<FiX /> {isOwnedByUser ? "Hide" : "Remove"}
</div>
) : (
<div
key="add"
className="flex items-center gap-x-2"
onClick={async () => {
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",
});
}
}}
>
<FiPlus /> Add
</div>
),
isOwnedByUser ? (
<div
key="delete"
className="flex items-center gap-x-2"
onClick={() => deleteAssistant(assistant)}
>
<FiTrash /> Delete
</div>
) : (
<></>
),
isOwnedByUser ? (
<div
key="delete"
className="flex items-center gap-x-2"
onClick={() => shareAssistant(assistant)}
>
{assistant.is_public ? <FiMinus /> : <FiPlus />} Make{" "}
{assistant.is_public ? "Private" : "Public"}
</div>
) : (
<></>
),
]}
</DefaultPopover>
</div>
)}
<div className="p-2 cursor-not-allowed opacity-50 rounded-full hover:bg-gray-100 transition-colors duration-200">
<FiEdit2 size={20} className="text-text-900" />
</div>
</CustomTooltip>
)}
<DefaultPopover
content={
<div className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200 cursor-pointer">
<FiMoreHorizontal size={20} className="text-text-900" />
</div>
}
side="bottom"
align="end"
sideOffset={5}
>
{[
isVisible ? (
<button
key="remove"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={async () => {
if (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
);
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",
});
}
}}
>
<FiX size={18} className="text-text-800" />{" "}
{isOwnedByUser ? "Hide" : "Remove"}
</button>
) : (
<button
key="add"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={async () => {
const success = await addAssistantToList(assistant.id);
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",
});
}
}}
>
<FiPlus size={18} className="text-text-800" /> Add
</button>
),
isOwnedByUser ? (
<button
key="delete"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left text-red-600"
onClick={() => deleteAssistant(assistant)}
>
<FiTrash size={18} /> Delete
</button>
) : null,
isOwnedByUser ? (
<button
key="visibility"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={() => shareAssistant(assistant)}
>
{assistant.is_public ? (
<FiMinus size={18} className="text-text-800" />
) : (
<FiPlus size={18} className="text-text-800" />
)}{" "}
Make {assistant.is_public ? "Private" : "Public"}
</button>
) : null,
!assistant.is_public ? (
<button
key="share"
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
onClick={(e) => {
setShowSharingModal(true);
}}
>
<FiShare2 size={18} className="text-text-800" /> Share
</button>
) : null,
]}
</DefaultPopover>
</div>
{/* )} */}
</div>
</div>
</>
@@ -294,17 +298,19 @@ export function AssistantsList({
user: User | null;
assistants: Persona[];
}) {
const [filteredAssistants, setFilteredAssistants] = useState<Persona[]>([]);
// Define the distinct groups of assistants
const { visibleAssistants, hiddenAssistants } = classifyAssistants(
user,
assistants
);
useEffect(() => {
setFilteredAssistants(orderAssistantsForUser(assistants, user));
}, [user, assistants, orderAssistantsForUser]);
const [currentlyVisibleAssistants, setCurrentlyVisibleAssistants] = useState<
Persona[]
>(orderAssistantsForUser(visibleAssistants, user));
const ownedButHiddenAssistants = assistants.filter(
(assistant) =>
checkUserOwnsAssistant(user, assistant) &&
user?.preferences?.chosen_assistants &&
!user?.preferences?.chosen_assistants?.includes(assistant.id)
const ownedButHiddenAssistants = getUserCreatedAssistants(
user,
hiddenAssistants
);
const allAssistantIds = assistants.map((assistant) =>
@@ -332,9 +338,9 @@ export function AssistantsList({
async function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
filteredAssistants;
if (over && active.id !== over.id) {
setFilteredAssistants((assistants) => {
setCurrentlyVisibleAssistants((assistants) => {
const oldIndex = assistants.findIndex(
(a) => a.id.toString() === active.id
);
@@ -390,44 +396,36 @@ export function AssistantsList({
/>
)}
<div className="mx-auto mobile:w-[90%] desktop:w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<AssistantsPageTitle>My Assistants</AssistantsPageTitle>
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<AssistantsPageTitle>Your Assistants</AssistantsPageTitle>
<div className="grid grid-cols-2 gap-4 mt-3">
<Link href="/assistants/new">
<NavigationButton>
<div className="flex justify-center">
<FiPlus className="mr-2 my-auto" size={20} />
Create New Assistant
</div>
</NavigationButton>
</Link>
<div className="grid grid-cols-2 gap-4 mt-4 mb-8">
<Button
onClick={() => router.push("/assistants/new")}
className="w-full py-3 text-lg rounded-full bg-background-800 text-white hover:bg-background-800 transition duration-300 ease-in-out"
icon={FiPlus}
>
Create New Assistant
</Button>
<Link href="/assistants/gallery">
<NavigationButton>
<div className="flex justify-center">
<FiSearch className="mr-2 my-auto" size={20} />
View Available Assistants
</div>
</NavigationButton>
</Link>
<Button
onClick={() => router.push("/assistants/gallery")}
className="w-full hover:border-border-strong py-3 text-lg rounded-full bg-white border !border-border shadow text-text-700 hover:bg-background-50 transition duration-300 ease-in-out"
icon={FiList}
>
Assistant Gallery
</Button>
</div>
<p className="mt-6 text-center text-base">
Assistants allow you to customize your experience for a specific
purpose. Specifically, they combine instructions, extra knowledge, and
any combination of tools.
</p>
<h2 className="text-2xl font-semibold mb-2 text-text-900">
Active Assistants
</h2>
<Divider />
<h3 className="text-xl font-bold mb-4">Active Assistants</h3>
<Text>
<h3 className="text-lg text-text-500">
The order the assistants appear below will be the order they appear in
the Assistants dropdown. The first assistant listed will be your
default assistant when you start a new chat. Drag and drop to reorder.
</Text>
</h3>
<DndContext
sensors={sensors}
@@ -435,11 +433,11 @@ export function AssistantsList({
onDragEnd={handleDragEnd}
>
<SortableContext
items={filteredAssistants.map((a) => a.id.toString())}
items={currentlyVisibleAssistants.map((a) => a.id.toString())}
strategy={verticalListSortingStrategy}
>
<div className="w-full p-4 mt-3">
{filteredAssistants.map((assistant, index) => (
<div className="w-full items-center py-4">
{currentlyVisibleAssistants.map((assistant, index) => (
<DraggableAssistantListItem
deleteAssistant={setDeletingPersona}
shareAssistant={setMakePublicPersona}
@@ -462,10 +460,10 @@ export function AssistantsList({
<h3 className="text-xl font-bold mb-4">Your Hidden Assistants</h3>
<Text>
<h3 className="text-lg text-text-500">
Assistants you&apos;ve created that aren&apos;t currently visible
in the Assistants selector.
</Text>
</h3>
<div className="w-full p-4">
{ownedButHiddenAssistants.map((assistant, index) => (
@@ -475,7 +473,6 @@ export function AssistantsList({
key={assistant.id}
assistant={assistant}
user={user}
allAssistantIds={allAssistantIds}
allUsers={users || []}
isVisible={false}
setPopup={setPopup}

View File

@@ -86,7 +86,6 @@ import {
import { ChatInputBar } from "./input/ChatInputBar";
import { useChatContext } from "@/components/context/ChatContext";
import { v4 as uuidv4 } from "uuid";
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
import { ChatPopup } from "./ChatPopup";
import FunctionalHeader from "@/components/chat_search/Header";
@@ -101,6 +100,10 @@ import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import { SEARCH_TOOL_NAME } from "./tools/constants";
import { useUser } from "@/components/user/UserProvider";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import {
classifyAssistants,
orderAssistantsForUser,
} from "@/lib/assistants/utils";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -136,7 +139,6 @@ export function ChatPage({
const { user, refreshUser, isLoadingUser } = useUser();
// chat session
const existingChatIdRaw = searchParams.get("chatId");
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
@@ -155,7 +157,10 @@ export function ChatPage({
const loadedIdSessionRef = useRef<number | null>(existingChatSessionId);
// Assistants
const filteredAssistants = orderAssistantsForUser(availableAssistants, user);
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
user,
availableAssistants
);
const existingChatSessionAssistantId = selectedChatSession?.persona_id;
const [selectedAssistant, setSelectedAssistant] = useState<
@@ -210,7 +215,7 @@ export function ChatPage({
const liveAssistant =
alternativeAssistant ||
selectedAssistant ||
filteredAssistants[0] ||
visibleAssistants[0] ||
availableAssistants[0];
useEffect(() => {
@@ -680,7 +685,7 @@ export function ChatPage({
useEffect(() => {
if (messageHistory.length === 0 && chatSessionIdRef.current === null) {
setSelectedAssistant(
filteredAssistants.find((persona) => persona.id === defaultAssistantId)
visibleAssistants.find((persona) => persona.id === defaultAssistantId)
);
}
}, [defaultAssistantId]);
@@ -2379,7 +2384,10 @@ export function ChatPage({
showDocs={() => setDocumentSelection(true)}
selectedDocuments={selectedDocuments}
// assistant stuff
assistantOptions={filteredAssistants}
assistantOptions={orderAssistantsForUser(
visibleAssistants,
user
)}
selectedAssistant={liveAssistant}
setSelectedAssistant={onAssistantChange}
setAlternativeAssistant={setAlternativeAssistant}

View File

@@ -20,6 +20,7 @@ import { getFinalLLM } from "@/lib/llm/utils";
import React, { useState } from "react";
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
import { orderAssistantsForUser } from "@/lib/assistants/utils";
export function AssistantsTab({
selectedAssistant,

View File

@@ -6,6 +6,7 @@ import { ChatProvider } from "@/components/context/ChatContext";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import WrappedChat from "./WrappedChat";
import { ProviderContextProvider } from "@/components/chat_search/ProviderContext";
import { orderAssistantsForUser } from "@/lib/assistants/utils";
export default async function Page({
searchParams,

View File

@@ -207,7 +207,6 @@ export function ChatSessionDisplay({
<div
onClick={(e) => {
e.preventDefault();
// e.stopPropagation();
setIsMoreOptionsDropdownOpen(
!isMoreOptionsDropdownOpen
);

View File

@@ -192,7 +192,7 @@ export function SearchMultiSelectDropdown({
export const CustomDropdown = ({
children,
dropdown,
direction = "down", // Default to 'down' if not specified
direction = "down",
}: {
children: JSX.Element | string;
dropdown: JSX.Element | string;

View File

@@ -3,6 +3,7 @@ import React from "react";
import { Tooltip } from "../tooltip/Tooltip";
import { createSVG } from "@/lib/assistantIconUtils";
import { buildImgUrl } from "@/app/chat/files/images/utils";
import { CustomTooltip } from "../tooltip/CustomTooltip";
export function darkerGenerateColorFromId(id: string): string {
const hash = Array.from(id).reduce(
@@ -30,7 +31,7 @@ export function AssistantIcon({
const color = darkerGenerateColorFromId(assistant.id.toString());
return (
<Tooltip delayDuration={1000} content={assistant.description}>
<CustomTooltip showTick line wrap content={assistant.description}>
{
// Prioritization order: image, graph, defaults
assistant.uploaded_image_id ? (
@@ -44,7 +45,7 @@ export function AssistantIcon({
<div
className={`flex-none
${border && "ring ring-[1px] ring-border-strong "}
${size === "large" ? "w-8 h-8" : "w-6 h-6"} `}
${size === "large" ? "w-10 h-10" : "w-6 h-6"} `}
>
{createSVG(
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
@@ -62,6 +63,6 @@ export function AssistantIcon({
/>
)
}
</Tooltip>
</CustomTooltip>
);
}

View File

@@ -5,7 +5,7 @@ import { Popover } from "./Popover";
export function DefaultPopover(props: {
content: JSX.Element;
children: JSX.Element[];
children: (JSX.Element | null)[];
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
sideOffset?: number;
@@ -39,15 +39,17 @@ export function DefaultPopover(props: {
overscroll-contain
`}
>
{props.children.map((child, index) => (
<div
key={index}
className="cursor-pointer text-left text-sm p-2 hover:bg-hover-light"
onClick={() => setPopoverOpen(false)}
>
{child}
</div>
))}
{props.children
.filter((child) => child !== null)
.map((child, index) => (
<div
key={index}
className="cursor-pointer text-left text-sm p-2 hover:bg-hover-light"
onClick={() => setPopoverOpen(false)}
>
{child}
</div>
))}
</div>
}
{...props}

View File

@@ -121,7 +121,7 @@ export const CustomTooltip = ({
{isVisible &&
createPortal(
<div
className={`fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
className={`min-w-8 fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
}
transform -translate-x-1/2 text-sm

View File

@@ -14,6 +14,6 @@ export function checkUserIdOwnsAssistant(
(!userId ||
checkUserIsNoAuthUser(userId) ||
assistant.owner?.id === userId) &&
!assistant.default_persona
!assistant.builtin_persona
);
}

View File

@@ -1,34 +0,0 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { User } from "../types";
export function orderAssistantsForUser(
assistants: Persona[],
user: User | null
) {
if (user && user.preferences && user.preferences.chosen_assistants) {
const chosenAssistantsSet = new Set(user.preferences.chosen_assistants);
const assistantOrderMap = new Map(
user.preferences.chosen_assistants.map((id: number, index: number) => [
id,
index,
])
);
let filteredAssistants = assistants.filter((assistant) =>
chosenAssistantsSet.has(assistant.id)
);
if (filteredAssistants.length == 0) {
return assistants;
}
filteredAssistants.sort((a, b) => {
const orderA = assistantOrderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
const orderB = assistantOrderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
return filteredAssistants;
}
return assistants;
}

View File

@@ -11,24 +11,33 @@ export async function updateUserAssistantList(
return response.ok;
}
export async function updateAssistantVisibility(
assistantId: number,
show: boolean
): Promise<boolean> {
const response = await fetch(
`/api/user/assistant-list/update/${assistantId}?show=${show}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
}
);
return response.ok;
}
export async function removeAssistantFromList(
assistantId: number,
chosenAssistants: number[]
assistantId: number
): Promise<boolean> {
const updatedAssistants = chosenAssistants.filter((id) => id !== assistantId);
return updateUserAssistantList(updatedAssistants);
return updateAssistantVisibility(assistantId, false);
}
export async function addAssistantToList(
assistantId: number,
chosenAssistants: number[]
assistantId: number
): Promise<boolean> {
if (!chosenAssistants.includes(assistantId)) {
const updatedAssistants = [...chosenAssistants, assistantId];
return updateUserAssistantList(updatedAssistants);
}
return false;
return updateAssistantVisibility(assistantId, true);
}
export async function moveAssistantUp(

View File

@@ -0,0 +1,120 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { User } from "../types";
import { checkUserIsNoAuthUser } from "../user";
export function checkUserOwnsAssistant(user: User | null, assistant: Persona) {
return checkUserIdOwnsAssistant(user?.id, assistant);
}
export function checkUserIdOwnsAssistant(
userId: string | undefined,
assistant: Persona
) {
return (
(!userId ||
checkUserIsNoAuthUser(userId) ||
assistant.owner?.id === userId) &&
!assistant.is_default_persona
);
}
export function classifyAssistants(user: User | null, assistants: Persona[]) {
if (!user) {
return {
visibleAssistants: assistants.filter(
(assistant) => assistant.is_default_persona
),
hiddenAssistants: [],
};
}
const visibleAssistants = assistants.filter((assistant) => {
const isVisible = user.preferences?.visible_assistants?.includes(
assistant.id
);
const isNotHidden = !user.preferences?.hidden_assistants?.includes(
assistant.id
);
const isSelected = user.preferences?.chosen_assistants?.includes(
assistant.id
);
const isBuiltIn = assistant.builtin_persona;
const isDefault = assistant.is_default_persona;
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
const isShown =
(isVisible && isNotHidden && isSelected) ||
(isNotHidden && (isBuiltIn || isDefault || isOwnedByUser));
return isShown;
});
const hiddenAssistants = assistants.filter((assistant) => {
return !visibleAssistants.includes(assistant);
});
return {
visibleAssistants,
hiddenAssistants,
};
}
export function orderAssistantsForUser(
assistants: Persona[],
user: User | null
) {
let orderedAssistants = [...assistants];
if (user?.preferences?.chosen_assistants) {
const chosenAssistantsSet = new Set(user.preferences.chosen_assistants);
const assistantOrderMap = new Map(
user.preferences.chosen_assistants.map((id: number, index: number) => [
id,
index,
])
);
// Sort chosen assistants based on user preferences
orderedAssistants.sort((a, b) => {
const orderA = assistantOrderMap.get(a.id);
const orderB = assistantOrderMap.get(b.id);
if (orderA !== undefined && orderB !== undefined) {
return orderA - orderB;
} else if (orderA !== undefined) {
return -1;
} else if (orderB !== undefined) {
return 1;
} else {
return 0;
}
});
// Filter out assistants not in the user's chosen list
orderedAssistants = orderedAssistants.filter((assistant) =>
chosenAssistantsSet.has(assistant.id)
);
}
// Sort remaining assistants based on display_priority
const remainingAssistants = assistants.filter(
(assistant) => !orderedAssistants.includes(assistant)
);
remainingAssistants.sort((a, b) => {
const priorityA = a.display_priority ?? Number.MAX_SAFE_INTEGER;
const priorityB = b.display_priority ?? Number.MAX_SAFE_INTEGER;
return priorityA - priorityB;
});
// Combine ordered chosen assistants with remaining assistants
return [...orderedAssistants, ...remainingAssistants];
}
export function getUserCreatedAssistants(
user: User | null,
assistants: Persona[]
) {
return assistants.filter((assistant) =>
checkUserOwnsAssistant(user, assistant)
);
}

View File

@@ -32,10 +32,7 @@ export async function deleteCredential<T>(
});
}
export async function forceDeleteCredential<T>(
credentialId: number,
force?: boolean
) {
export async function forceDeleteCredential<T>(credentialId: number) {
return await fetch(`/api/manage/credential/force/${credentialId}`, {
method: "DELETE",
headers: {

View File

@@ -5,6 +5,8 @@ import { ConnectorCredentialPairStatus } from "@/app/admin/connector/[ccPairId]/
export interface UserPreferences {
chosen_assistants: number[] | null;
visible_assistants: number[];
hidden_assistants: number[];
default_model: string | null;
}