diff --git a/backend/alembic/versions/55546a7967ee_assistant_rework.py b/backend/alembic/versions/55546a7967ee_assistant_rework.py
new file mode 100644
index 000000000..a027321a7
--- /dev/null
+++ b/backend/alembic/versions/55546a7967ee_assistant_rework.py
@@ -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"),
+ )
diff --git a/backend/danswer/chat/load_yamls.py b/backend/danswer/chat/load_yamls.py
index 0690f08b7..8d0fd34d8 100644
--- a/backend/danswer/chat/load_yamls.py
+++ b/backend/danswer/chat/load_yamls.py
@@ -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
diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py
index fd2d1344a..70e583ad9 100644
--- a/backend/danswer/db/models.py
+++ b/backend/danswer/db/models.py
@@ -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
),
)
diff --git a/backend/danswer/db/persona.py b/backend/danswer/db/persona.py
index 6cb93ad9f..316919e58 100644
--- a/backend/danswer/db/persona.py
+++ b/backend/danswer/db/persona.py
@@ -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)
diff --git a/backend/danswer/db/slack_bot_config.py b/backend/danswer/db/slack_bot_config.py
index a37bd18c0..1398057cf 100644
--- a/backend/danswer/db/slack_bot_config.py
+++ b/backend/danswer/db/slack_bot_config.py
@@ -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,
)
diff --git a/backend/danswer/server/features/persona/models.py b/backend/danswer/server/features/persona/models.py
index 777ef2037..0112cc110 100644
--- a/backend/danswer/server/features/persona/models.py
+++ b/backend/danswer/server/features/persona/models.py
@@ -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=[
diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py
index 7b0a3813a..e3be4c489 100644
--- a/backend/danswer/server/manage/models.py
+++ b/backend/danswer/server/manage/models.py
@@ -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
diff --git a/backend/danswer/server/manage/users.py b/backend/danswer/server/manage/users.py
index 04d2c1244..30b71ef0a 100644
--- a/backend/danswer/server/manage/users.py
+++ b/backend/danswer/server/manage/users.py
@@ -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()
diff --git a/backend/ee/danswer/server/seeding.py b/backend/ee/danswer/server/seeding.py
index ab6c4b017..b161f0570 100644
--- a/backend/ee/danswer/server/seeding.py
+++ b/backend/ee/danswer/server/seeding.py
@@ -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,
)
diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx
index 821949a5b..4951f459b 100644
--- a/web/src/app/admin/assistants/AssistantEditor.tsx
+++ b/web/src/app/admin/assistants/AssistantEditor.tsx
@@ -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.`,
diff --git a/web/src/app/admin/assistants/PersonaTable.tsx b/web/src/app/admin/assistants/PersonaTable.tsx
index be81d6aa4..e30e858f1 100644
--- a/web/src/app/admin/assistants/PersonaTable.tsx
+++ b/web/src/app/admin/assistants/PersonaTable.tsx
@@ -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 Built-In ;
}
@@ -119,7 +119,7 @@ export function PersonasTable({
id: persona.id.toString(),
cells: [
- {!persona.default_persona && (
+ {!persona.is_default_persona && (
@@ -173,7 +173,7 @@ export function PersonasTable({
,
- {!persona.default_persona && isEditable ? (
+ {!persona.is_default_persona && isEditable ? (
{
diff --git a/web/src/app/admin/assistants/interfaces.ts b/web/src/app/admin/assistants/interfaces.ts
index 0696b5ae8..a4528e26b 100644
--- a/web/src/app/admin/assistants/interfaces.ts
+++ b/web/src/app/admin/assistants/interfaces.ts
@@ -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;
diff --git a/web/src/app/admin/assistants/lib.ts b/web/src/app/admin/assistants/lib.ts
index ca2c36108..61ede3f16 100644
--- a/web/src/app/admin/assistants/lib.ts
+++ b/web/src/app/admin/assistants/lib.ts
@@ -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,
};
}
diff --git a/web/src/app/assistants/AssistantSharedStatus.tsx b/web/src/app/assistants/AssistantSharedStatus.tsx
index c5127c87e..f9c68e791 100644
--- a/web/src/app/assistants/AssistantSharedStatus.tsx
+++ b/web/src/app/assistants/AssistantSharedStatus.tsx
@@ -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 (
-
+
Public
@@ -27,7 +31,9 @@ export function AssistantSharedStatusDisplay({
if (assistantSharedUsersWithoutOwner.length > 0) {
return (
-
+
{isOwnedByUser ? (
`Shared with: ${
@@ -54,7 +60,9 @@ export function AssistantSharedStatusDisplay({
}
return (
-
+
Private
diff --git a/web/src/app/assistants/AssistantsPageTitle.tsx b/web/src/app/assistants/AssistantsPageTitle.tsx
index 5bcdea7ea..f0acc0c2c 100644
--- a/web/src/app/assistants/AssistantsPageTitle.tsx
+++ b/web/src/app/assistants/AssistantsPageTitle.tsx
@@ -6,10 +6,11 @@ export function AssistantsPageTitle({
return (
{children}
diff --git a/web/src/app/assistants/ToolsDisplay.tsx b/web/src/app/assistants/ToolsDisplay.tsx
index 2be7670c0..8efa02d55 100644
--- a/web/src/app/assistants/ToolsDisplay.tsx
+++ b/web/src/app/assistants/ToolsDisplay.tsx
@@ -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 (
-
-
Tools:
- {tools.map((tool) => {
- let toolName = tool.name;
- let toolIcon = null;
-
- if (tool.name === "SearchTool") {
- toolName = "Search";
- toolIcon =
;
- } else if (tool.name === "ImageGenerationTool") {
- toolName = "Image Generation";
- toolIcon =
;
- } else if (tool.name === "InternetSearchTool") {
- toolName = "Internet Search";
- toolIcon =
;
- }
-
- return (
-
-
- {toolIcon}
- {toolName}
-
-
- );
- })}
-
- );
-}
export function AssistantTools({
assistant,
diff --git a/web/src/app/assistants/gallery/AssistantsGallery.tsx b/web/src/app/assistants/gallery/AssistantsGallery.tsx
index d4882aa60..8926238b4 100644
--- a/web/src/app/assistants/gallery/AssistantsGallery.tsx
+++ b/web/src/app/assistants/gallery/AssistantsGallery.tsx
@@ -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 (
+
+
+
+
+ {assistant.name}
+
+ {user && (
+
+ {selectedAssistant ? (
+ {
+ 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
+
+ ) : (
+ {
+ 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
+
+ )}
+
+ )}
+
+
+
{assistant.description}
+
+ Author: {assistant.owner?.email || "Danswer"}
+
+ {assistant.tools.length > 0 && (
+
+ )}
+
+ );
+}
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}
Assistant Gallery
-
-
-
View Your Assistants
-
+
+
+ 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
+
+
+ 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
+
-
- Discover and create custom assistants that combine instructions, extra
- knowledge, and any combination of tools.
-
-
-
- setSearchQuery(e.target.value)}
- className="
- w-full
- p-2
- border
- border-gray-300
- rounded
- focus:outline-none
- focus:ring-2
- focus:ring-blue-500
- "
- />
-
-
- {filteredAssistants.map((assistant) => (
-
+
+
setSearchQuery(e.target.value)}
className="
- bg-background-emphasis
- rounded-lg
- shadow-md
- p-4
- "
- >
-
-
-
- {assistant.name}
-
- {user && (
-
- {!user.preferences?.chosen_assistants ||
- user.preferences?.chosen_assistants?.includes(
- assistant.id
- ) ? (
- {
- if (
- user.preferences?.chosen_assistants &&
- user.preferences?.chosen_assistants.length === 1
- ) {
- setPopup({
- message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
- type: "error",
- });
- return;
- }
-
- const success = await removeAssistantFromList(
- assistant.id,
- user.preferences?.chosen_assistants ||
- allAssistantIds
- );
- if (success) {
- setPopup({
- message: `"${assistant.name}" has been removed from your list.`,
- type: "success",
- });
- router.refresh();
- } else {
- setPopup({
- message: `"${assistant.name}" could not be removed from your list.`,
- type: "error",
- });
- }
- }}
- size="xs"
- color="blue"
- >
- Deselect
-
- ) : (
- {
- const success = await addAssistantToList(
- assistant.id,
- user.preferences?.chosen_assistants ||
- allAssistantIds
- );
- if (success) {
- setPopup({
- message: `"${assistant.name}" has been added to your list.`,
- type: "success",
- });
- router.refresh();
- } else {
- setPopup({
- message: `"${assistant.name}" could not be added to your list.`,
- type: "error",
- });
- }
- }}
- size="xs"
- color="green"
- >
- Add
-
- )}
-
- )}
-
-
-
{assistant.description}
-
- Author: {assistant.owner?.email || "Danswer"}
-
- {assistant.tools.length > 0 && (
-
- )}
+ 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
+ "
+ />
+
- ))}
+
+
+ {defaultAssistants.length == 0 &&
+ nonDefaultAssistants.length == 0 &&
+ assistants.length != 0 && (
+
+ No assistants found for this search
+
+ )}
+
+ {defaultAssistants.length > 0 && (
+ <>
+
+
+ Default Assistants
+
+
+
+ These are assistant created by your admins are and preferred.
+
+
+
+ {defaultAssistants.map((assistant) => (
+
+ ))}
+
+ >
+ )}
+
+ {nonDefaultAssistants.length > 0 && (
+
+
+
+ Other Assistants
+
+
+ These are community-contributed assistants.
+
+
+
+
+ {nonDefaultAssistants.map((assistant) => (
+
+ ))}
+
+
+ )}
>
);
diff --git a/web/src/app/assistants/mine/AssistantSharingModal.tsx b/web/src/app/assistants/mine/AssistantSharingModal.tsx
index 0fa55fea4..b0e96b000 100644
--- a/web/src/app/assistants/mine/AssistantSharingModal.tsx
+++ b/web/src/app/assistants/mine/AssistantSharingModal.tsx
@@ -73,7 +73,11 @@ export function AssistantSharingModal({
let sharedStatus = null;
if (assistant.is_public || !sharedUsersWithoutOwner.length) {
sharedStatus = (
-
+
);
} else {
sharedStatus = (
@@ -122,27 +126,30 @@ export function AssistantSharingModal({
<>
{popup}
- {" "}
- {assistantName}
+
}
onOutsideClick={onClose}
>
-
+
{isUpdating &&
}
-
- Control which other users should have access to this assistant.
-
+
+ Manage access to this assistant by sharing it with other users.
+
-
-
Current status:
- {sharedStatus}
+
+
Current Status
+
{sharedStatus}
-
Share Assistant:
-
+
+
Share Assistant
{
- 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 }) => (
-
-
- {option.name}
-
-
-
+
+
+ {option.name}
+
)}
/>
-
- {selectedUsers.length > 0 &&
- selectedUsers.map((selectedUser) => (
+
+
+ {selectedUsers.length > 0 && (
+
+
+ Selected Users:
+
+
+ {selectedUsers.map((selectedUser) => (
{
@@ -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}
+ {selectedUser.email}
+
))}
+
+ )}
- {selectedUsers.length > 0 && (
-
{
- handleShare();
- setSelectedUsers([]);
- }}
- size="xs"
- color="blue"
- >
- Add
-
- )}
-
+ {selectedUsers.length > 0 && (
+ {
+ handleShare();
+ setSelectedUsers([]);
+ }}
+ size="sm"
+ color="blue"
+ className="w-full"
+ >
+ Share with Selected Users
+
+ )}
>
diff --git a/web/src/app/assistants/mine/AssistantsList.tsx b/web/src/app/assistants/mine/AssistantsList.tsx
index 880dcad49..211ac3946 100644
--- a/web/src/app/assistants/mine/AssistantsList.tsx
+++ b/web/src/app/assistants/mine/AssistantsList.tsx
@@ -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 (
-
+
);
@@ -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
>;
shareAssistant: Dispatch>;
setPopup: (popupSpec: PopupSpec | null) => void;
+ isDragging?: boolean;
}) {
const router = useRouter();
const [showSharingModal, setShowSharingModal] = useState(false);
@@ -133,155 +138,154 @@ function AssistantListItem({
show={showSharingModal}
/>
-
-
-
-
-
- {assistant.name}
-
+
+
+
+
+ {assistant.name}
+
+
+ {/* {isOwnedByUser && ( */}
+
+
+ {assistant.tools.length > 0 && (
+
+ {assistant.tools.length} tool
+ {assistant.tools.length > 1 && "s"}
+
+ )}
+
-
{assistant.description}
-
-
- {assistant.tools.length != 0 && (
-
- )}
-
-
-
- {isOwnedByUser && (
-
- {!assistant.is_public && (
-
{
- e.stopPropagation();
- setShowSharingModal(true);
- }}
- >
-
-
- )}
+ {isOwnedByUser ? (
-
+
-
-
-
-
- }
- side="bottom"
- align="start"
- sideOffset={5}
+ ) : (
+
- {[
- isVisible ? (
- {
- if (
- currentChosenAssistants &&
- currentChosenAssistants.length === 1
- ) {
- setPopup({
- message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
- type: "error",
- });
- return;
- }
- const success = await removeAssistantFromList(
- assistant.id,
- currentChosenAssistants || allAssistantIds
- );
- if (success) {
- setPopup({
- message: `"${assistant.name}" has been removed from your list.`,
- type: "success",
- });
- router.refresh();
- } else {
- setPopup({
- message: `"${assistant.name}" could not be removed from your list.`,
- type: "error",
- });
- }
- }}
- >
- {isOwnedByUser ? "Hide" : "Remove"}
-
- ) : (
- {
- const success = await addAssistantToList(
- assistant.id,
- currentChosenAssistants || allAssistantIds
- );
- if (success) {
- setPopup({
- message: `"${assistant.name}" has been added to your list.`,
- type: "success",
- });
- router.refresh();
- } else {
- setPopup({
- message: `"${assistant.name}" could not be added to your list.`,
- type: "error",
- });
- }
- }}
- >
- Add
-
- ),
- isOwnedByUser ? (
- deleteAssistant(assistant)}
- >
- Delete
-
- ) : (
- <>>
- ),
- isOwnedByUser ? (
- shareAssistant(assistant)}
- >
- {assistant.is_public ? : } Make{" "}
- {assistant.is_public ? "Private" : "Public"}
-
- ) : (
- <>>
- ),
- ]}
-
-
- )}
+
+
+
+
+ )}
+
+
+
+
+ }
+ side="bottom"
+ align="end"
+ sideOffset={5}
+ >
+ {[
+ isVisible ? (
+
{
+ 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",
+ });
+ }
+ }}
+ >
+ {" "}
+ {isOwnedByUser ? "Hide" : "Remove"}
+
+ ) : (
+
{
+ 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",
+ });
+ }
+ }}
+ >
+ Add
+
+ ),
+ isOwnedByUser ? (
+
deleteAssistant(assistant)}
+ >
+ Delete
+
+ ) : null,
+ isOwnedByUser ? (
+
shareAssistant(assistant)}
+ >
+ {assistant.is_public ? (
+
+ ) : (
+
+ )}{" "}
+ Make {assistant.is_public ? "Private" : "Public"}
+
+ ) : null,
+ !assistant.is_public ? (
+
{
+ setShowSharingModal(true);
+ }}
+ >
+ Share
+
+ ) : null,
+ ]}
+
+
+ {/* )} */}
>
@@ -294,17 +298,19 @@ export function AssistantsList({
user: User | null;
assistants: Persona[];
}) {
- const [filteredAssistants, setFilteredAssistants] = useState([]);
+ // 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({
/>
)}
-
-
My Assistants
+
+
Your Assistants
-
-
-
-
-
- Create New Assistant
-
-
-
+
+
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
+
-
-
-
-
- View Available Assistants
-
-
-
+
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
+
-
- Assistants allow you to customize your experience for a specific
- purpose. Specifically, they combine instructions, extra knowledge, and
- any combination of tools.
-
+
+ Active Assistants
+
-
-
-
Active Assistants
-
-
+
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.
-
+
a.id.toString())}
+ items={currentlyVisibleAssistants.map((a) => a.id.toString())}
strategy={verticalListSortingStrategy}
>
-
- {filteredAssistants.map((assistant, index) => (
+
+ {currentlyVisibleAssistants.map((assistant, index) => (
Your Hidden Assistants
-
+
Assistants you've created that aren't currently visible
in the Assistants selector.
-
+
{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}
diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx
index 8e4bea443..403c40b3d 100644
--- a/web/src/app/chat/ChatPage.tsx
+++ b/web/src/app/chat/ChatPage.tsx
@@ -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
(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}
diff --git a/web/src/app/chat/modal/configuration/AssistantsTab.tsx b/web/src/app/chat/modal/configuration/AssistantsTab.tsx
index d4933771f..dcf31138c 100644
--- a/web/src/app/chat/modal/configuration/AssistantsTab.tsx
+++ b/web/src/app/chat/modal/configuration/AssistantsTab.tsx
@@ -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,
diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx
index e043f5446..72a1cf1aa 100644
--- a/web/src/app/chat/page.tsx
+++ b/web/src/app/chat/page.tsx
@@ -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,
diff --git a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx
index 35256ada9..aeea9b977 100644
--- a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx
+++ b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx
@@ -207,7 +207,6 @@ export function ChatSessionDisplay({
{
e.preventDefault();
- // e.stopPropagation();
setIsMoreOptionsDropdownOpen(
!isMoreOptionsDropdownOpen
);
diff --git a/web/src/components/Dropdown.tsx b/web/src/components/Dropdown.tsx
index 79fe03083..5e36dca98 100644
--- a/web/src/components/Dropdown.tsx
+++ b/web/src/components/Dropdown.tsx
@@ -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;
diff --git a/web/src/components/assistants/AssistantIcon.tsx b/web/src/components/assistants/AssistantIcon.tsx
index 4855e9576..07ab05aa1 100644
--- a/web/src/components/assistants/AssistantIcon.tsx
+++ b/web/src/components/assistants/AssistantIcon.tsx
@@ -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 (
-
+
{
// Prioritization order: image, graph, defaults
assistant.uploaded_image_id ? (
@@ -44,7 +45,7 @@ export function AssistantIcon({
{createSVG(
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
@@ -62,6 +63,6 @@ export function AssistantIcon({
/>
)
}
-
+
);
}
diff --git a/web/src/components/popover/DefaultPopover.tsx b/web/src/components/popover/DefaultPopover.tsx
index 6032d7909..ff017d972 100644
--- a/web/src/components/popover/DefaultPopover.tsx
+++ b/web/src/components/popover/DefaultPopover.tsx
@@ -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) => (
-
setPopoverOpen(false)}
- >
- {child}
-
- ))}
+ {props.children
+ .filter((child) => child !== null)
+ .map((child, index) => (
+
setPopoverOpen(false)}
+ >
+ {child}
+
+ ))}
}
{...props}
diff --git a/web/src/components/tooltip/CustomTooltip.tsx b/web/src/components/tooltip/CustomTooltip.tsx
index 6695b99eb..2f4ca2d12 100644
--- a/web/src/components/tooltip/CustomTooltip.tsx
+++ b/web/src/components/tooltip/CustomTooltip.tsx
@@ -121,7 +121,7 @@ export const CustomTooltip = ({
{isVisible &&
createPortal(
[
- 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;
-}
diff --git a/web/src/lib/assistants/updateAssistantPreferences.ts b/web/src/lib/assistants/updateAssistantPreferences.ts
index 61a0128b2..e996867a3 100644
--- a/web/src/lib/assistants/updateAssistantPreferences.ts
+++ b/web/src/lib/assistants/updateAssistantPreferences.ts
@@ -11,24 +11,33 @@ export async function updateUserAssistantList(
return response.ok;
}
+export async function updateAssistantVisibility(
+ assistantId: number,
+ show: boolean
+): Promise {
+ 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 {
- 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 {
- if (!chosenAssistants.includes(assistantId)) {
- const updatedAssistants = [...chosenAssistants, assistantId];
- return updateUserAssistantList(updatedAssistants);
- }
- return false;
+ return updateAssistantVisibility(assistantId, true);
}
export async function moveAssistantUp(
diff --git a/web/src/lib/assistants/utils.ts b/web/src/lib/assistants/utils.ts
new file mode 100644
index 000000000..208c2ae98
--- /dev/null
+++ b/web/src/lib/assistants/utils.ts
@@ -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)
+ );
+}
diff --git a/web/src/lib/credential.ts b/web/src/lib/credential.ts
index c65968047..f39f3dded 100644
--- a/web/src/lib/credential.ts
+++ b/web/src/lib/credential.ts
@@ -32,10 +32,7 @@ export async function deleteCredential(
});
}
-export async function forceDeleteCredential(
- credentialId: number,
- force?: boolean
-) {
+export async function forceDeleteCredential(credentialId: number) {
return await fetch(`/api/manage/credential/force/${credentialId}`, {
method: "DELETE",
headers: {
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index bf342ca40..1ea67d140 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -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;
}