From 12b2126e6970b6e855c2a8f2265cd92b8f2d8981 Mon Sep 17 00:00:00 2001 From: pablonyx Date: Tue, 11 Feb 2025 16:43:20 -0800 Subject: [PATCH] Update assistants visibility, minor UX, .. (#3965) * update assistant logic * quick nit * k * fix "featured" logic * Small tweaks * k --------- Co-authored-by: Weves --- .../2cdeff6d8c93_set_built_in_to_default.py | 32 ++++ backend/onyx/db/persona.py | 26 +++ backend/onyx/server/features/persona/api.py | 25 ++- web/README.md | 6 +- .../app/admin/assistants/AssistantEditor.tsx | 135 +++++++++++---- web/src/app/admin/assistants/PersonaTable.tsx | 93 +++++++++- .../assistants/[id]/DeletePersonaButton.tsx | 36 ---- web/src/app/admin/assistants/[id]/page.tsx | 43 ----- web/src/app/admin/assistants/lib.ts | 16 ++ web/src/app/admin/assistants/new/page.tsx | 25 --- web/src/app/admin/assistants/page.tsx | 2 +- .../[bot-id]/SlackChannelConfigsTable.tsx | 2 +- .../embeddings/pages/CloudEmbeddingPage.tsx | 4 +- web/src/app/assistants/mine/AssistantCard.tsx | 160 ++++++++++++------ .../app/assistants/mine/AssistantModal.tsx | 22 ++- web/src/app/chat/ChatPage.tsx | 6 +- .../chat/sessionSidebar/HistorySidebar.tsx | 93 +++++++--- web/src/app/globals.css | 2 + .../admin/users/buttons/DeleteUserButton.tsx | 4 +- .../users/buttons/LeaveOrganizationButton.tsx | 7 +- .../components/context/AssistantsContext.tsx | 4 +- .../components/modals/ConfirmEntityModal.tsx | 67 ++++++++ .../components/modals/DeleteEntityModal.tsx | 51 ------ web/tailwind-themes/tailwind.config.js | 1 + 24 files changed, 571 insertions(+), 291 deletions(-) create mode 100644 backend/alembic/versions/2cdeff6d8c93_set_built_in_to_default.py delete mode 100644 web/src/app/admin/assistants/[id]/DeletePersonaButton.tsx delete mode 100644 web/src/app/admin/assistants/[id]/page.tsx delete mode 100644 web/src/app/admin/assistants/new/page.tsx create mode 100644 web/src/components/modals/ConfirmEntityModal.tsx delete mode 100644 web/src/components/modals/DeleteEntityModal.tsx diff --git a/backend/alembic/versions/2cdeff6d8c93_set_built_in_to_default.py b/backend/alembic/versions/2cdeff6d8c93_set_built_in_to_default.py new file mode 100644 index 000000000..c856fdeb7 --- /dev/null +++ b/backend/alembic/versions/2cdeff6d8c93_set_built_in_to_default.py @@ -0,0 +1,32 @@ +"""set built in to default + +Revision ID: 2cdeff6d8c93 +Revises: f5437cc136c5 +Create Date: 2025-02-11 14:57:51.308775 + +""" +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "2cdeff6d8c93" +down_revision = "f5437cc136c5" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Prior to this migration / point in the codebase history, + # built in personas were implicitly treated as default personas (with no option to change this) + # This migration makes that explicit + op.execute( + """ + UPDATE persona + SET is_default_persona = TRUE + WHERE builtin_persona = TRUE + """ + ) + + +def downgrade() -> None: + pass diff --git a/backend/onyx/db/persona.py b/backend/onyx/db/persona.py index e37cde55d..250b26117 100644 --- a/backend/onyx/db/persona.py +++ b/backend/onyx/db/persona.py @@ -204,6 +204,14 @@ def create_update_persona( if not all_prompt_ids: raise ValueError("No prompt IDs provided") + # Default persona validation + if create_persona_request.is_default_persona: + if not create_persona_request.is_public: + raise ValueError("Cannot make a default persona non public") + + if user and user.role != UserRole.ADMIN: + raise ValueError("Only admins can make a default persona") + persona = upsert_persona( persona_id=persona_id, user=user, @@ -510,6 +518,7 @@ def upsert_persona( existing_persona.is_visible = is_visible existing_persona.search_start_date = search_start_date existing_persona.labels = labels or [] + existing_persona.is_default_persona = is_default_persona # Do not delete any associations manually added unless # a new updated list is provided if document_sets is not None: @@ -590,6 +599,23 @@ def delete_old_default_personas( db_session.commit() +def update_persona_is_default( + persona_id: int, + is_default: bool, + db_session: Session, + user: User | None = None, +) -> None: + persona = fetch_persona_by_id_for_user( + db_session=db_session, persona_id=persona_id, user=user, get_editable=True + ) + + if not persona.is_public: + persona.is_public = True + + persona.is_default_persona = is_default + db_session.commit() + + def update_persona_visibility( persona_id: int, is_visible: bool, diff --git a/backend/onyx/server/features/persona/api.py b/backend/onyx/server/features/persona/api.py index a1977c9cb..18c792c11 100644 --- a/backend/onyx/server/features/persona/api.py +++ b/backend/onyx/server/features/persona/api.py @@ -32,6 +32,7 @@ from onyx.db.persona import get_personas_for_user from onyx.db.persona import mark_persona_as_deleted from onyx.db.persona import mark_persona_as_not_deleted from onyx.db.persona import update_all_personas_display_priority +from onyx.db.persona import update_persona_is_default from onyx.db.persona import update_persona_label from onyx.db.persona import update_persona_public_status from onyx.db.persona import update_persona_shared_users @@ -56,7 +57,6 @@ from onyx.tools.utils import is_image_generation_available from onyx.utils.logger import setup_logger from onyx.utils.telemetry import create_milestone_and_report - logger = setup_logger() @@ -72,6 +72,10 @@ class IsPublicRequest(BaseModel): is_public: bool +class IsDefaultRequest(BaseModel): + is_default_persona: bool + + @admin_router.patch("/{persona_id}/visible") def patch_persona_visibility( persona_id: int, @@ -106,6 +110,25 @@ def patch_user_presona_public_status( raise HTTPException(status_code=403, detail=str(e)) +@admin_router.patch("/{persona_id}/default") +def patch_persona_default_status( + persona_id: int, + is_default_request: IsDefaultRequest, + user: User | None = Depends(current_curator_or_admin_user), + db_session: Session = Depends(get_session), +) -> None: + try: + update_persona_is_default( + persona_id=persona_id, + is_default=is_default_request.is_default_persona, + db_session=db_session, + user=user, + ) + except ValueError as e: + logger.exception("Failed to update persona default status") + raise HTTPException(status_code=403, detail=str(e)) + + @admin_router.put("/display-priority") def patch_persona_display_priority( display_priority_request: DisplayPriorityRequest, diff --git a/web/README.md b/web/README.md index 91d8fd067..6644db26b 100644 --- a/web/README.md +++ b/web/README.md @@ -23,12 +23,12 @@ _Note:_ if you are having problems accessing the ^, try setting the `WEB_DOMAIN` `http://127.0.0.1:3000` and accessing it there. ## Testing -This testing process will reset your application into a clean state. + +This testing process will reset your application into a clean state. Don't run these tests if you don't want to do this! Bring up the entire application. - 1. Reset the instance ```cd backend @@ -59,4 +59,4 @@ may use this for local troubleshooting and testing. ``` cd web npx chromatic --playwright --project-token={your token here} -``` \ No newline at end of file +``` diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx index 2bab0423f..b1892ddff 100644 --- a/web/src/app/admin/assistants/AssistantEditor.tsx +++ b/web/src/app/admin/assistants/AssistantEditor.tsx @@ -3,7 +3,13 @@ import React from "react"; import { Option } from "@/components/Dropdown"; import { generateRandomIconShape } from "@/lib/assistantIconUtils"; -import { CCPairBasicInfo, DocumentSet, User, UserGroup } from "@/lib/types"; +import { + CCPairBasicInfo, + DocumentSet, + User, + UserGroup, + UserRole, +} from "@/lib/types"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { ArrayHelpers, FieldArray, Form, Formik, FormikProps } from "formik"; @@ -33,9 +39,8 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; -import { FiInfo } from "react-icons/fi"; import * as Yup from "yup"; import CollapsibleSection from "./CollapsibleSection"; import { SuccessfulPersonaUpdateRedirectType } from "./enums"; @@ -71,11 +76,11 @@ import { Option as DropdownOption, } from "@/components/Dropdown"; import { SourceChip } from "@/app/chat/input/ChatInputBar"; -import { TagIcon, UserIcon, XIcon } from "lucide-react"; +import { TagIcon, UserIcon, XIcon, InfoIcon } from "lucide-react"; import { LLMSelector } from "@/components/llm/LLMSelector"; import useSWR from "swr"; import { errorHandlingFetcher } from "@/lib/fetcher"; -import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; +import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal"; import Title from "@/components/ui/title"; import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants"; @@ -127,6 +132,8 @@ export function AssistantEditor({ }) { const { refreshAssistants, isImageGenerationAvailable } = useAssistants(); const router = useRouter(); + const searchParams = useSearchParams(); + const isAdminPage = searchParams.get("admin") === "true"; const { popup, setPopup } = usePopup(); const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } = @@ -216,6 +223,8 @@ export function AssistantEditor({ enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id); }); + const [showVisibilityWarning, setShowVisibilityWarning] = useState(false); + const initialValues = { name: existingPersona?.name ?? "", description: existingPersona?.description ?? "", @@ -252,6 +261,7 @@ export function AssistantEditor({ (u) => u.id !== existingPersona.owner?.id ) ?? [], selectedGroups: existingPersona?.groups ?? [], + is_default_persona: existingPersona?.is_default_persona ?? false, }; interface AssistantPrompt { @@ -308,24 +318,12 @@ export function AssistantEditor({ const [isRequestSuccessful, setIsRequestSuccessful] = useState(false); const { data: userGroups } = useUserGroups(); - // const { data: allUsers } = useUsers({ includeApiKeys: false }) as { - // data: MinimalUserSnapshot[] | undefined; - // }; const { data: users } = useSWR( "/api/users", errorHandlingFetcher ); - const mapUsersToMinimalSnapshot = (users: any): MinimalUserSnapshot[] => { - if (!users || !Array.isArray(users.users)) return []; - return users.users.map((user: any) => ({ - id: user.id, - name: user.name, - email: user.email, - })); - }; - const [deleteModalOpen, setDeleteModalOpen] = useState(false); if (!labels) { @@ -346,9 +344,7 @@ export function AssistantEditor({ if (response.ok) { await refreshAssistants(); router.push( - redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN - ? `/admin/assistants?u=${Date.now()}` - : `/chat` + isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat` ); } else { setPopup({ @@ -374,8 +370,9 @@ export function AssistantEditor({ )} + {labelToDelete && ( - setLabelToDelete(null)} @@ -398,7 +395,7 @@ export function AssistantEditor({ /> )} {deleteModalOpen && existingPersona && ( - { if ( @@ -499,7 +510,6 @@ export function AssistantEditor({ const submissionData: PersonaUpsertParameters = { ...values, existing_prompt_id: existingPrompt?.id ?? null, - is_default_persona: admin!, starter_messages: starterMessages, groups: groups, users: values.is_public @@ -563,8 +573,9 @@ export function AssistantEditor({ } await refreshAssistants(); + router.push( - redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN + isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat?assistantId=${assistantId}` ); @@ -1005,6 +1016,22 @@ export function AssistantEditor({ {showAdvancedOptions && ( <>
+ {user?.role == UserRole.ADMIN && ( + { + if (checked) { + setFieldValue("is_public", true); + setFieldValue("is_default_persona", true); + } + }} + name="is_default_persona" + label="Featured Assistant" + subtext="If set, this assistant will be pinned for all new users and appear in the Featured list in the assistant explorer. This also makes the assistant public." + /> + )} + + +
Access
@@ -1014,22 +1041,60 @@ export function AssistantEditor({
- { - setFieldValue("is_public", checked); - if (checked) { - setFieldValue("selectedUsers", []); - setFieldValue("selectedGroups", []); - } - }} - /> + + + +
+ { + if (values.is_default_persona && !checked) { + setShowVisibilityWarning(true); + } else { + setFieldValue("is_public", checked); + if (!checked) { + // Even though this code path should not be possible, + // we set the default persona to false to be safe + setFieldValue( + "is_default_persona", + false + ); + } + if (checked) { + setFieldValue("selectedUsers", []); + setFieldValue("selectedGroups", []); + } + } + }} + disabled={values.is_default_persona} + /> +
+
+ {values.is_default_persona && ( + + Default persona must be public. Set + "Default Persona" to false to change + visibility. + + )} +
+
{values.is_public ? "Public" : "Private"}
+ {showVisibilityWarning && ( +
+ + + Default persona must be public. Visibility has been + automatically set to public. + +
+ )} + {values.is_public ? (

Anyone from your organization can view and use this diff --git a/web/src/app/admin/assistants/PersonaTable.tsx b/web/src/app/admin/assistants/PersonaTable.tsx index 30fda44ad..1177e91ac 100644 --- a/web/src/app/admin/assistants/PersonaTable.tsx +++ b/web/src/app/admin/assistants/PersonaTable.tsx @@ -11,13 +11,14 @@ import { DraggableTable } from "@/components/table/DraggableTable"; import { deletePersona, personaComparator, + togglePersonaDefault, togglePersonaVisibility, } from "./lib"; import { FiEdit2 } from "react-icons/fi"; import { TrashIcon } from "@/components/icons/icons"; import { useUser } from "@/components/user/UserProvider"; import { useAssistants } from "@/components/context/AssistantsContext"; -import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; +import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal"; function PersonaTypeDisplay({ persona }: { persona: Persona }) { if (persona.builtin_persona) { @@ -56,6 +57,9 @@ export function PersonasTable() { const [finalPersonas, setFinalPersonas] = useState([]); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [personaToDelete, setPersonaToDelete] = useState(null); + const [defaultModalOpen, setDefaultModalOpen] = useState(false); + const [personaToToggleDefault, setPersonaToToggleDefault] = + useState(null); useEffect(() => { const editable = editablePersonas.sort(personaComparator); @@ -126,11 +130,39 @@ export function PersonasTable() { } }; + const openDefaultModal = (persona: Persona) => { + setPersonaToToggleDefault(persona); + setDefaultModalOpen(true); + }; + + const closeDefaultModal = () => { + setDefaultModalOpen(false); + setPersonaToToggleDefault(null); + }; + + const handleToggleDefault = async () => { + if (personaToToggleDefault) { + const response = await togglePersonaDefault( + personaToToggleDefault.id, + personaToToggleDefault.is_default_persona + ); + if (response.ok) { + await refreshAssistants(); + closeDefaultModal(); + } else { + setPopup({ + type: "error", + message: `Failed to update persona - ${await response.text()}`, + }); + } + } + }; + return (

{popup} {deleteModalOpen && personaToDelete && ( - )} + {defaultModalOpen && personaToToggleDefault && ( + + )} + { const isEditable = editablePersonas.includes(persona); @@ -152,7 +211,9 @@ export function PersonasTable() { className="mr-1 my-auto cursor-pointer" onClick={() => router.push( - `/admin/assistants/${persona.id}?u=${Date.now()}` + `/assistants/edit/${ + persona.id + }?u=${Date.now()}&admin=true` ) } /> @@ -168,6 +229,30 @@ export function PersonasTable() { {persona.description}

, , +
{ + if (isEditable) { + openDefaultModal(persona); + } + }} + className={`px-1 py-0.5 rounded flex ${ + isEditable + ? "hover:bg-accent-background-hovered cursor-pointer" + : "" + } select-none w-fit`} + > +
+ {!persona.is_default_persona ? ( +
Not Featured
+ ) : ( + "Featured" + )} +
+
+ +
+
,
{ diff --git a/web/src/app/admin/assistants/[id]/DeletePersonaButton.tsx b/web/src/app/admin/assistants/[id]/DeletePersonaButton.tsx deleted file mode 100644 index dc12ca5d9..000000000 --- a/web/src/app/admin/assistants/[id]/DeletePersonaButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { deletePersona } from "../lib"; -import { useRouter } from "next/navigation"; -import { SuccessfulPersonaUpdateRedirectType } from "../enums"; - -export function DeletePersonaButton({ - personaId, - redirectType, -}: { - personaId: number; - redirectType: SuccessfulPersonaUpdateRedirectType; -}) { - const router = useRouter(); - - return ( - - ); -} diff --git a/web/src/app/admin/assistants/[id]/page.tsx b/web/src/app/admin/assistants/[id]/page.tsx deleted file mode 100644 index e3419e271..000000000 --- a/web/src/app/admin/assistants/[id]/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ErrorCallout } from "@/components/ErrorCallout"; -import { AssistantEditor } from "../AssistantEditor"; -import { BackButton } from "@/components/BackButton"; - -import { DeletePersonaButton } from "./DeletePersonaButton"; -import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS"; -import { SuccessfulPersonaUpdateRedirectType } from "../enums"; -import { RobotIcon } from "@/components/icons/icons"; -import { AdminPageTitle } from "@/components/admin/Title"; -import CardSection from "@/components/admin/CardSection"; -import Title from "@/components/ui/title"; - -export default async function Page(props: { params: Promise<{ id: string }> }) { - const params = await props.params; - const [values, error] = await fetchAssistantEditorInfoSS(params.id); - - let body; - if (!values) { - body = ( - - ); - } else { - body = ( - <> - - - - - ); - } - - return ( -
- } /> - {body} -
- ); -} diff --git a/web/src/app/admin/assistants/lib.ts b/web/src/app/admin/assistants/lib.ts index 030c9440c..a6494782f 100644 --- a/web/src/app/admin/assistants/lib.ts +++ b/web/src/app/admin/assistants/lib.ts @@ -261,6 +261,22 @@ export function personaComparator(a: Persona, b: Persona) { return closerToZeroNegativesFirstComparator(a.id, b.id); } +export const togglePersonaDefault = async ( + personaId: number, + isDefault: boolean +) => { + const response = await fetch(`/api/admin/persona/${personaId}/default`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + is_default_persona: !isDefault, + }), + }); + return response; +}; + export const togglePersonaVisibility = async ( personaId: number, isVisible: boolean diff --git a/web/src/app/admin/assistants/new/page.tsx b/web/src/app/admin/assistants/new/page.tsx deleted file mode 100644 index 2ccd7e177..000000000 --- a/web/src/app/admin/assistants/new/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { AssistantEditor } from "../AssistantEditor"; -import { ErrorCallout } from "@/components/ErrorCallout"; -import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS"; -import { SuccessfulPersonaUpdateRedirectType } from "../enums"; - -export default async function Page() { - const [values, error] = await fetchAssistantEditorInfoSS(); - - if (!values) { - return ( - - ); - } else { - return ( -
- -
- ); - } -} diff --git a/web/src/app/admin/assistants/page.tsx b/web/src/app/admin/assistants/page.tsx index 7acd03f8d..3764a6079 100644 --- a/web/src/app/admin/assistants/page.tsx +++ b/web/src/app/admin/assistants/page.tsx @@ -29,7 +29,7 @@ export default async function Page() { Create an Assistant - + diff --git a/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx b/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx index ac0ccabe2..590252ce2 100644 --- a/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx +++ b/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx @@ -100,7 +100,7 @@ export function SlackChannelConfigsTable({ slackChannelConfig.persona ) ? ( {slackChannelConfig.persona.name} diff --git a/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx b/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx index 2dacd5ba7..16c6c8252 100644 --- a/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx +++ b/web/src/app/admin/embeddings/pages/CloudEmbeddingPage.tsx @@ -19,7 +19,7 @@ import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { CustomEmbeddingModelForm } from "@/components/embedding/CustomEmbeddingModelForm"; import { deleteSearchSettings } from "./utils"; import { usePopup } from "@/components/admin/connectors/Popup"; -import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; +import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal"; import { AdvancedSearchConfiguration } from "../interfaces"; import CardSection from "@/components/admin/CardSection"; @@ -456,7 +456,7 @@ export function CloudModelCard({ > {popup} {showDeleteModel && ( - deleteModel()} diff --git a/web/src/app/assistants/mine/AssistantCard.tsx b/web/src/app/assistants/mine/AssistantCard.tsx index 896c8d5d3..b95d79fb5 100644 --- a/web/src/app/assistants/mine/AssistantCard.tsx +++ b/web/src/app/assistants/mine/AssistantCard.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState, useRef, useLayoutEffect } from "react"; +import React, { useState, useRef, useLayoutEffect } from "react"; import { useRouter } from "next/navigation"; import { FiMoreHorizontal, @@ -8,7 +8,7 @@ import { FiLock, FiUnlock, } from "react-icons/fi"; -import { FaHashtag } from "react-icons/fa"; + import { Popover, PopoverTrigger, @@ -26,14 +26,12 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { PinnedIcon } from "@/components/icons/icons"; -import { - deletePersona, - togglePersonaPublicStatus, -} from "@/app/admin/assistants/lib"; +import { deletePersona } from "@/app/admin/assistants/lib"; import { PencilIcon } from "lucide-react"; -import { SettingsContext } from "@/components/settings/SettingsProvider"; import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled"; import { truncateString } from "@/lib/utils"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { Button } from "@/components/ui/button"; export const AssistantBadge = ({ text, @@ -63,6 +61,7 @@ const AssistantCard: React.FC<{ const { user, toggleAssistantPinnedStatus } = useUser(); const router = useRouter(); const { refreshAssistants, pinnedAssistants } = useAssistants(); + const { popup, setPopup } = usePopup(); const isOwnedByUser = checkUserOwnsAssistant(user, persona); @@ -72,7 +71,34 @@ const AssistantCard: React.FC<{ const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled(); - const handleDelete = () => setActivePopover("delete"); + const [isDeleteConfirmation, setIsDeleteConfirmation] = useState(false); + + const handleDelete = () => { + setIsDeleteConfirmation(true); + }; + + const confirmDelete = async () => { + const response = await deletePersona(persona.id); + if (response.ok) { + await refreshAssistants(); + setActivePopover(null); + setIsDeleteConfirmation(false); + setPopup({ + message: `${persona.name} has been successfully deleted.`, + type: "success", + }); + } else { + setPopup({ + message: `Failed to delete assistant - ${await response.text()}`, + type: "error", + }); + } + }; + + const cancelDelete = () => { + setIsDeleteConfirmation(false); + }; + const handleEdit = () => { router.push(`/assistants/edit/${persona.id}`); setActivePopover(null); @@ -100,6 +126,7 @@ const AssistantCard: React.FC<{ return (
+ {popup}
@@ -148,7 +175,7 @@ const AssistantCard: React.FC<{
{isOwnedByUser && (
- + - -
- - {isPaidEnterpriseFeaturesEnabled && isOwnedByUser && ( + + {!isDeleteConfirmation ? ( +
- )} - -
+ {isPaidEnterpriseFeaturesEnabled && isOwnedByUser && ( + + )} + +
+ ) : ( +
+

+ Are you sure you want to delete assistant{" "} + {persona.name}? +

+
+ + +
+
+ )}
diff --git a/web/src/app/assistants/mine/AssistantModal.tsx b/web/src/app/assistants/mine/AssistantModal.tsx index 78debbbbc..b5876c36d 100644 --- a/web/src/app/assistants/mine/AssistantModal.tsx +++ b/web/src/app/assistants/mine/AssistantModal.tsx @@ -5,9 +5,8 @@ import { useRouter } from "next/navigation"; import AssistantCard from "./AssistantCard"; import { useAssistants } from "@/components/context/AssistantsContext"; import { useUser } from "@/components/user/UserProvider"; -import { FilterIcon } from "lucide-react"; +import { FilterIcon, XIcon } from "lucide-react"; import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; export const AssistantBadgeSelector = ({ text, @@ -108,16 +107,20 @@ export function AssistantModal({ const featuredAssistants = [ ...memoizedCurrentlyVisibleAssistants.filter( - (assistant) => assistant.builtin_persona || assistant.is_default_persona + (assistant) => assistant.is_default_persona ), ]; const allAssistants = memoizedCurrentlyVisibleAssistants.filter( - (assistant) => !assistant.builtin_persona && !assistant.is_default_persona + (assistant) => !assistant.is_default_persona ); return ( -
+
e.stopPropagation()} className="p-0 max-w-4xl overflow-hidden max-h-[80vh] w-[95%] bg-background rounded-md shadow-2xl transform transition-all duration-300 ease-in-out relative w-11/12 max-w-4xl pt-10 pb-10 px-10 overflow-hidden flex flex-col" style={{ position: "fixed", @@ -127,6 +130,15 @@ export function AssistantModal({ margin: 0, }} > +
+ +
diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index e1584d90d..22df8a6fc 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -97,7 +97,6 @@ import { } from "@/components/resizable/constants"; import FixedLogo from "../../components/logo/FixedLogo"; -import { DeleteEntityModal } from "../../components/modals/DeleteEntityModal"; import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown"; import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal"; @@ -130,6 +129,7 @@ import { useSidebarShortcut, } from "@/lib/browserUtilities"; import { Button } from "@/components/ui/button"; +import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal"; const TEMP_USER_MESSAGE_ID = -1; const TEMP_ASSISTANT_MESSAGE_ID = -2; @@ -2122,7 +2122,7 @@ export function ChatPage({ {showDeleteAllModal && ( - setShowDeleteAllModal(false)} @@ -2287,6 +2287,7 @@ export function ChatPage({ >
setMessage("")} @@ -2294,7 +2295,6 @@ export function ChatPage({ ref={innerSidebarElementRef} toggleSidebar={toggleSidebar} toggled={sidebarVisible} - currentAssistantId={liveAssistant?.id} existingChats={chatSessions} currentChatSession={selectedChatSession} folders={folders} diff --git a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx index b2cdaecde..819d5f0b6 100644 --- a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx +++ b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx @@ -50,10 +50,12 @@ import { } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { CircleX } from "lucide-react"; +import { CirclePlus, CircleX, PinIcon } from "lucide-react"; import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { turborepoTraceAccess } from "next/dist/build/turborepo-access-trace"; interface HistorySidebarProps { + liveAssistant?: Persona | null; page: pageType; existingChats?: ChatSession[]; currentChatSession?: ChatSession | null | undefined; @@ -66,22 +68,23 @@ interface HistorySidebarProps { showDeleteModal?: (chatSession: ChatSession) => void; explicitlyUntoggle: () => void; showDeleteAllModal?: () => void; - currentAssistantId?: number | null; setShowAssistantsModal: (show: boolean) => void; } interface SortableAssistantProps { assistant: Persona; - currentAssistantId: number | null | undefined; + active: boolean; onClick: () => void; - onUnpin: (e: React.MouseEvent) => void; + onPinAction: (e: React.MouseEvent) => void; + pinned?: boolean; } const SortableAssistant: React.FC = ({ assistant, - currentAssistantId, + active, onClick, - onUnpin, + onPinAction, + pinned = true, }) => { const { attributes, @@ -126,7 +129,9 @@ const SortableAssistant: React.FC = ({ >
= ({ } }} className={`cursor-pointer w-full group hover:bg-background-chat-hover ${ - currentAssistantId === assistant.id - ? "bg-background-chat-hover/60" - : "" + active ? "bg-accent-background-selected" : "" } relative flex items-center gap-x-2 py-1 px-2 rounded-md`} > @@ -164,15 +167,36 @@ const SortableAssistant: React.FC = ({ > {assistant.name} - + + + + + + + {pinned + ? "Unpin this assistant from the sidebar" + : "Pin this assistant to the sidebar"} + + +
); @@ -181,6 +205,7 @@ const SortableAssistant: React.FC = ({ export const HistorySidebar = forwardRef( ( { + liveAssistant, reset = () => null, setShowAssistantsModal = () => null, toggled, @@ -194,7 +219,6 @@ export const HistorySidebar = forwardRef( showShareModal, showDeleteModal, showDeleteAllModal, - currentAssistantId, }, ref: ForwardedRef ) => { @@ -353,13 +377,13 @@ export const HistorySidebar = forwardRef( { router.push( buildChatUrl(searchParams, null, assistant.id) ); }} - onUnpin={async (e: React.MouseEvent) => { + onPinAction={async (e: React.MouseEvent) => { e.stopPropagation(); await toggleAssistantPinnedStatus( pinnedAssistants.map((a) => a.id), @@ -373,6 +397,31 @@ export const HistorySidebar = forwardRef(
+ {!pinnedAssistants.some((a) => a.id === liveAssistant?.id) && + liveAssistant && ( +
+ { + router.push( + buildChatUrl(searchParams, null, liveAssistant.id) + ); + }} + onPinAction={async (e: React.MouseEvent) => { + e.stopPropagation(); + await toggleAssistantPinnedStatus( + [...pinnedAssistants.map((a) => a.id)], + liveAssistant.id, + true + ); + await refreshAssistants(); + }} + /> +
+ )} +
+ )} + +
+ + + ); +}; diff --git a/web/src/components/modals/DeleteEntityModal.tsx b/web/src/components/modals/DeleteEntityModal.tsx deleted file mode 100644 index 200163d58..000000000 --- a/web/src/components/modals/DeleteEntityModal.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { FiTrash, FiX } from "react-icons/fi"; -import { BasicClickable } from "@/components/BasicClickable"; -import { Modal } from "../Modal"; -import { Button } from "../ui/button"; - -export const DeleteEntityModal = ({ - onClose, - onSubmit, - entityType, - entityName, - additionalDetails, - deleteButtonText, - includeCancelButton = true, -}: { - entityType: string; - entityName: string; - onClose: () => void; - onSubmit: () => void; - additionalDetails?: string; - deleteButtonText?: string; - includeCancelButton?: boolean; -}) => { - return ( - - <> -
-

- {deleteButtonText || `Delete`} {entityType} -

-
-

- Are you sure you want to {deleteButtonText || "delete"}{" "} - {entityName}? -

- {additionalDetails &&

{additionalDetails}

} -
-
- {includeCancelButton && ( - - )} - -
-
- -
- ); -}; diff --git a/web/tailwind-themes/tailwind.config.js b/web/tailwind-themes/tailwind.config.js index 931f4a8f0..8453478c6 100644 --- a/web/tailwind-themes/tailwind.config.js +++ b/web/tailwind-themes/tailwind.config.js @@ -108,6 +108,7 @@ module.exports = { "input-option-hover": "var(--input-option-hover)", "accent-background": "var(--accent-background)", "accent-background-hovered": "var(--accent-background-hovered)", + "accent-background-selected": "var(--accent-background-selected)", "background-dark": "var(--off-white)", "background-100": "var(--neutral-100-border-light)", "background-125": "var(--neutral-125)",