From 8a8e2b310ed9b1382c593e0ad6157e75af0bd2b6 Mon Sep 17 00:00:00 2001 From: pablodanswer Date: Thu, 19 Sep 2024 16:36:15 -0700 Subject: [PATCH] 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 --- .../versions/55546a7967ee_assistant_rework.py | 79 ++++ backend/danswer/chat/load_yamls.py | 2 +- backend/danswer/db/models.py | 20 +- backend/danswer/db/persona.py | 22 +- backend/danswer/db/slack_bot_config.py | 2 +- .../danswer/server/features/persona/models.py | 8 +- backend/danswer/server/manage/models.py | 5 + backend/danswer/server/manage/users.py | 62 +++ backend/ee/danswer/server/seeding.py | 1 + .../app/admin/assistants/AssistantEditor.tsx | 6 +- web/src/app/admin/assistants/PersonaTable.tsx | 6 +- web/src/app/admin/assistants/interfaces.ts | 3 +- web/src/app/admin/assistants/lib.ts | 7 + .../app/assistants/AssistantSharedStatus.tsx | 14 +- .../app/assistants/AssistantsPageTitle.tsx | 3 +- web/src/app/assistants/ToolsDisplay.tsx | 38 +- .../assistants/gallery/AssistantsGallery.tsx | 437 +++++++++++------- .../assistants/mine/AssistantSharingModal.tsx | 107 ++--- .../app/assistants/mine/AssistantsList.tsx | 401 ++++++++-------- web/src/app/chat/ChatPage.tsx | 20 +- .../modal/configuration/AssistantsTab.tsx | 1 + web/src/app/chat/page.tsx | 1 + .../sessionSidebar/ChatSessionDisplay.tsx | 1 - web/src/components/Dropdown.tsx | 2 +- .../components/assistants/AssistantIcon.tsx | 7 +- web/src/components/popover/DefaultPopover.tsx | 22 +- web/src/components/tooltip/CustomTooltip.tsx | 2 +- web/src/lib/assistants/checkOwnership.ts | 2 +- web/src/lib/assistants/orderAssistants.ts | 34 -- .../assistants/updateAssistantPreferences.ts | 31 +- web/src/lib/assistants/utils.ts | 120 +++++ web/src/lib/credential.ts | 5 +- web/src/lib/types.ts | 2 + 33 files changed, 917 insertions(+), 556 deletions(-) create mode 100644 backend/alembic/versions/55546a7967ee_assistant_rework.py create mode 100644 web/src/lib/assistants/utils.ts 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 ? ( + + ) : ( + + )} +
+ )} +
+ +

{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 - + +
+ + +
-

- 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 - ) ? ( - - ) : ( - - )} -
- )} -
- -

{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}
+
+ +

+ {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 && ( - - )} -
+ {selectedUsers.length > 0 && ( + + )}
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 ? ( + + ) : ( + + ), + isOwnedByUser ? ( + + ) : null, + isOwnedByUser ? ( + + ) : null, + !assistant.is_public ? ( + + ) : 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 -
-
- +
+ - - -
- - View Available Assistants -
-
- +
-

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