Update assistants visibility, minor UX, .. (#3965)

* update assistant logic

* quick nit

* k

* fix "featured" logic

* Small tweaks

* k

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
pablonyx 2025-02-11 16:43:20 -08:00 committed by GitHub
parent 037943c6ff
commit 12b2126e69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 571 additions and 291 deletions

View File

@ -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

View File

@ -204,6 +204,14 @@ def create_update_persona(
if not all_prompt_ids: if not all_prompt_ids:
raise ValueError("No prompt IDs provided") 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 = upsert_persona(
persona_id=persona_id, persona_id=persona_id,
user=user, user=user,
@ -510,6 +518,7 @@ def upsert_persona(
existing_persona.is_visible = is_visible existing_persona.is_visible = is_visible
existing_persona.search_start_date = search_start_date existing_persona.search_start_date = search_start_date
existing_persona.labels = labels or [] existing_persona.labels = labels or []
existing_persona.is_default_persona = is_default_persona
# Do not delete any associations manually added unless # Do not delete any associations manually added unless
# a new updated list is provided # a new updated list is provided
if document_sets is not None: if document_sets is not None:
@ -590,6 +599,23 @@ def delete_old_default_personas(
db_session.commit() 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( def update_persona_visibility(
persona_id: int, persona_id: int,
is_visible: bool, is_visible: bool,

View File

@ -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_deleted
from onyx.db.persona import mark_persona_as_not_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_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_label
from onyx.db.persona import update_persona_public_status from onyx.db.persona import update_persona_public_status
from onyx.db.persona import update_persona_shared_users 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.logger import setup_logger
from onyx.utils.telemetry import create_milestone_and_report from onyx.utils.telemetry import create_milestone_and_report
logger = setup_logger() logger = setup_logger()
@ -72,6 +72,10 @@ class IsPublicRequest(BaseModel):
is_public: bool is_public: bool
class IsDefaultRequest(BaseModel):
is_default_persona: bool
@admin_router.patch("/{persona_id}/visible") @admin_router.patch("/{persona_id}/visible")
def patch_persona_visibility( def patch_persona_visibility(
persona_id: int, persona_id: int,
@ -106,6 +110,25 @@ def patch_user_presona_public_status(
raise HTTPException(status_code=403, detail=str(e)) 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") @admin_router.put("/display-priority")
def patch_persona_display_priority( def patch_persona_display_priority(
display_priority_request: DisplayPriorityRequest, display_priority_request: DisplayPriorityRequest,

View File

@ -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. `http://127.0.0.1:3000` and accessing it there.
## Testing ## 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! Don't run these tests if you don't want to do this!
Bring up the entire application. Bring up the entire application.
1. Reset the instance 1. Reset the instance
```cd backend ```cd backend
@ -59,4 +59,4 @@ may use this for local troubleshooting and testing.
``` ```
cd web cd web
npx chromatic --playwright --project-token={your token here} npx chromatic --playwright --project-token={your token here}
``` ```

View File

@ -3,7 +3,13 @@
import React from "react"; import React from "react";
import { Option } from "@/components/Dropdown"; import { Option } from "@/components/Dropdown";
import { generateRandomIconShape } from "@/lib/assistantIconUtils"; 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 { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrayHelpers, FieldArray, Form, Formik, FormikProps } from "formik"; import { ArrayHelpers, FieldArray, Form, Formik, FormikProps } from "formik";
@ -33,9 +39,8 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { FiInfo } from "react-icons/fi";
import * as Yup from "yup"; import * as Yup from "yup";
import CollapsibleSection from "./CollapsibleSection"; import CollapsibleSection from "./CollapsibleSection";
import { SuccessfulPersonaUpdateRedirectType } from "./enums"; import { SuccessfulPersonaUpdateRedirectType } from "./enums";
@ -71,11 +76,11 @@ import {
Option as DropdownOption, Option as DropdownOption,
} from "@/components/Dropdown"; } from "@/components/Dropdown";
import { SourceChip } from "@/app/chat/input/ChatInputBar"; 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 { LLMSelector } from "@/components/llm/LLMSelector";
import useSWR from "swr"; import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher"; import { errorHandlingFetcher } from "@/lib/fetcher";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import Title from "@/components/ui/title"; import Title from "@/components/ui/title";
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants"; import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
@ -127,6 +132,8 @@ export function AssistantEditor({
}) { }) {
const { refreshAssistants, isImageGenerationAvailable } = useAssistants(); const { refreshAssistants, isImageGenerationAvailable } = useAssistants();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const isAdminPage = searchParams.get("admin") === "true";
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } = const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } =
@ -216,6 +223,8 @@ export function AssistantEditor({
enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id); enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id);
}); });
const [showVisibilityWarning, setShowVisibilityWarning] = useState(false);
const initialValues = { const initialValues = {
name: existingPersona?.name ?? "", name: existingPersona?.name ?? "",
description: existingPersona?.description ?? "", description: existingPersona?.description ?? "",
@ -252,6 +261,7 @@ export function AssistantEditor({
(u) => u.id !== existingPersona.owner?.id (u) => u.id !== existingPersona.owner?.id
) ?? [], ) ?? [],
selectedGroups: existingPersona?.groups ?? [], selectedGroups: existingPersona?.groups ?? [],
is_default_persona: existingPersona?.is_default_persona ?? false,
}; };
interface AssistantPrompt { interface AssistantPrompt {
@ -308,24 +318,12 @@ export function AssistantEditor({
const [isRequestSuccessful, setIsRequestSuccessful] = useState(false); const [isRequestSuccessful, setIsRequestSuccessful] = useState(false);
const { data: userGroups } = useUserGroups(); const { data: userGroups } = useUserGroups();
// const { data: allUsers } = useUsers({ includeApiKeys: false }) as {
// data: MinimalUserSnapshot[] | undefined;
// };
const { data: users } = useSWR<MinimalUserSnapshot[]>( const { data: users } = useSWR<MinimalUserSnapshot[]>(
"/api/users", "/api/users",
errorHandlingFetcher 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); const [deleteModalOpen, setDeleteModalOpen] = useState(false);
if (!labels) { if (!labels) {
@ -346,9 +344,7 @@ export function AssistantEditor({
if (response.ok) { if (response.ok) {
await refreshAssistants(); await refreshAssistants();
router.push( router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat`
? `/admin/assistants?u=${Date.now()}`
: `/chat`
); );
} else { } else {
setPopup({ setPopup({
@ -374,8 +370,9 @@ export function AssistantEditor({
<BackButton /> <BackButton />
</div> </div>
)} )}
{labelToDelete && ( {labelToDelete && (
<DeleteEntityModal <ConfirmEntityModal
entityType="label" entityType="label"
entityName={labelToDelete.name} entityName={labelToDelete.name}
onClose={() => setLabelToDelete(null)} onClose={() => setLabelToDelete(null)}
@ -398,7 +395,7 @@ export function AssistantEditor({
/> />
)} )}
{deleteModalOpen && existingPersona && ( {deleteModalOpen && existingPersona && (
<DeleteEntityModal <ConfirmEntityModal
entityType="Persona" entityType="Persona"
entityName={existingPersona.name} entityName={existingPersona.name}
onClose={closeDeleteModal} onClose={closeDeleteModal}
@ -439,6 +436,7 @@ export function AssistantEditor({
label_ids: Yup.array().of(Yup.number()), label_ids: Yup.array().of(Yup.number()),
selectedUsers: Yup.array().of(Yup.object()), selectedUsers: Yup.array().of(Yup.object()),
selectedGroups: Yup.array().of(Yup.number()), selectedGroups: Yup.array().of(Yup.number()),
is_default_persona: Yup.boolean().required(),
}) })
.test( .test(
"system-prompt-or-task-prompt", "system-prompt-or-task-prompt",
@ -459,6 +457,19 @@ export function AssistantEditor({
"Must provide either Instructions or Reminders (Advanced)", "Must provide either Instructions or Reminders (Advanced)",
}); });
} }
)
.test(
"default-persona-public",
"Default persona must be public",
function (values) {
if (values.is_default_persona && !values.is_public) {
return this.createError({
path: "is_public",
message: "Default persona must be public",
});
}
return true;
}
)} )}
onSubmit={async (values, formikHelpers) => { onSubmit={async (values, formikHelpers) => {
if ( if (
@ -499,7 +510,6 @@ export function AssistantEditor({
const submissionData: PersonaUpsertParameters = { const submissionData: PersonaUpsertParameters = {
...values, ...values,
existing_prompt_id: existingPrompt?.id ?? null, existing_prompt_id: existingPrompt?.id ?? null,
is_default_persona: admin!,
starter_messages: starterMessages, starter_messages: starterMessages,
groups: groups, groups: groups,
users: values.is_public users: values.is_public
@ -563,8 +573,9 @@ export function AssistantEditor({
} }
await refreshAssistants(); await refreshAssistants();
router.push( router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN isAdminPage
? `/admin/assistants?u=${Date.now()}` ? `/admin/assistants?u=${Date.now()}`
: `/chat?assistantId=${assistantId}` : `/chat?assistantId=${assistantId}`
); );
@ -1005,6 +1016,22 @@ export function AssistantEditor({
{showAdvancedOptions && ( {showAdvancedOptions && (
<> <>
<div className="max-w-4xl w-full"> <div className="max-w-4xl w-full">
{user?.role == UserRole.ADMIN && (
<BooleanFormField
onChange={(checked) => {
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."
/>
)}
<Separator />
<div className="flex gap-x-2 items-center "> <div className="flex gap-x-2 items-center ">
<div className="block font-medium text-sm">Access</div> <div className="block font-medium text-sm">Access</div>
</div> </div>
@ -1014,22 +1041,60 @@ export function AssistantEditor({
<div className="min-h-[100px]"> <div className="min-h-[100px]">
<div className="flex items-center mb-2"> <div className="flex items-center mb-2">
<SwitchField <TooltipProvider delayDuration={0}>
name="is_public" <Tooltip>
size="md" <TooltipTrigger asChild>
onCheckedChange={(checked) => { <div>
setFieldValue("is_public", checked); <SwitchField
if (checked) { name="is_public"
setFieldValue("selectedUsers", []); size="md"
setFieldValue("selectedGroups", []); onCheckedChange={(checked) => {
} 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}
/>
</div>
</TooltipTrigger>
{values.is_default_persona && (
<TooltipContent side="top" align="center">
Default persona must be public. Set
&quot;Default Persona&quot; to false to change
visibility.
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<span className="text-sm ml-2"> <span className="text-sm ml-2">
{values.is_public ? "Public" : "Private"} {values.is_public ? "Public" : "Private"}
</span> </span>
</div> </div>
{showVisibilityWarning && (
<div className="flex items-center text-warning mt-2">
<InfoIcon size={16} className="mr-2" />
<span className="text-sm">
Default persona must be public. Visibility has been
automatically set to public.
</span>
</div>
)}
{values.is_public ? ( {values.is_public ? (
<p className="text-sm text-text-dark"> <p className="text-sm text-text-dark">
Anyone from your organization can view and use this Anyone from your organization can view and use this

View File

@ -11,13 +11,14 @@ import { DraggableTable } from "@/components/table/DraggableTable";
import { import {
deletePersona, deletePersona,
personaComparator, personaComparator,
togglePersonaDefault,
togglePersonaVisibility, togglePersonaVisibility,
} from "./lib"; } from "./lib";
import { FiEdit2 } from "react-icons/fi"; import { FiEdit2 } from "react-icons/fi";
import { TrashIcon } from "@/components/icons/icons"; import { TrashIcon } from "@/components/icons/icons";
import { useUser } from "@/components/user/UserProvider"; import { useUser } from "@/components/user/UserProvider";
import { useAssistants } from "@/components/context/AssistantsContext"; import { useAssistants } from "@/components/context/AssistantsContext";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
function PersonaTypeDisplay({ persona }: { persona: Persona }) { function PersonaTypeDisplay({ persona }: { persona: Persona }) {
if (persona.builtin_persona) { if (persona.builtin_persona) {
@ -56,6 +57,9 @@ export function PersonasTable() {
const [finalPersonas, setFinalPersonas] = useState<Persona[]>([]); const [finalPersonas, setFinalPersonas] = useState<Persona[]>([]);
const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [personaToDelete, setPersonaToDelete] = useState<Persona | null>(null); const [personaToDelete, setPersonaToDelete] = useState<Persona | null>(null);
const [defaultModalOpen, setDefaultModalOpen] = useState(false);
const [personaToToggleDefault, setPersonaToToggleDefault] =
useState<Persona | null>(null);
useEffect(() => { useEffect(() => {
const editable = editablePersonas.sort(personaComparator); 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 ( return (
<div> <div>
{popup} {popup}
{deleteModalOpen && personaToDelete && ( {deleteModalOpen && personaToDelete && (
<DeleteEntityModal <ConfirmEntityModal
entityType="Persona" entityType="Persona"
entityName={personaToDelete.name} entityName={personaToDelete.name}
onClose={closeDeleteModal} onClose={closeDeleteModal}
@ -138,8 +170,35 @@ export function PersonasTable() {
/> />
)} )}
{defaultModalOpen && personaToToggleDefault && (
<ConfirmEntityModal
variant="action"
entityType="Assistant"
entityName={personaToToggleDefault.name}
onClose={closeDefaultModal}
onSubmit={handleToggleDefault}
actionButtonText={
personaToToggleDefault.is_default_persona
? "Remove Featured"
: "Set as Featured"
}
additionalDetails={
personaToToggleDefault.is_default_persona
? `Removing "${personaToToggleDefault.name}" as a featured assistant will not affect its visibility or accessibility.`
: `Setting "${personaToToggleDefault.name}" as a featured assistant will make it public and visible to all users. This action cannot be undone.`
}
/>
)}
<DraggableTable <DraggableTable
headers={["Name", "Description", "Type", "Is Visible", "Delete"]} headers={[
"Name",
"Description",
"Type",
"Featured Assistant",
"Is Visible",
"Delete",
]}
isAdmin={isAdmin} isAdmin={isAdmin}
rows={finalPersonas.map((persona) => { rows={finalPersonas.map((persona) => {
const isEditable = editablePersonas.includes(persona); const isEditable = editablePersonas.includes(persona);
@ -152,7 +211,9 @@ export function PersonasTable() {
className="mr-1 my-auto cursor-pointer" className="mr-1 my-auto cursor-pointer"
onClick={() => onClick={() =>
router.push( 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} {persona.description}
</p>, </p>,
<PersonaTypeDisplay key={persona.id} persona={persona} />, <PersonaTypeDisplay key={persona.id} persona={persona} />,
<div
key="is_default_persona"
onClick={() => {
if (isEditable) {
openDefaultModal(persona);
}
}}
className={`px-1 py-0.5 rounded flex ${
isEditable
? "hover:bg-accent-background-hovered cursor-pointer"
: ""
} select-none w-fit`}
>
<div className="my-auto flex-none w-22">
{!persona.is_default_persona ? (
<div className="text-error">Not Featured</div>
) : (
"Featured"
)}
</div>
<div className="ml-1 my-auto">
<CustomCheckbox checked={persona.is_default_persona} />
</div>
</div>,
<div <div
key="is_visible" key="is_visible"
onClick={async () => { onClick={async () => {

View File

@ -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 (
<Button
variant="destructive"
onClick={async () => {
const response = await deletePersona(personaId);
if (response.ok) {
router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
? `/admin/assistants?u=${Date.now()}`
: `/chat`
);
} else {
alert(`Failed to delete persona - ${await response.text()}`);
}
}}
>
Delete
</Button>
);
}

View File

@ -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 = (
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
);
} else {
body = (
<>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
admin
defaultPublic={true}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</CardSection>
</>
);
}
return (
<div className="w-full">
<AdminPageTitle title="Edit Assistant" icon={<RobotIcon size={32} />} />
{body}
</div>
);
}

View File

@ -261,6 +261,22 @@ export function personaComparator(a: Persona, b: Persona) {
return closerToZeroNegativesFirstComparator(a.id, b.id); 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 ( export const togglePersonaVisibility = async (
personaId: number, personaId: number,
isVisible: boolean isVisible: boolean

View File

@ -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 (
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
);
} else {
return (
<div className="w-full">
<AssistantEditor
{...values}
admin
defaultPublic={true}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</div>
);
}
}

View File

@ -29,7 +29,7 @@ export default async function Page() {
<Separator /> <Separator />
<Title>Create an Assistant</Title> <Title>Create an Assistant</Title>
<CreateButton href="/admin/assistants/new" text="New Assistant" /> <CreateButton href="/assistants/new?admin=true" text="New Assistant" />
<Separator /> <Separator />

View File

@ -100,7 +100,7 @@ export function SlackChannelConfigsTable({
slackChannelConfig.persona slackChannelConfig.persona
) ? ( ) ? (
<Link <Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`} href={`/assistants/${slackChannelConfig.persona.id}`}
className="text-primary hover:underline" className="text-primary hover:underline"
> >
{slackChannelConfig.persona.name} {slackChannelConfig.persona.name}

View File

@ -19,7 +19,7 @@ import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { CustomEmbeddingModelForm } from "@/components/embedding/CustomEmbeddingModelForm"; import { CustomEmbeddingModelForm } from "@/components/embedding/CustomEmbeddingModelForm";
import { deleteSearchSettings } from "./utils"; import { deleteSearchSettings } from "./utils";
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import { AdvancedSearchConfiguration } from "../interfaces"; import { AdvancedSearchConfiguration } from "../interfaces";
import CardSection from "@/components/admin/CardSection"; import CardSection from "@/components/admin/CardSection";
@ -456,7 +456,7 @@ export function CloudModelCard({
> >
{popup} {popup}
{showDeleteModel && ( {showDeleteModel && (
<DeleteEntityModal <ConfirmEntityModal
entityName={model.model_name} entityName={model.model_name}
entityType="embedding model configuration" entityType="embedding model configuration"
onSubmit={() => deleteModel()} onSubmit={() => deleteModel()}

View File

@ -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 { useRouter } from "next/navigation";
import { import {
FiMoreHorizontal, FiMoreHorizontal,
@ -8,7 +8,7 @@ import {
FiLock, FiLock,
FiUnlock, FiUnlock,
} from "react-icons/fi"; } from "react-icons/fi";
import { FaHashtag } from "react-icons/fa";
import { import {
Popover, Popover,
PopoverTrigger, PopoverTrigger,
@ -26,14 +26,12 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { PinnedIcon } from "@/components/icons/icons"; import { PinnedIcon } from "@/components/icons/icons";
import { import { deletePersona } from "@/app/admin/assistants/lib";
deletePersona,
togglePersonaPublicStatus,
} from "@/app/admin/assistants/lib";
import { PencilIcon } from "lucide-react"; import { PencilIcon } from "lucide-react";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled"; import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { truncateString } from "@/lib/utils"; import { truncateString } from "@/lib/utils";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Button } from "@/components/ui/button";
export const AssistantBadge = ({ export const AssistantBadge = ({
text, text,
@ -63,6 +61,7 @@ const AssistantCard: React.FC<{
const { user, toggleAssistantPinnedStatus } = useUser(); const { user, toggleAssistantPinnedStatus } = useUser();
const router = useRouter(); const router = useRouter();
const { refreshAssistants, pinnedAssistants } = useAssistants(); const { refreshAssistants, pinnedAssistants } = useAssistants();
const { popup, setPopup } = usePopup();
const isOwnedByUser = checkUserOwnsAssistant(user, persona); const isOwnedByUser = checkUserOwnsAssistant(user, persona);
@ -72,7 +71,34 @@ const AssistantCard: React.FC<{
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled(); 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 = () => { const handleEdit = () => {
router.push(`/assistants/edit/${persona.id}`); router.push(`/assistants/edit/${persona.id}`);
setActivePopover(null); setActivePopover(null);
@ -100,6 +126,7 @@ const AssistantCard: React.FC<{
return ( return (
<div className="w-full text-text-800 p-2 overflow-visible pb-4 pt-3 bg-transparent dark:bg-neutral-800/80 rounded shadow-[0px_0px_4px_0px_rgba(0,0,0,0.25)] flex flex-col"> <div className="w-full text-text-800 p-2 overflow-visible pb-4 pt-3 bg-transparent dark:bg-neutral-800/80 rounded shadow-[0px_0px_4px_0px_rgba(0,0,0,0.25)] flex flex-col">
{popup}
<div className="w-full flex"> <div className="w-full flex">
<div className="ml-2 flex-none mr-2 mt-1 w-10 h-10"> <div className="ml-2 flex-none mr-2 mt-1 w-10 h-10">
<AssistantIcon assistant={persona} size="large" /> <AssistantIcon assistant={persona} size="large" />
@ -148,7 +175,7 @@ const AssistantCard: React.FC<{
</div> </div>
{isOwnedByUser && ( {isOwnedByUser && (
<div className="flex ml-2 relative items-center gap-x-2"> <div className="flex ml-2 relative items-center gap-x-2">
<Popover modal> <Popover>
<PopoverTrigger> <PopoverTrigger>
<button <button
type="button" type="button"
@ -157,55 +184,84 @@ const AssistantCard: React.FC<{
<FiMoreHorizontal size={16} /> <FiMoreHorizontal size={16} />
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className={`w-32 z-[10000] p-2`}> <PopoverContent
<div className="flex flex-col text-sm space-y-1"> className={`${
<button isDeleteConfirmation ? "w-64" : "w-32"
onClick={isOwnedByUser ? handleEdit : undefined} } z-[10000] p-2`}
className={`w-full flex items-center text-left px-2 py-1 rounded ${ >
isOwnedByUser {!isDeleteConfirmation ? (
? "hover:bg-neutral-200 dark:hover:bg-neutral-700" <div className="flex flex-col text-sm space-y-1">
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiEdit size={12} className="inline mr-2" />
Edit
</button>
{isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
<button <button
onClick={ onClick={isOwnedByUser ? handleEdit : undefined}
className={`w-full flex items-center text-left px-2 py-1 rounded ${
isOwnedByUser isOwnedByUser
? () => { ? "hover:bg-neutral-200 dark:hover:bg-neutral-700"
router.push(
`/assistants/stats/${persona.id}`
);
closePopover();
}
: undefined
}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-800"
: "opacity-50 cursor-not-allowed" : "opacity-50 cursor-not-allowed"
}`} }`}
disabled={!isOwnedByUser}
> >
<FiBarChart size={12} className="inline mr-2" /> <FiEdit size={12} className="inline mr-2" />
Stats Edit
</button> </button>
)} {isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
<button <button
onClick={isOwnedByUser ? handleDelete : undefined} onClick={
className={`w-full text-left items-center px-2 py-1 rounded ${ isOwnedByUser
isOwnedByUser ? () => {
? "hover:bg-neutral-200 dark:hover:bg-neutral- text-red-600 dark:text-red-400" router.push(
: "opacity-50 cursor-not-allowed text-red-300 dark:text-red-500" `/assistants/stats/${persona.id}`
}`} );
disabled={!isOwnedByUser} closePopover();
> }
<FiTrash size={12} className="inline mr-2" /> : undefined
Delete }
</button> className={`w-full text-left items-center px-2 py-1 rounded ${
</div> isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-800"
: "opacity-50 cursor-not-allowed"
}`}
>
<FiBarChart size={12} className="inline mr-2" />
Stats
</button>
)}
<button
onClick={isOwnedByUser ? handleDelete : undefined}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral- text-red-600 dark:text-red-400"
: "opacity-50 cursor-not-allowed text-red-300 dark:text-red-500"
}`}
disabled={!isOwnedByUser}
>
<FiTrash size={12} className="inline mr-2" />
Delete
</button>
</div>
) : (
<div className="w-full">
<p className="text-sm mb-3">
Are you sure you want to delete assistant{" "}
<b>{persona.name}</b>?
</p>
<div className="flex justify-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={cancelDelete}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
onClick={confirmDelete}
>
Delete
</Button>
</div>
</div>
)}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>

View File

@ -5,9 +5,8 @@ import { useRouter } from "next/navigation";
import AssistantCard from "./AssistantCard"; import AssistantCard from "./AssistantCard";
import { useAssistants } from "@/components/context/AssistantsContext"; import { useAssistants } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider"; import { useUser } from "@/components/user/UserProvider";
import { FilterIcon } from "lucide-react"; import { FilterIcon, XIcon } from "lucide-react";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership"; import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { Dialog, DialogContent } from "@/components/ui/dialog";
export const AssistantBadgeSelector = ({ export const AssistantBadgeSelector = ({
text, text,
@ -108,16 +107,20 @@ export function AssistantModal({
const featuredAssistants = [ const featuredAssistants = [
...memoizedCurrentlyVisibleAssistants.filter( ...memoizedCurrentlyVisibleAssistants.filter(
(assistant) => assistant.builtin_persona || assistant.is_default_persona (assistant) => assistant.is_default_persona
), ),
]; ];
const allAssistants = memoizedCurrentlyVisibleAssistants.filter( const allAssistants = memoizedCurrentlyVisibleAssistants.filter(
(assistant) => !assistant.builtin_persona && !assistant.is_default_persona (assistant) => !assistant.is_default_persona
); );
return ( return (
<div className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50"> <div
onClick={hideModal}
className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50"
>
<div <div
onClick={(e) => 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" 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={{ style={{
position: "fixed", position: "fixed",
@ -127,6 +130,15 @@ export function AssistantModal({
margin: 0, margin: 0,
}} }}
> >
<div className="absolute top-2 right-2">
<button
onClick={hideModal}
className="cursor-pointer text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-300 transition-colors duration-200 p-2"
aria-label="Close modal"
>
<XIcon className="w-5 h-5" />
</button>
</div>
<div className="flex overflow-hidden flex-col h-full"> <div className="flex overflow-hidden flex-col h-full">
<div className="flex overflow-hidden flex-col h-full"> <div className="flex overflow-hidden flex-col h-full">
<div className="flex flex-col sticky top-0 z-10"> <div className="flex flex-col sticky top-0 z-10">

View File

@ -97,7 +97,6 @@ import {
} from "@/components/resizable/constants"; } from "@/components/resizable/constants";
import FixedLogo from "../../components/logo/FixedLogo"; import FixedLogo from "../../components/logo/FixedLogo";
import { DeleteEntityModal } from "../../components/modals/DeleteEntityModal";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown"; import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal"; import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
@ -130,6 +129,7 @@ import {
useSidebarShortcut, useSidebarShortcut,
} from "@/lib/browserUtilities"; } from "@/lib/browserUtilities";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
const TEMP_USER_MESSAGE_ID = -1; const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2; const TEMP_ASSISTANT_MESSAGE_ID = -2;
@ -2122,7 +2122,7 @@ export function ChatPage({
<ChatPopup /> <ChatPopup />
{showDeleteAllModal && ( {showDeleteAllModal && (
<DeleteEntityModal <ConfirmEntityModal
entityType="All Chats" entityType="All Chats"
entityName="all your chat sessions" entityName="all your chat sessions"
onClose={() => setShowDeleteAllModal(false)} onClose={() => setShowDeleteAllModal(false)}
@ -2287,6 +2287,7 @@ export function ChatPage({
> >
<div className="w-full relative"> <div className="w-full relative">
<HistorySidebar <HistorySidebar
liveAssistant={liveAssistant}
setShowAssistantsModal={setShowAssistantsModal} setShowAssistantsModal={setShowAssistantsModal}
explicitlyUntoggle={explicitlyUntoggle} explicitlyUntoggle={explicitlyUntoggle}
reset={() => setMessage("")} reset={() => setMessage("")}
@ -2294,7 +2295,6 @@ export function ChatPage({
ref={innerSidebarElementRef} ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
toggled={sidebarVisible} toggled={sidebarVisible}
currentAssistantId={liveAssistant?.id}
existingChats={chatSessions} existingChats={chatSessions}
currentChatSession={selectedChatSession} currentChatSession={selectedChatSession}
folders={folders} folders={folders}

View File

@ -50,10 +50,12 @@ import {
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; 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 { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { turborepoTraceAccess } from "next/dist/build/turborepo-access-trace";
interface HistorySidebarProps { interface HistorySidebarProps {
liveAssistant?: Persona | null;
page: pageType; page: pageType;
existingChats?: ChatSession[]; existingChats?: ChatSession[];
currentChatSession?: ChatSession | null | undefined; currentChatSession?: ChatSession | null | undefined;
@ -66,22 +68,23 @@ interface HistorySidebarProps {
showDeleteModal?: (chatSession: ChatSession) => void; showDeleteModal?: (chatSession: ChatSession) => void;
explicitlyUntoggle: () => void; explicitlyUntoggle: () => void;
showDeleteAllModal?: () => void; showDeleteAllModal?: () => void;
currentAssistantId?: number | null;
setShowAssistantsModal: (show: boolean) => void; setShowAssistantsModal: (show: boolean) => void;
} }
interface SortableAssistantProps { interface SortableAssistantProps {
assistant: Persona; assistant: Persona;
currentAssistantId: number | null | undefined; active: boolean;
onClick: () => void; onClick: () => void;
onUnpin: (e: React.MouseEvent) => void; onPinAction: (e: React.MouseEvent) => void;
pinned?: boolean;
} }
const SortableAssistant: React.FC<SortableAssistantProps> = ({ const SortableAssistant: React.FC<SortableAssistantProps> = ({
assistant, assistant,
currentAssistantId, active,
onClick, onClick,
onUnpin, onPinAction,
pinned = true,
}) => { }) => {
const { const {
attributes, attributes,
@ -126,7 +129,9 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
> >
<DragHandle <DragHandle
size={16} size={16}
className="w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab" className={`w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab ${
!pinned ? "opacity-0" : ""
}`}
/> />
<div <div
data-testid={`assistant-[${assistant.id}]`} data-testid={`assistant-[${assistant.id}]`}
@ -137,9 +142,7 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
} }
}} }}
className={`cursor-pointer w-full group hover:bg-background-chat-hover ${ className={`cursor-pointer w-full group hover:bg-background-chat-hover ${
currentAssistantId === assistant.id active ? "bg-accent-background-selected" : ""
? "bg-background-chat-hover/60"
: ""
} relative flex items-center gap-x-2 py-1 px-2 rounded-md`} } relative flex items-center gap-x-2 py-1 px-2 rounded-md`}
> >
<AssistantIcon assistant={assistant} size={16} className="flex-none" /> <AssistantIcon assistant={assistant} size={16} className="flex-none" />
@ -164,15 +167,36 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
> >
{assistant.name} {assistant.name}
</span> </span>
<button <TooltipProvider>
onClick={(e) => { <Tooltip>
e.stopPropagation(); <TooltipTrigger asChild>
onUnpin(e); <button
}} onClick={(e) => {
className="group-hover:block hidden absolute right-2" e.stopPropagation();
> onPinAction(e);
<CircleX size={16} className="text-text-history-sidebar-button" /> }}
</button> className="group-hover:block hidden absolute right-2"
>
{pinned ? (
<CircleX
size={16}
className="text-text-history-sidebar-button"
/>
) : (
<PinIcon
size={16}
className="text-text-history-sidebar-button"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent>
{pinned
? "Unpin this assistant from the sidebar"
: "Pin this assistant to the sidebar"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
</div> </div>
); );
@ -181,6 +205,7 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>( export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
( (
{ {
liveAssistant,
reset = () => null, reset = () => null,
setShowAssistantsModal = () => null, setShowAssistantsModal = () => null,
toggled, toggled,
@ -194,7 +219,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
showShareModal, showShareModal,
showDeleteModal, showDeleteModal,
showDeleteAllModal, showDeleteAllModal,
currentAssistantId,
}, },
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
) => { ) => {
@ -353,13 +377,13 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
<SortableAssistant <SortableAssistant
key={assistant.id === 0 ? "assistant-0" : assistant.id} key={assistant.id === 0 ? "assistant-0" : assistant.id}
assistant={assistant} assistant={assistant}
currentAssistantId={currentAssistantId} active={assistant.id === liveAssistant?.id}
onClick={() => { onClick={() => {
router.push( router.push(
buildChatUrl(searchParams, null, assistant.id) buildChatUrl(searchParams, null, assistant.id)
); );
}} }}
onUnpin={async (e: React.MouseEvent) => { onPinAction={async (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
await toggleAssistantPinnedStatus( await toggleAssistantPinnedStatus(
pinnedAssistants.map((a) => a.id), pinnedAssistants.map((a) => a.id),
@ -373,6 +397,31 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
</div> </div>
</SortableContext> </SortableContext>
</DndContext> </DndContext>
{!pinnedAssistants.some((a) => a.id === liveAssistant?.id) &&
liveAssistant && (
<div className="w-full mt-1 pr-4">
<SortableAssistant
pinned={false}
assistant={liveAssistant}
active={liveAssistant.id === liveAssistant?.id}
onClick={() => {
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();
}}
/>
</div>
)}
<div className="w-full px-4"> <div className="w-full px-4">
<button <button
onClick={() => setShowAssistantsModal(true)} onClick={() => setShowAssistantsModal(true)}

View File

@ -70,6 +70,7 @@
--accent-foreground: 0 0% 9%; --accent-foreground: 0 0% 9%;
--accent-background: #f0eee8; --accent-background: #f0eee8;
--accent-background-hovered: #e5e3dd; --accent-background-hovered: #e5e3dd;
--accent-background-selected: #eae8e2;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%; --ring: 0 0% 3.9%;
@ -247,6 +248,7 @@
--accent-background: #333333; --accent-background: #333333;
--accent-background-hovered: #2f2f2f; --accent-background-hovered: #2f2f2f;
--accent-background-selected: #222222;
--text-darker: #f0f0f0; --text-darker: #f0f0f0;

View File

@ -4,7 +4,7 @@ import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
import useSWRMutation from "swr/mutation"; import useSWRMutation from "swr/mutation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState } from "react"; import { useState } from "react";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
const DeleteUserButton = ({ const DeleteUserButton = ({
user, user,
@ -38,7 +38,7 @@ const DeleteUserButton = ({
return ( return (
<> <>
{showDeleteModal && ( {showDeleteModal && (
<DeleteEntityModal <ConfirmEntityModal
entityType="user" entityType="user"
entityName={user.email} entityName={user.email}
onClose={() => setShowDeleteModal(false)} onClose={() => setShowDeleteModal(false)}

View File

@ -4,7 +4,7 @@ import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
import useSWRMutation from "swr/mutation"; import useSWRMutation from "swr/mutation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState } from "react"; import { useState } from "react";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal"; import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export const LeaveOrganizationButton = ({ export const LeaveOrganizationButton = ({
@ -46,8 +46,9 @@ export const LeaveOrganizationButton = ({
return ( return (
<> <>
{showLeaveModal && ( {showLeaveModal && (
<DeleteEntityModal <ConfirmEntityModal
deleteButtonText="Leave" variant="action"
actionButtonText="Leave"
entityType="organization" entityType="organization"
entityName="your organization" entityName="your organization"
onClose={() => setShowLeaveModal(false)} onClose={() => setShowLeaveModal(false)}

View File

@ -60,7 +60,7 @@ export const AssistantsProvider: React.FC<{
.map((id) => assistants.find((assistant) => assistant.id === id)) .map((id) => assistants.find((assistant) => assistant.id === id))
.filter((assistant): assistant is Persona => assistant !== undefined); .filter((assistant): assistant is Persona => assistant !== undefined);
} else { } else {
return assistants.filter((a) => a.builtin_persona); return assistants.filter((a) => a.is_default_persona);
} }
}); });
@ -71,7 +71,7 @@ export const AssistantsProvider: React.FC<{
.map((id) => assistants.find((assistant) => assistant.id === id)) .map((id) => assistants.find((assistant) => assistant.id === id))
.filter((assistant): assistant is Persona => assistant !== undefined); .filter((assistant): assistant is Persona => assistant !== undefined);
} else { } else {
return assistants.filter((a) => a.builtin_persona); return assistants.filter((a) => a.is_default_persona);
} }
}); });
}, [user?.preferences?.pinned_assistants, assistants]); }, [user?.preferences?.pinned_assistants, assistants]);

View File

@ -0,0 +1,67 @@
import { Modal } from "../Modal";
import { Button } from "../ui/button";
export const ConfirmEntityModal = ({
onClose,
onSubmit,
entityType,
entityName,
additionalDetails,
actionButtonText,
includeCancelButton = true,
variant = "delete",
}: {
entityType: string;
entityName: string;
onClose: () => void;
onSubmit: () => void;
additionalDetails?: string;
actionButtonText?: string;
includeCancelButton?: boolean;
variant?: "delete" | "action";
}) => {
const isDeleteVariant = variant === "delete";
const defaultButtonText = isDeleteVariant ? "Delete" : "Confirm";
const buttonText = actionButtonText || defaultButtonText;
const getActionText = () => {
if (isDeleteVariant) {
return "delete";
}
switch (entityType) {
case "Default Persona":
return "change the default status of";
default:
return "modify";
}
};
return (
<Modal width="rounded max-w-sm w-full" onOutsideClick={onClose}>
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">
{buttonText} {entityType}
</h2>
</div>
<p className="mb-4">
Are you sure you want to {getActionText()} <b>{entityName}</b>?
</p>
{additionalDetails && <p className="mb-4">{additionalDetails}</p>}
<div className="flex justify-end gap-2">
{includeCancelButton && (
<Button onClick={onClose} variant="outline">
Cancel
</Button>
)}
<Button
onClick={onSubmit}
variant={isDeleteVariant ? "destructive" : "default"}
>
{buttonText}
</Button>
</div>
</>
</Modal>
);
};

View File

@ -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 (
<Modal width="rounded max-w-sm w-full" onOutsideClick={onClose}>
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">
{deleteButtonText || `Delete`} {entityType}
</h2>
</div>
<p className="mb-4">
Are you sure you want to {deleteButtonText || "delete"}{" "}
<b>{entityName}</b>?
</p>
{additionalDetails && <p className="mb-4">{additionalDetails}</p>}
<div className="flex items-end justify-end">
<div className="flex gap-x-2">
{includeCancelButton && (
<Button variant="outline" onClick={onClose}>
<div className="flex mx-2">Cancel</div>
</Button>
)}
<Button size="sm" variant="destructive" onClick={onSubmit}>
<div className="flex mx-2">{deleteButtonText || "Delete"}</div>
</Button>
</div>
</div>
</>
</Modal>
);
};

View File

@ -108,6 +108,7 @@ module.exports = {
"input-option-hover": "var(--input-option-hover)", "input-option-hover": "var(--input-option-hover)",
"accent-background": "var(--accent-background)", "accent-background": "var(--accent-background)",
"accent-background-hovered": "var(--accent-background-hovered)", "accent-background-hovered": "var(--accent-background-hovered)",
"accent-background-selected": "var(--accent-background-selected)",
"background-dark": "var(--off-white)", "background-dark": "var(--off-white)",
"background-100": "var(--neutral-100-border-light)", "background-100": "var(--neutral-100-border-light)",
"background-125": "var(--neutral-125)", "background-125": "var(--neutral-125)",