mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-17 13:22:42 +01:00
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:
parent
037943c6ff
commit
12b2126e69
@ -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
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
```
|
||||
```
|
||||
|
@ -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<MinimalUserSnapshot[]>(
|
||||
"/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({
|
||||
<BackButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{labelToDelete && (
|
||||
<DeleteEntityModal
|
||||
<ConfirmEntityModal
|
||||
entityType="label"
|
||||
entityName={labelToDelete.name}
|
||||
onClose={() => setLabelToDelete(null)}
|
||||
@ -398,7 +395,7 @@ export function AssistantEditor({
|
||||
/>
|
||||
)}
|
||||
{deleteModalOpen && existingPersona && (
|
||||
<DeleteEntityModal
|
||||
<ConfirmEntityModal
|
||||
entityType="Persona"
|
||||
entityName={existingPersona.name}
|
||||
onClose={closeDeleteModal}
|
||||
@ -439,6 +436,7 @@ export function AssistantEditor({
|
||||
label_ids: Yup.array().of(Yup.number()),
|
||||
selectedUsers: Yup.array().of(Yup.object()),
|
||||
selectedGroups: Yup.array().of(Yup.number()),
|
||||
is_default_persona: Yup.boolean().required(),
|
||||
})
|
||||
.test(
|
||||
"system-prompt-or-task-prompt",
|
||||
@ -459,6 +457,19 @@ export function AssistantEditor({
|
||||
"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) => {
|
||||
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 && (
|
||||
<>
|
||||
<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="block font-medium text-sm">Access</div>
|
||||
</div>
|
||||
@ -1014,22 +1041,60 @@ export function AssistantEditor({
|
||||
|
||||
<div className="min-h-[100px]">
|
||||
<div className="flex items-center mb-2">
|
||||
<SwitchField
|
||||
name="is_public"
|
||||
size="md"
|
||||
onCheckedChange={(checked) => {
|
||||
setFieldValue("is_public", checked);
|
||||
if (checked) {
|
||||
setFieldValue("selectedUsers", []);
|
||||
setFieldValue("selectedGroups", []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<SwitchField
|
||||
name="is_public"
|
||||
size="md"
|
||||
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
|
||||
"Default Persona" to false to change
|
||||
visibility.
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<span className="text-sm ml-2">
|
||||
{values.is_public ? "Public" : "Private"}
|
||||
</span>
|
||||
</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 ? (
|
||||
<p className="text-sm text-text-dark">
|
||||
Anyone from your organization can view and use this
|
||||
|
@ -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<Persona[]>([]);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [personaToDelete, setPersonaToDelete] = useState<Persona | null>(null);
|
||||
const [defaultModalOpen, setDefaultModalOpen] = useState(false);
|
||||
const [personaToToggleDefault, setPersonaToToggleDefault] =
|
||||
useState<Persona | null>(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 (
|
||||
<div>
|
||||
{popup}
|
||||
{deleteModalOpen && personaToDelete && (
|
||||
<DeleteEntityModal
|
||||
<ConfirmEntityModal
|
||||
entityType="Persona"
|
||||
entityName={personaToDelete.name}
|
||||
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
|
||||
headers={["Name", "Description", "Type", "Is Visible", "Delete"]}
|
||||
headers={[
|
||||
"Name",
|
||||
"Description",
|
||||
"Type",
|
||||
"Featured Assistant",
|
||||
"Is Visible",
|
||||
"Delete",
|
||||
]}
|
||||
isAdmin={isAdmin}
|
||||
rows={finalPersonas.map((persona) => {
|
||||
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}
|
||||
</p>,
|
||||
<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
|
||||
key="is_visible"
|
||||
onClick={async () => {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ export default async function Page() {
|
||||
<Separator />
|
||||
|
||||
<Title>Create an Assistant</Title>
|
||||
<CreateButton href="/admin/assistants/new" text="New Assistant" />
|
||||
<CreateButton href="/assistants/new?admin=true" text="New Assistant" />
|
||||
|
||||
<Separator />
|
||||
|
||||
|
@ -100,7 +100,7 @@ export function SlackChannelConfigsTable({
|
||||
slackChannelConfig.persona
|
||||
) ? (
|
||||
<Link
|
||||
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
|
||||
href={`/assistants/${slackChannelConfig.persona.id}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{slackChannelConfig.persona.name}
|
||||
|
@ -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 && (
|
||||
<DeleteEntityModal
|
||||
<ConfirmEntityModal
|
||||
entityName={model.model_name}
|
||||
entityType="embedding model configuration"
|
||||
onSubmit={() => deleteModel()}
|
||||
|
@ -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 (
|
||||
<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="ml-2 flex-none mr-2 mt-1 w-10 h-10">
|
||||
<AssistantIcon assistant={persona} size="large" />
|
||||
@ -148,7 +175,7 @@ const AssistantCard: React.FC<{
|
||||
</div>
|
||||
{isOwnedByUser && (
|
||||
<div className="flex ml-2 relative items-center gap-x-2">
|
||||
<Popover modal>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button
|
||||
type="button"
|
||||
@ -157,55 +184,84 @@ const AssistantCard: React.FC<{
|
||||
<FiMoreHorizontal size={16} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={`w-32 z-[10000] p-2`}>
|
||||
<div className="flex flex-col text-sm space-y-1">
|
||||
<button
|
||||
onClick={isOwnedByUser ? handleEdit : undefined}
|
||||
className={`w-full flex items-center text-left px-2 py-1 rounded ${
|
||||
isOwnedByUser
|
||||
? "hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
: "opacity-50 cursor-not-allowed"
|
||||
}`}
|
||||
disabled={!isOwnedByUser}
|
||||
>
|
||||
<FiEdit size={12} className="inline mr-2" />
|
||||
Edit
|
||||
</button>
|
||||
{isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
|
||||
<PopoverContent
|
||||
className={`${
|
||||
isDeleteConfirmation ? "w-64" : "w-32"
|
||||
} z-[10000] p-2`}
|
||||
>
|
||||
{!isDeleteConfirmation ? (
|
||||
<div className="flex flex-col text-sm space-y-1">
|
||||
<button
|
||||
onClick={
|
||||
onClick={isOwnedByUser ? handleEdit : undefined}
|
||||
className={`w-full flex items-center text-left px-2 py-1 rounded ${
|
||||
isOwnedByUser
|
||||
? () => {
|
||||
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"
|
||||
? "hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
: "opacity-50 cursor-not-allowed"
|
||||
}`}
|
||||
disabled={!isOwnedByUser}
|
||||
>
|
||||
<FiBarChart size={12} className="inline mr-2" />
|
||||
Stats
|
||||
<FiEdit size={12} className="inline mr-2" />
|
||||
Edit
|
||||
</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>
|
||||
{isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
|
||||
<button
|
||||
onClick={
|
||||
isOwnedByUser
|
||||
? () => {
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
</Popover>
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<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
|
||||
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"
|
||||
style={{
|
||||
position: "fixed",
|
||||
@ -127,6 +130,15 @@ export function AssistantModal({
|
||||
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 flex-col sticky top-0 z-10">
|
||||
|
@ -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({
|
||||
<ChatPopup />
|
||||
|
||||
{showDeleteAllModal && (
|
||||
<DeleteEntityModal
|
||||
<ConfirmEntityModal
|
||||
entityType="All Chats"
|
||||
entityName="all your chat sessions"
|
||||
onClose={() => setShowDeleteAllModal(false)}
|
||||
@ -2287,6 +2287,7 @@ export function ChatPage({
|
||||
>
|
||||
<div className="w-full relative">
|
||||
<HistorySidebar
|
||||
liveAssistant={liveAssistant}
|
||||
setShowAssistantsModal={setShowAssistantsModal}
|
||||
explicitlyUntoggle={explicitlyUntoggle}
|
||||
reset={() => setMessage("")}
|
||||
@ -2294,7 +2295,6 @@ export function ChatPage({
|
||||
ref={innerSidebarElementRef}
|
||||
toggleSidebar={toggleSidebar}
|
||||
toggled={sidebarVisible}
|
||||
currentAssistantId={liveAssistant?.id}
|
||||
existingChats={chatSessions}
|
||||
currentChatSession={selectedChatSession}
|
||||
folders={folders}
|
||||
|
@ -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<SortableAssistantProps> = ({
|
||||
assistant,
|
||||
currentAssistantId,
|
||||
active,
|
||||
onClick,
|
||||
onUnpin,
|
||||
onPinAction,
|
||||
pinned = true,
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
@ -126,7 +129,9 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
>
|
||||
<DragHandle
|
||||
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
|
||||
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 ${
|
||||
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`}
|
||||
>
|
||||
<AssistantIcon assistant={assistant} size={16} className="flex-none" />
|
||||
@ -164,15 +167,36 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
>
|
||||
{assistant.name}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnpin(e);
|
||||
}}
|
||||
className="group-hover:block hidden absolute right-2"
|
||||
>
|
||||
<CircleX size={16} className="text-text-history-sidebar-button" />
|
||||
</button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPinAction(e);
|
||||
}}
|
||||
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>
|
||||
);
|
||||
@ -181,6 +205,7 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
(
|
||||
{
|
||||
liveAssistant,
|
||||
reset = () => null,
|
||||
setShowAssistantsModal = () => null,
|
||||
toggled,
|
||||
@ -194,7 +219,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
showShareModal,
|
||||
showDeleteModal,
|
||||
showDeleteAllModal,
|
||||
currentAssistantId,
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
@ -353,13 +377,13 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
<SortableAssistant
|
||||
key={assistant.id === 0 ? "assistant-0" : assistant.id}
|
||||
assistant={assistant}
|
||||
currentAssistantId={currentAssistantId}
|
||||
active={assistant.id === liveAssistant?.id}
|
||||
onClick={() => {
|
||||
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<HTMLDivElement, HistorySidebarProps>(
|
||||
</div>
|
||||
</SortableContext>
|
||||
</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">
|
||||
<button
|
||||
onClick={() => setShowAssistantsModal(true)}
|
||||
|
@ -70,6 +70,7 @@
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--accent-background: #f0eee8;
|
||||
--accent-background-hovered: #e5e3dd;
|
||||
--accent-background-selected: #eae8e2;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--ring: 0 0% 3.9%;
|
||||
@ -247,6 +248,7 @@
|
||||
--accent-background: #333333;
|
||||
|
||||
--accent-background-hovered: #2f2f2f;
|
||||
--accent-background-selected: #222222;
|
||||
|
||||
--text-darker: #f0f0f0;
|
||||
|
||||
|
@ -4,7 +4,7 @@ import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
|
||||
const DeleteUserButton = ({
|
||||
user,
|
||||
@ -38,7 +38,7 @@ const DeleteUserButton = ({
|
||||
return (
|
||||
<>
|
||||
{showDeleteModal && (
|
||||
<DeleteEntityModal
|
||||
<ConfirmEntityModal
|
||||
entityType="user"
|
||||
entityName={user.email}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
|
@ -4,7 +4,7 @@ import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const LeaveOrganizationButton = ({
|
||||
@ -46,8 +46,9 @@ export const LeaveOrganizationButton = ({
|
||||
return (
|
||||
<>
|
||||
{showLeaveModal && (
|
||||
<DeleteEntityModal
|
||||
deleteButtonText="Leave"
|
||||
<ConfirmEntityModal
|
||||
variant="action"
|
||||
actionButtonText="Leave"
|
||||
entityType="organization"
|
||||
entityName="your organization"
|
||||
onClose={() => setShowLeaveModal(false)}
|
||||
|
@ -60,7 +60,7 @@ export const AssistantsProvider: React.FC<{
|
||||
.map((id) => assistants.find((assistant) => assistant.id === id))
|
||||
.filter((assistant): assistant is Persona => assistant !== undefined);
|
||||
} 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))
|
||||
.filter((assistant): assistant is Persona => assistant !== undefined);
|
||||
} else {
|
||||
return assistants.filter((a) => a.builtin_persona);
|
||||
return assistants.filter((a) => a.is_default_persona);
|
||||
}
|
||||
});
|
||||
}, [user?.preferences?.pinned_assistants, assistants]);
|
||||
|
67
web/src/components/modals/ConfirmEntityModal.tsx
Normal file
67
web/src/components/modals/ConfirmEntityModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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)",
|
||||
|
Loading…
x
Reference in New Issue
Block a user