mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-21 14:12:42 +02:00
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:
@@ -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.`,
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -6,10 +6,11 @@ export function AssistantsPageTitle({
|
||||
return (
|
||||
<h1
|
||||
className="
|
||||
text-4xl
|
||||
text-5xl
|
||||
font-bold
|
||||
mb-4
|
||||
text-center
|
||||
text-text-900
|
||||
"
|
||||
>
|
||||
{children}
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
@@ -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>
|
||||
</>
|
||||
|
@@ -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've created that aren'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}
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -207,7 +207,6 @@ export function ChatSessionDisplay({
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
setIsMoreOptionsDropdownOpen(
|
||||
!isMoreOptionsDropdownOpen
|
||||
);
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -14,6 +14,6 @@ export function checkUserIdOwnsAssistant(
|
||||
(!userId ||
|
||||
checkUserIsNoAuthUser(userId) ||
|
||||
assistant.owner?.id === userId) &&
|
||||
!assistant.default_persona
|
||||
!assistant.builtin_persona
|
||||
);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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(
|
||||
|
120
web/src/lib/assistants/utils.ts
Normal file
120
web/src/lib/assistants/utils.ts
Normal 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)
|
||||
);
|
||||
}
|
@@ -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: {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user