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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 917 additions and 556 deletions

View File

@ -0,0 +1,79 @@
"""assistant_rework
Revision ID: 55546a7967ee
Revises: 61ff3651add4
Create Date: 2024-09-18 17:00:23.755399
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "55546a7967ee"
down_revision = "61ff3651add4"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Reworking persona and user tables for new assistant features
# keep track of user's chosen assistants separate from their `ordering`
op.add_column("persona", sa.Column("builtin_persona", sa.Boolean(), nullable=True))
op.execute("UPDATE persona SET builtin_persona = default_persona")
op.alter_column("persona", "builtin_persona", nullable=False)
op.drop_index("_default_persona_name_idx", table_name="persona")
op.create_index(
"_builtin_persona_name_idx",
"persona",
["name"],
unique=True,
postgresql_where=sa.text("builtin_persona = true"),
)
op.add_column(
"user", sa.Column("visible_assistants", postgresql.JSONB(), nullable=True)
)
op.add_column(
"user", sa.Column("hidden_assistants", postgresql.JSONB(), nullable=True)
)
op.execute(
"UPDATE \"user\" SET visible_assistants = '[]'::jsonb, hidden_assistants = '[]'::jsonb"
)
op.alter_column(
"user",
"visible_assistants",
nullable=False,
server_default=sa.text("'[]'::jsonb"),
)
op.alter_column(
"user",
"hidden_assistants",
nullable=False,
server_default=sa.text("'[]'::jsonb"),
)
op.drop_column("persona", "default_persona")
op.add_column(
"persona", sa.Column("is_default_persona", sa.Boolean(), nullable=True)
)
def downgrade() -> None:
# Reverting changes made in upgrade
op.drop_column("user", "hidden_assistants")
op.drop_column("user", "visible_assistants")
op.drop_index("_builtin_persona_name_idx", table_name="persona")
op.drop_column("persona", "is_default_persona")
op.add_column("persona", sa.Column("default_persona", sa.Boolean(), nullable=True))
op.execute("UPDATE persona SET default_persona = builtin_persona")
op.alter_column("persona", "default_persona", nullable=False)
op.drop_column("persona", "builtin_persona")
op.create_index(
"_default_persona_name_idx",
"persona",
["name"],
unique=True,
postgresql_where=sa.text("default_persona = true"),
)

View File

@ -122,7 +122,7 @@ def load_personas_from_yaml(
prompt_ids=prompt_ids,
document_set_ids=doc_set_ids,
tool_ids=tool_ids,
default_persona=True,
builtin_persona=True,
is_public=True,
display_priority=existing_persona.display_priority
if existing_persona is not None

View File

@ -125,6 +125,12 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
chosen_assistants: Mapped[list[int]] = mapped_column(
postgresql.JSONB(), nullable=False, default=[-2, -1, 0]
)
visible_assistants: Mapped[list[int]] = mapped_column(
postgresql.JSONB(), nullable=False, default=[]
)
hidden_assistants: Mapped[list[int]] = mapped_column(
postgresql.JSONB(), nullable=False, default=[]
)
oidc_expiry: Mapped[datetime.datetime] = mapped_column(
TIMESTAMPAware(timezone=True), nullable=True
@ -1308,9 +1314,15 @@ class Persona(Base):
starter_messages: Mapped[list[StarterMessage] | None] = mapped_column(
postgresql.JSONB(), nullable=True
)
# Default personas are configured via backend during deployment
# Built-in personas are configured via backend during deployment
# Treated specially (cannot be user edited etc.)
default_persona: Mapped[bool] = mapped_column(Boolean, default=False)
builtin_persona: Mapped[bool] = mapped_column(Boolean, default=False)
# Default personas are personas created by admins and are automatically added
# to all users' assistants list.
is_default_persona: Mapped[bool] = mapped_column(
Boolean, default=False, nullable=False
)
# controls whether the persona is available to be selected by users
is_visible: Mapped[bool] = mapped_column(Boolean, default=True)
# controls the ordering of personas in the UI
@ -1361,10 +1373,10 @@ class Persona(Base):
# Default personas loaded via yaml cannot have the same name
__table_args__ = (
Index(
"_default_persona_name_idx",
"_builtin_persona_name_idx",
"name",
unique=True,
postgresql_where=(default_persona == True), # noqa: E712
postgresql_where=(builtin_persona == True), # noqa: E712
),
)

View File

@ -178,6 +178,7 @@ def create_update_persona(
except ValueError as e:
logger.exception("Failed to create persona")
raise HTTPException(status_code=400, detail=str(e))
return PersonaSnapshot.from_model(persona)
@ -258,7 +259,7 @@ def get_personas(
stmt = _add_user_filters(stmt=stmt, user=user, get_editable=get_editable)
if not include_default:
stmt = stmt.where(Persona.default_persona.is_(False))
stmt = stmt.where(Persona.builtin_persona.is_(False))
if not include_slack_bot_personas:
stmt = stmt.where(not_(Persona.name.startswith(SLACK_BOT_PERSONA_PREFIX)))
if not include_deleted:
@ -306,7 +307,7 @@ def mark_delete_persona_by_name(
) -> None:
stmt = (
update(Persona)
.where(Persona.name == persona_name, Persona.default_persona == is_default)
.where(Persona.name == persona_name, Persona.builtin_persona == is_default)
.values(deleted=True)
)
@ -406,7 +407,6 @@ def upsert_persona(
document_set_ids: list[int] | None = None,
tool_ids: list[int] | None = None,
persona_id: int | None = None,
default_persona: bool = False,
commit: bool = True,
icon_color: str | None = None,
icon_shape: int | None = None,
@ -414,6 +414,8 @@ def upsert_persona(
display_priority: int | None = None,
is_visible: bool = True,
remove_image: bool | None = None,
builtin_persona: bool = False,
is_default_persona: bool = False,
chunks_above: int = CONTEXT_CHUNKS_ABOVE,
chunks_below: int = CONTEXT_CHUNKS_BELOW,
) -> Persona:
@ -454,8 +456,8 @@ def upsert_persona(
validate_persona_tools(tools)
if persona:
if not default_persona and persona.default_persona:
raise ValueError("Cannot update default persona with non-default.")
if not builtin_persona and persona.builtin_persona:
raise ValueError("Cannot update builtin persona with non-builtin.")
# this checks if the user has permission to edit the persona
persona = fetch_persona_by_id(
@ -470,7 +472,7 @@ def upsert_persona(
persona.llm_relevance_filter = llm_relevance_filter
persona.llm_filter_extraction = llm_filter_extraction
persona.recency_bias = recency_bias
persona.default_persona = default_persona
persona.builtin_persona = builtin_persona
persona.llm_model_provider_override = llm_model_provider_override
persona.llm_model_version_override = llm_model_version_override
persona.starter_messages = starter_messages
@ -482,6 +484,7 @@ def upsert_persona(
persona.uploaded_image_id = uploaded_image_id
persona.display_priority = display_priority
persona.is_visible = is_visible
persona.is_default_persona = is_default_persona
# Do not delete any associations manually added unless
# a new updated list is provided
@ -509,7 +512,7 @@ def upsert_persona(
llm_relevance_filter=llm_relevance_filter,
llm_filter_extraction=llm_filter_extraction,
recency_bias=recency_bias,
default_persona=default_persona,
builtin_persona=builtin_persona,
prompts=prompts or [],
document_sets=document_sets or [],
llm_model_provider_override=llm_model_provider_override,
@ -521,6 +524,7 @@ def upsert_persona(
uploaded_image_id=uploaded_image_id,
display_priority=display_priority,
is_visible=is_visible,
is_default_persona=is_default_persona,
)
db_session.add(persona)
@ -550,7 +554,7 @@ def delete_old_default_personas(
Need a more graceful fix later or those need to never have IDs"""
stmt = (
update(Persona)
.where(Persona.default_persona, Persona.id > 0)
.where(Persona.builtin_persona, Persona.id > 0)
.values(deleted=True, name=func.concat(Persona.name, "_old"))
)
@ -732,7 +736,7 @@ def delete_persona_by_name(
persona_name: str, db_session: Session, is_default: bool = True
) -> None:
stmt = delete(Persona).where(
Persona.name == persona_name, Persona.default_persona == is_default
Persona.name == persona_name, Persona.builtin_persona == is_default
)
db_session.execute(stmt)

View File

@ -66,7 +66,7 @@ def create_slack_bot_persona(
llm_model_version_override=None,
starter_messages=None,
is_public=True,
default_persona=False,
is_default_persona=False,
db_session=db_session,
commit=False,
)

View File

@ -38,6 +38,8 @@ class CreatePersonaRequest(BaseModel):
icon_shape: int | None = None
uploaded_image_id: str | None = None # New field for uploaded image
remove_image: bool | None = None
is_default_persona: bool = False
display_priority: int | None = None
class PersonaSnapshot(BaseModel):
@ -54,7 +56,7 @@ class PersonaSnapshot(BaseModel):
llm_model_provider_override: str | None
llm_model_version_override: str | None
starter_messages: list[StarterMessage] | None
default_persona: bool
builtin_persona: bool
prompts: list[PromptSnapshot]
tools: list[ToolSnapshot]
document_sets: list[DocumentSet]
@ -63,6 +65,7 @@ class PersonaSnapshot(BaseModel):
icon_color: str | None
icon_shape: int | None
uploaded_image_id: str | None = None
is_default_persona: bool
@classmethod
def from_model(
@ -93,7 +96,8 @@ class PersonaSnapshot(BaseModel):
llm_model_provider_override=persona.llm_model_provider_override,
llm_model_version_override=persona.llm_model_version_override,
starter_messages=persona.starter_messages,
default_persona=persona.default_persona,
builtin_persona=persona.builtin_persona,
is_default_persona=persona.is_default_persona,
prompts=[PromptSnapshot.from_model(prompt) for prompt in persona.prompts],
tools=[ToolSnapshot.from_model(tool) for tool in persona.tools],
document_sets=[

View File

@ -40,6 +40,9 @@ class AuthTypeResponse(BaseModel):
class UserPreferences(BaseModel):
chosen_assistants: list[int] | None = None
hidden_assistants: list[int] = []
visible_assistants: list[int] = []
default_model: str | None = None
@ -73,6 +76,8 @@ class UserInfo(BaseModel):
UserPreferences(
chosen_assistants=user.chosen_assistants,
default_model=user.default_model,
hidden_assistants=user.hidden_assistants,
visible_assistants=user.visible_assistants,
)
),
# set to None if TRACK_EXTERNAL_IDP_EXPIRY is False so that we avoid cases

View File

@ -41,6 +41,7 @@ from danswer.dynamic_configs.factory import get_dynamic_config_store
from danswer.server.manage.models import AllUsersResponse
from danswer.server.manage.models import UserByEmail
from danswer.server.manage.models import UserInfo
from danswer.server.manage.models import UserPreferences
from danswer.server.manage.models import UserRoleResponse
from danswer.server.manage.models import UserRoleUpdateRequest
from danswer.server.models import FullUserSnapshot
@ -444,3 +445,64 @@ def update_user_assistant_list(
.values(chosen_assistants=request.chosen_assistants)
)
db_session.commit()
def update_assistant_list(
preferences: UserPreferences, assistant_id: int, show: bool
) -> UserPreferences:
visible_assistants = preferences.visible_assistants or []
hidden_assistants = preferences.hidden_assistants or []
chosen_assistants = preferences.chosen_assistants or []
if show:
if assistant_id not in visible_assistants:
visible_assistants.append(assistant_id)
if assistant_id in hidden_assistants:
hidden_assistants.remove(assistant_id)
if assistant_id not in chosen_assistants:
chosen_assistants.append(assistant_id)
else:
if assistant_id in visible_assistants:
visible_assistants.remove(assistant_id)
if assistant_id not in hidden_assistants:
hidden_assistants.append(assistant_id)
if assistant_id in chosen_assistants:
chosen_assistants.remove(assistant_id)
preferences.visible_assistants = visible_assistants
preferences.hidden_assistants = hidden_assistants
preferences.chosen_assistants = chosen_assistants
return preferences
@router.patch("/user/assistant-list/update/{assistant_id}")
def update_user_assistant_visibility(
assistant_id: int,
show: bool,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> None:
if user is None:
if AUTH_TYPE == AuthType.DISABLED:
store = get_dynamic_config_store()
no_auth_user = fetch_no_auth_user(store)
preferences = no_auth_user.preferences
updated_preferences = update_assistant_list(preferences, assistant_id, show)
set_no_auth_user_preferences(store, updated_preferences)
return
else:
raise RuntimeError("This should never happen")
user_preferences = UserInfo.from_model(user).preferences
updated_preferences = update_assistant_list(user_preferences, assistant_id, show)
db_session.execute(
update(User)
.where(User.id == user.id) # type: ignore
.values(
hidden_assistants=updated_preferences.hidden_assistants,
visible_assistants=updated_preferences.visible_assistants,
chosen_assistants=updated_preferences.chosen_assistants,
)
)
db_session.commit()

View File

@ -85,6 +85,7 @@ def _seed_personas(db_session: Session, personas: list[CreatePersonaRequest]) ->
is_public=persona.is_public,
db_session=db_session,
tool_ids=persona.tool_ids,
display_priority=persona.display_priority,
)

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;
}