mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-01 00:18:18 +02:00
Assistants panel rework (#2509)
* update user model * squash - update assistant gallery * rework assistant display logic + ux * update tool + assistant display * update a couple function names * update typing + some logic * remove unnecessary comments * finalize functionality * updated logic * fully functional * remove logs + ports * small update to logic * update typing * allow seeding of display priority * reorder migrations * update for alembic
This commit is contained in:
parent
2274cab554
commit
8a8e2b310e
79
backend/alembic/versions/55546a7967ee_assistant_rework.py
Normal file
79
backend/alembic/versions/55546a7967ee_assistant_rework.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""assistant_rework
|
||||
|
||||
Revision ID: 55546a7967ee
|
||||
Revises: 61ff3651add4
|
||||
Create Date: 2024-09-18 17:00:23.755399
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "55546a7967ee"
|
||||
down_revision = "61ff3651add4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Reworking persona and user tables for new assistant features
|
||||
# keep track of user's chosen assistants separate from their `ordering`
|
||||
op.add_column("persona", sa.Column("builtin_persona", sa.Boolean(), nullable=True))
|
||||
op.execute("UPDATE persona SET builtin_persona = default_persona")
|
||||
op.alter_column("persona", "builtin_persona", nullable=False)
|
||||
op.drop_index("_default_persona_name_idx", table_name="persona")
|
||||
op.create_index(
|
||||
"_builtin_persona_name_idx",
|
||||
"persona",
|
||||
["name"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("builtin_persona = true"),
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"user", sa.Column("visible_assistants", postgresql.JSONB(), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"user", sa.Column("hidden_assistants", postgresql.JSONB(), nullable=True)
|
||||
)
|
||||
op.execute(
|
||||
"UPDATE \"user\" SET visible_assistants = '[]'::jsonb, hidden_assistants = '[]'::jsonb"
|
||||
)
|
||||
op.alter_column(
|
||||
"user",
|
||||
"visible_assistants",
|
||||
nullable=False,
|
||||
server_default=sa.text("'[]'::jsonb"),
|
||||
)
|
||||
op.alter_column(
|
||||
"user",
|
||||
"hidden_assistants",
|
||||
nullable=False,
|
||||
server_default=sa.text("'[]'::jsonb"),
|
||||
)
|
||||
op.drop_column("persona", "default_persona")
|
||||
op.add_column(
|
||||
"persona", sa.Column("is_default_persona", sa.Boolean(), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Reverting changes made in upgrade
|
||||
op.drop_column("user", "hidden_assistants")
|
||||
op.drop_column("user", "visible_assistants")
|
||||
op.drop_index("_builtin_persona_name_idx", table_name="persona")
|
||||
|
||||
op.drop_column("persona", "is_default_persona")
|
||||
op.add_column("persona", sa.Column("default_persona", sa.Boolean(), nullable=True))
|
||||
op.execute("UPDATE persona SET default_persona = builtin_persona")
|
||||
op.alter_column("persona", "default_persona", nullable=False)
|
||||
op.drop_column("persona", "builtin_persona")
|
||||
op.create_index(
|
||||
"_default_persona_name_idx",
|
||||
"persona",
|
||||
["name"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("default_persona = true"),
|
||||
)
|
@ -122,7 +122,7 @@ def load_personas_from_yaml(
|
||||
prompt_ids=prompt_ids,
|
||||
document_set_ids=doc_set_ids,
|
||||
tool_ids=tool_ids,
|
||||
default_persona=True,
|
||||
builtin_persona=True,
|
||||
is_public=True,
|
||||
display_priority=existing_persona.display_priority
|
||||
if existing_persona is not None
|
||||
|
@ -125,6 +125,12 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
chosen_assistants: Mapped[list[int]] = mapped_column(
|
||||
postgresql.JSONB(), nullable=False, default=[-2, -1, 0]
|
||||
)
|
||||
visible_assistants: Mapped[list[int]] = mapped_column(
|
||||
postgresql.JSONB(), nullable=False, default=[]
|
||||
)
|
||||
hidden_assistants: Mapped[list[int]] = mapped_column(
|
||||
postgresql.JSONB(), nullable=False, default=[]
|
||||
)
|
||||
|
||||
oidc_expiry: Mapped[datetime.datetime] = mapped_column(
|
||||
TIMESTAMPAware(timezone=True), nullable=True
|
||||
@ -1308,9 +1314,15 @@ class Persona(Base):
|
||||
starter_messages: Mapped[list[StarterMessage] | None] = mapped_column(
|
||||
postgresql.JSONB(), nullable=True
|
||||
)
|
||||
# Default personas are configured via backend during deployment
|
||||
# Built-in personas are configured via backend during deployment
|
||||
# Treated specially (cannot be user edited etc.)
|
||||
default_persona: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
builtin_persona: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Default personas are personas created by admins and are automatically added
|
||||
# to all users' assistants list.
|
||||
is_default_persona: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False
|
||||
)
|
||||
# controls whether the persona is available to be selected by users
|
||||
is_visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
# controls the ordering of personas in the UI
|
||||
@ -1361,10 +1373,10 @@ class Persona(Base):
|
||||
# Default personas loaded via yaml cannot have the same name
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"_default_persona_name_idx",
|
||||
"_builtin_persona_name_idx",
|
||||
"name",
|
||||
unique=True,
|
||||
postgresql_where=(default_persona == True), # noqa: E712
|
||||
postgresql_where=(builtin_persona == True), # noqa: E712
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -178,6 +178,7 @@ def create_update_persona(
|
||||
except ValueError as e:
|
||||
logger.exception("Failed to create persona")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
return PersonaSnapshot.from_model(persona)
|
||||
|
||||
|
||||
@ -258,7 +259,7 @@ def get_personas(
|
||||
stmt = _add_user_filters(stmt=stmt, user=user, get_editable=get_editable)
|
||||
|
||||
if not include_default:
|
||||
stmt = stmt.where(Persona.default_persona.is_(False))
|
||||
stmt = stmt.where(Persona.builtin_persona.is_(False))
|
||||
if not include_slack_bot_personas:
|
||||
stmt = stmt.where(not_(Persona.name.startswith(SLACK_BOT_PERSONA_PREFIX)))
|
||||
if not include_deleted:
|
||||
@ -306,7 +307,7 @@ def mark_delete_persona_by_name(
|
||||
) -> None:
|
||||
stmt = (
|
||||
update(Persona)
|
||||
.where(Persona.name == persona_name, Persona.default_persona == is_default)
|
||||
.where(Persona.name == persona_name, Persona.builtin_persona == is_default)
|
||||
.values(deleted=True)
|
||||
)
|
||||
|
||||
@ -406,7 +407,6 @@ def upsert_persona(
|
||||
document_set_ids: list[int] | None = None,
|
||||
tool_ids: list[int] | None = None,
|
||||
persona_id: int | None = None,
|
||||
default_persona: bool = False,
|
||||
commit: bool = True,
|
||||
icon_color: str | None = None,
|
||||
icon_shape: int | None = None,
|
||||
@ -414,6 +414,8 @@ def upsert_persona(
|
||||
display_priority: int | None = None,
|
||||
is_visible: bool = True,
|
||||
remove_image: bool | None = None,
|
||||
builtin_persona: bool = False,
|
||||
is_default_persona: bool = False,
|
||||
chunks_above: int = CONTEXT_CHUNKS_ABOVE,
|
||||
chunks_below: int = CONTEXT_CHUNKS_BELOW,
|
||||
) -> Persona:
|
||||
@ -454,8 +456,8 @@ def upsert_persona(
|
||||
validate_persona_tools(tools)
|
||||
|
||||
if persona:
|
||||
if not default_persona and persona.default_persona:
|
||||
raise ValueError("Cannot update default persona with non-default.")
|
||||
if not builtin_persona and persona.builtin_persona:
|
||||
raise ValueError("Cannot update builtin persona with non-builtin.")
|
||||
|
||||
# this checks if the user has permission to edit the persona
|
||||
persona = fetch_persona_by_id(
|
||||
@ -470,7 +472,7 @@ def upsert_persona(
|
||||
persona.llm_relevance_filter = llm_relevance_filter
|
||||
persona.llm_filter_extraction = llm_filter_extraction
|
||||
persona.recency_bias = recency_bias
|
||||
persona.default_persona = default_persona
|
||||
persona.builtin_persona = builtin_persona
|
||||
persona.llm_model_provider_override = llm_model_provider_override
|
||||
persona.llm_model_version_override = llm_model_version_override
|
||||
persona.starter_messages = starter_messages
|
||||
@ -482,6 +484,7 @@ def upsert_persona(
|
||||
persona.uploaded_image_id = uploaded_image_id
|
||||
persona.display_priority = display_priority
|
||||
persona.is_visible = is_visible
|
||||
persona.is_default_persona = is_default_persona
|
||||
|
||||
# Do not delete any associations manually added unless
|
||||
# a new updated list is provided
|
||||
@ -509,7 +512,7 @@ def upsert_persona(
|
||||
llm_relevance_filter=llm_relevance_filter,
|
||||
llm_filter_extraction=llm_filter_extraction,
|
||||
recency_bias=recency_bias,
|
||||
default_persona=default_persona,
|
||||
builtin_persona=builtin_persona,
|
||||
prompts=prompts or [],
|
||||
document_sets=document_sets or [],
|
||||
llm_model_provider_override=llm_model_provider_override,
|
||||
@ -521,6 +524,7 @@ def upsert_persona(
|
||||
uploaded_image_id=uploaded_image_id,
|
||||
display_priority=display_priority,
|
||||
is_visible=is_visible,
|
||||
is_default_persona=is_default_persona,
|
||||
)
|
||||
db_session.add(persona)
|
||||
|
||||
@ -550,7 +554,7 @@ def delete_old_default_personas(
|
||||
Need a more graceful fix later or those need to never have IDs"""
|
||||
stmt = (
|
||||
update(Persona)
|
||||
.where(Persona.default_persona, Persona.id > 0)
|
||||
.where(Persona.builtin_persona, Persona.id > 0)
|
||||
.values(deleted=True, name=func.concat(Persona.name, "_old"))
|
||||
)
|
||||
|
||||
@ -732,7 +736,7 @@ def delete_persona_by_name(
|
||||
persona_name: str, db_session: Session, is_default: bool = True
|
||||
) -> None:
|
||||
stmt = delete(Persona).where(
|
||||
Persona.name == persona_name, Persona.default_persona == is_default
|
||||
Persona.name == persona_name, Persona.builtin_persona == is_default
|
||||
)
|
||||
|
||||
db_session.execute(stmt)
|
||||
|
@ -66,7 +66,7 @@ def create_slack_bot_persona(
|
||||
llm_model_version_override=None,
|
||||
starter_messages=None,
|
||||
is_public=True,
|
||||
default_persona=False,
|
||||
is_default_persona=False,
|
||||
db_session=db_session,
|
||||
commit=False,
|
||||
)
|
||||
|
@ -38,6 +38,8 @@ class CreatePersonaRequest(BaseModel):
|
||||
icon_shape: int | None = None
|
||||
uploaded_image_id: str | None = None # New field for uploaded image
|
||||
remove_image: bool | None = None
|
||||
is_default_persona: bool = False
|
||||
display_priority: int | None = None
|
||||
|
||||
|
||||
class PersonaSnapshot(BaseModel):
|
||||
@ -54,7 +56,7 @@ class PersonaSnapshot(BaseModel):
|
||||
llm_model_provider_override: str | None
|
||||
llm_model_version_override: str | None
|
||||
starter_messages: list[StarterMessage] | None
|
||||
default_persona: bool
|
||||
builtin_persona: bool
|
||||
prompts: list[PromptSnapshot]
|
||||
tools: list[ToolSnapshot]
|
||||
document_sets: list[DocumentSet]
|
||||
@ -63,6 +65,7 @@ class PersonaSnapshot(BaseModel):
|
||||
icon_color: str | None
|
||||
icon_shape: int | None
|
||||
uploaded_image_id: str | None = None
|
||||
is_default_persona: bool
|
||||
|
||||
@classmethod
|
||||
def from_model(
|
||||
@ -93,7 +96,8 @@ class PersonaSnapshot(BaseModel):
|
||||
llm_model_provider_override=persona.llm_model_provider_override,
|
||||
llm_model_version_override=persona.llm_model_version_override,
|
||||
starter_messages=persona.starter_messages,
|
||||
default_persona=persona.default_persona,
|
||||
builtin_persona=persona.builtin_persona,
|
||||
is_default_persona=persona.is_default_persona,
|
||||
prompts=[PromptSnapshot.from_model(prompt) for prompt in persona.prompts],
|
||||
tools=[ToolSnapshot.from_model(tool) for tool in persona.tools],
|
||||
document_sets=[
|
||||
|
@ -40,6 +40,9 @@ class AuthTypeResponse(BaseModel):
|
||||
|
||||
class UserPreferences(BaseModel):
|
||||
chosen_assistants: list[int] | None = None
|
||||
hidden_assistants: list[int] = []
|
||||
visible_assistants: list[int] = []
|
||||
|
||||
default_model: str | None = None
|
||||
|
||||
|
||||
@ -73,6 +76,8 @@ class UserInfo(BaseModel):
|
||||
UserPreferences(
|
||||
chosen_assistants=user.chosen_assistants,
|
||||
default_model=user.default_model,
|
||||
hidden_assistants=user.hidden_assistants,
|
||||
visible_assistants=user.visible_assistants,
|
||||
)
|
||||
),
|
||||
# set to None if TRACK_EXTERNAL_IDP_EXPIRY is False so that we avoid cases
|
||||
|
@ -41,6 +41,7 @@ from danswer.dynamic_configs.factory import get_dynamic_config_store
|
||||
from danswer.server.manage.models import AllUsersResponse
|
||||
from danswer.server.manage.models import UserByEmail
|
||||
from danswer.server.manage.models import UserInfo
|
||||
from danswer.server.manage.models import UserPreferences
|
||||
from danswer.server.manage.models import UserRoleResponse
|
||||
from danswer.server.manage.models import UserRoleUpdateRequest
|
||||
from danswer.server.models import FullUserSnapshot
|
||||
@ -444,3 +445,64 @@ def update_user_assistant_list(
|
||||
.values(chosen_assistants=request.chosen_assistants)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def update_assistant_list(
|
||||
preferences: UserPreferences, assistant_id: int, show: bool
|
||||
) -> UserPreferences:
|
||||
visible_assistants = preferences.visible_assistants or []
|
||||
hidden_assistants = preferences.hidden_assistants or []
|
||||
chosen_assistants = preferences.chosen_assistants or []
|
||||
|
||||
if show:
|
||||
if assistant_id not in visible_assistants:
|
||||
visible_assistants.append(assistant_id)
|
||||
if assistant_id in hidden_assistants:
|
||||
hidden_assistants.remove(assistant_id)
|
||||
if assistant_id not in chosen_assistants:
|
||||
chosen_assistants.append(assistant_id)
|
||||
else:
|
||||
if assistant_id in visible_assistants:
|
||||
visible_assistants.remove(assistant_id)
|
||||
if assistant_id not in hidden_assistants:
|
||||
hidden_assistants.append(assistant_id)
|
||||
if assistant_id in chosen_assistants:
|
||||
chosen_assistants.remove(assistant_id)
|
||||
|
||||
preferences.visible_assistants = visible_assistants
|
||||
preferences.hidden_assistants = hidden_assistants
|
||||
preferences.chosen_assistants = chosen_assistants
|
||||
return preferences
|
||||
|
||||
|
||||
@router.patch("/user/assistant-list/update/{assistant_id}")
|
||||
def update_user_assistant_visibility(
|
||||
assistant_id: int,
|
||||
show: bool,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
if user is None:
|
||||
if AUTH_TYPE == AuthType.DISABLED:
|
||||
store = get_dynamic_config_store()
|
||||
no_auth_user = fetch_no_auth_user(store)
|
||||
preferences = no_auth_user.preferences
|
||||
updated_preferences = update_assistant_list(preferences, assistant_id, show)
|
||||
set_no_auth_user_preferences(store, updated_preferences)
|
||||
return
|
||||
else:
|
||||
raise RuntimeError("This should never happen")
|
||||
|
||||
user_preferences = UserInfo.from_model(user).preferences
|
||||
updated_preferences = update_assistant_list(user_preferences, assistant_id, show)
|
||||
|
||||
db_session.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id) # type: ignore
|
||||
.values(
|
||||
hidden_assistants=updated_preferences.hidden_assistants,
|
||||
visible_assistants=updated_preferences.visible_assistants,
|
||||
chosen_assistants=updated_preferences.chosen_assistants,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
@ -85,6 +85,7 @@ def _seed_personas(db_session: Session, personas: list[CreatePersonaRequest]) ->
|
||||
is_public=persona.is_public,
|
||||
db_session=db_session,
|
||||
tool_ids=persona.tool_ids,
|
||||
display_priority=persona.display_priority,
|
||||
)
|
||||
|
||||
|
||||
|
@ -383,6 +383,7 @@ export function AssistantEditor({
|
||||
} else {
|
||||
[promptResponse, personaResponse] = await createPersona({
|
||||
...values,
|
||||
is_default_persona: admin!,
|
||||
num_chunks: numChunks,
|
||||
users:
|
||||
user && !checkUserIsNoAuthUser(user.id) ? [user.id] : undefined,
|
||||
@ -414,10 +415,7 @@ export function AssistantEditor({
|
||||
shouldAddAssistantToUserPreferences &&
|
||||
user?.preferences?.chosen_assistants
|
||||
) {
|
||||
const success = await addAssistantToList(
|
||||
assistantId,
|
||||
user.preferences.chosen_assistants
|
||||
);
|
||||
const success = await addAssistantToList(assistantId);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
|
@ -20,7 +20,7 @@ import { UserRole, User } from "@/lib/types";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
|
||||
if (persona.default_persona) {
|
||||
if (persona.is_default_persona) {
|
||||
return <Text>Built-In</Text>;
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ export function PersonasTable({
|
||||
id: persona.id.toString(),
|
||||
cells: [
|
||||
<div key="name" className="flex">
|
||||
{!persona.default_persona && (
|
||||
{!persona.is_default_persona && (
|
||||
<FiEdit2
|
||||
className="mr-1 my-auto cursor-pointer"
|
||||
onClick={() =>
|
||||
@ -173,7 +173,7 @@ export function PersonasTable({
|
||||
</div>,
|
||||
<div key="edit" className="flex">
|
||||
<div className="mx-auto my-auto">
|
||||
{!persona.default_persona && isEditable ? (
|
||||
{!persona.is_default_persona && isEditable ? (
|
||||
<div
|
||||
className="hover:bg-hover rounded p-1 cursor-pointer"
|
||||
onClick={async () => {
|
||||
|
@ -35,7 +35,8 @@ export interface Persona {
|
||||
llm_model_provider_override?: string;
|
||||
llm_model_version_override?: string;
|
||||
starter_messages: StarterMessage[] | null;
|
||||
default_persona: boolean;
|
||||
builtin_persona: boolean;
|
||||
is_default_persona: boolean;
|
||||
users: MinimalUserSnapshot[];
|
||||
groups: number[];
|
||||
icon_shape?: number;
|
||||
|
@ -21,6 +21,7 @@ interface PersonaCreationRequest {
|
||||
icon_shape: number | null;
|
||||
remove_image?: boolean;
|
||||
uploaded_image: File | null;
|
||||
is_default_persona: boolean;
|
||||
}
|
||||
|
||||
interface PersonaUpdateRequest {
|
||||
@ -125,6 +126,11 @@ function buildPersonaAPIBody(
|
||||
remove_image,
|
||||
} = creationRequest;
|
||||
|
||||
const is_default_persona =
|
||||
"is_default_persona" in creationRequest
|
||||
? creationRequest.is_default_persona
|
||||
: false;
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
@ -145,6 +151,7 @@ function buildPersonaAPIBody(
|
||||
icon_shape,
|
||||
uploaded_image_id,
|
||||
remove_image,
|
||||
is_default_persona,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,11 @@ import { FiLock, FiUnlock } from "react-icons/fi";
|
||||
export function AssistantSharedStatusDisplay({
|
||||
assistant,
|
||||
user,
|
||||
size = "sm",
|
||||
}: {
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}) {
|
||||
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
|
||||
|
||||
@ -18,7 +20,9 @@ export function AssistantSharedStatusDisplay({
|
||||
|
||||
if (assistant.is_public) {
|
||||
return (
|
||||
<div className="text-subtle text-sm flex items-center">
|
||||
<div
|
||||
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
|
||||
>
|
||||
<FiUnlock className="mr-1" />
|
||||
Public
|
||||
</div>
|
||||
@ -27,7 +31,9 @@ export function AssistantSharedStatusDisplay({
|
||||
|
||||
if (assistantSharedUsersWithoutOwner.length > 0) {
|
||||
return (
|
||||
<div className="text-subtle text-sm flex items-center">
|
||||
<div
|
||||
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
|
||||
>
|
||||
<FiUnlock className="mr-1" />
|
||||
{isOwnedByUser ? (
|
||||
`Shared with: ${
|
||||
@ -54,7 +60,9 @@ export function AssistantSharedStatusDisplay({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-subtle text-sm flex items-center">
|
||||
<div
|
||||
className={`text-subtle ${size === "sm" ? "text-sm" : size === "md" ? "text-base" : "text-lg"} flex items-center`}
|
||||
>
|
||||
<FiLock className="mr-1" />
|
||||
Private
|
||||
</div>
|
||||
|
@ -6,10 +6,11 @@ export function AssistantsPageTitle({
|
||||
return (
|
||||
<h1
|
||||
className="
|
||||
text-4xl
|
||||
text-5xl
|
||||
font-bold
|
||||
mb-4
|
||||
text-center
|
||||
text-text-900
|
||||
"
|
||||
>
|
||||
{children}
|
||||
|
@ -1,41 +1,5 @@
|
||||
import { Bubble } from "@/components/Bubble";
|
||||
import { ToolSnapshot } from "@/lib/tools/interfaces";
|
||||
import { FiImage, FiSearch, FiGlobe, FiMoreHorizontal } from "react-icons/fi";
|
||||
import { FiImage, FiSearch } from "react-icons/fi";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ToolsDisplay({ tools }: { tools: ToolSnapshot[] }) {
|
||||
return (
|
||||
<div className="text-xs text-subtle flex flex-wrap gap-1 mt-2">
|
||||
<p className="text-sm text-default my-auto">Tools:</p>
|
||||
{tools.map((tool) => {
|
||||
let toolName = tool.name;
|
||||
let toolIcon = null;
|
||||
|
||||
if (tool.name === "SearchTool") {
|
||||
toolName = "Search";
|
||||
toolIcon = <FiSearch className="mr-1 my-auto" />;
|
||||
} else if (tool.name === "ImageGenerationTool") {
|
||||
toolName = "Image Generation";
|
||||
toolIcon = <FiImage className="mr-1 my-auto" />;
|
||||
} else if (tool.name === "InternetSearchTool") {
|
||||
toolName = "Internet Search";
|
||||
toolIcon = <FiGlobe className="mr-1 my-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Bubble key={tool.id} isSelected={false} notSelectable>
|
||||
<div className="flex flex-row gap-0.5">
|
||||
{toolIcon}
|
||||
{toolName}
|
||||
</div>
|
||||
</Bubble>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssistantTools({
|
||||
assistant,
|
||||
|
@ -6,17 +6,137 @@ import { User } from "@/lib/types";
|
||||
import { Button } from "@tremor/react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { FiMinus, FiPlus, FiX } from "react-icons/fi";
|
||||
import { NavigationButton } from "../NavigationButton";
|
||||
import { FiList, FiMinus, FiPlus } from "react-icons/fi";
|
||||
import { AssistantsPageTitle } from "../AssistantsPageTitle";
|
||||
import {
|
||||
addAssistantToList,
|
||||
removeAssistantFromList,
|
||||
} from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AssistantTools, ToolsDisplay } from "../ToolsDisplay";
|
||||
import { AssistantTools } from "../ToolsDisplay";
|
||||
import { classifyAssistants } from "@/lib/assistants/utils";
|
||||
export function AssistantGalleryCard({
|
||||
assistant,
|
||||
user,
|
||||
setPopup,
|
||||
selectedAssistant,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
setPopup: (popup: PopupSpec) => void;
|
||||
selectedAssistant: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div
|
||||
key={assistant.id}
|
||||
className="
|
||||
bg-background-emphasis
|
||||
rounded-lg
|
||||
shadow-md
|
||||
p-4
|
||||
"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
<h2
|
||||
className="
|
||||
text-xl
|
||||
font-semibold
|
||||
my-auto
|
||||
ml-2
|
||||
text-strong
|
||||
line-clamp-2
|
||||
"
|
||||
>
|
||||
{assistant.name}
|
||||
</h2>
|
||||
{user && (
|
||||
<div className="ml-auto">
|
||||
{selectedAssistant ? (
|
||||
<Button
|
||||
className="
|
||||
mr-2
|
||||
my-auto
|
||||
bg-background-700
|
||||
hover:bg-background-600
|
||||
"
|
||||
icon={FiMinus}
|
||||
onClick={async () => {
|
||||
if (
|
||||
user.preferences?.chosen_assistants &&
|
||||
user.preferences?.chosen_assistants.length === 1
|
||||
) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await removeAssistantFromList(assistant.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
Deselect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="
|
||||
mr-2
|
||||
my-auto
|
||||
bg-accent
|
||||
hover:bg-accent-hover
|
||||
"
|
||||
icon={FiPlus}
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(assistant.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="green"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2">{assistant.description}</p>
|
||||
<p className="text-subtle text-sm my-2">
|
||||
Author: {assistant.owner?.email || "Danswer"}
|
||||
</p>
|
||||
{assistant.tools.length > 0 && (
|
||||
<AssistantTools list assistant={assistant} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function AssistantsGallery({
|
||||
assistants,
|
||||
user,
|
||||
@ -25,182 +145,171 @@ export function AssistantsGallery({
|
||||
|
||||
user: User | null;
|
||||
}) {
|
||||
function filterAssistants(assistants: Persona[], query: string): Persona[] {
|
||||
return assistants.filter(
|
||||
(assistant) =>
|
||||
assistant.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
assistant.description.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const allAssistantIds = assistants.map((assistant) => assistant.id);
|
||||
const filteredAssistants = filterAssistants(assistants, searchQuery);
|
||||
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
|
||||
user,
|
||||
assistants
|
||||
);
|
||||
|
||||
const defaultAssistants = assistants
|
||||
.filter((assistant) => assistant.is_default_persona)
|
||||
.filter(
|
||||
(assistant) =>
|
||||
assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
assistant.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const nonDefaultAssistants = assistants
|
||||
.filter((assistant) => !assistant.is_default_persona)
|
||||
.filter(
|
||||
(assistant) =>
|
||||
assistant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
assistant.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Assistant Gallery</AssistantsPageTitle>
|
||||
<div className="flex justify-center mb-6">
|
||||
<Link href="/assistants/mine">
|
||||
<NavigationButton>View Your Assistants</NavigationButton>
|
||||
</Link>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-4 mb-6">
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/new")}
|
||||
className="w-full py-3 text-lg rounded-full bg-background-800 text-white hover:bg-background-800 transition duration-300 ease-in-out"
|
||||
icon={FiPlus}
|
||||
>
|
||||
Create New Assistant
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/mine")}
|
||||
className="w-full hover:border-border-strong py-3 text-lg rounded-full bg-white border border-border shadow text-text-700 hover:bg-background-50 transition duration-300 ease-in-out"
|
||||
icon={FiList}
|
||||
>
|
||||
Your Assistants
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-center mb-6">
|
||||
Discover and create custom assistants that combine instructions, extra
|
||||
knowledge, and any combination of tools.
|
||||
</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search assistants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="
|
||||
w-full
|
||||
p-2
|
||||
border
|
||||
border-gray-300
|
||||
rounded
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-blue-500
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
w-full
|
||||
grid
|
||||
grid-cols-2
|
||||
gap-4
|
||||
py-2
|
||||
"
|
||||
>
|
||||
{filteredAssistants.map((assistant) => (
|
||||
<div
|
||||
key={assistant.id}
|
||||
<div className="mt-4 mb-12">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search assistants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="
|
||||
bg-background-emphasis
|
||||
rounded-lg
|
||||
shadow-md
|
||||
p-4
|
||||
"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
<h2
|
||||
className="
|
||||
text-xl
|
||||
font-semibold
|
||||
my-auto
|
||||
ml-2
|
||||
text-strong
|
||||
line-clamp-2
|
||||
"
|
||||
>
|
||||
{assistant.name}
|
||||
</h2>
|
||||
{user && (
|
||||
<div className="ml-auto">
|
||||
{!user.preferences?.chosen_assistants ||
|
||||
user.preferences?.chosen_assistants?.includes(
|
||||
assistant.id
|
||||
) ? (
|
||||
<Button
|
||||
className="
|
||||
mr-2
|
||||
my-auto
|
||||
"
|
||||
icon={FiMinus}
|
||||
onClick={async () => {
|
||||
if (
|
||||
user.preferences?.chosen_assistants &&
|
||||
user.preferences?.chosen_assistants.length === 1
|
||||
) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await removeAssistantFromList(
|
||||
assistant.id,
|
||||
user.preferences?.chosen_assistants ||
|
||||
allAssistantIds
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="blue"
|
||||
>
|
||||
Deselect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="
|
||||
mr-2
|
||||
my-auto
|
||||
"
|
||||
icon={FiPlus}
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(
|
||||
assistant.id,
|
||||
user.preferences?.chosen_assistants ||
|
||||
allAssistantIds
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="green"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2">{assistant.description}</p>
|
||||
<p className="text-subtle text-sm my-2">
|
||||
Author: {assistant.owner?.email || "Danswer"}
|
||||
</p>
|
||||
{assistant.tools.length > 0 && (
|
||||
<AssistantTools list assistant={assistant} />
|
||||
)}
|
||||
w-full
|
||||
py-3
|
||||
px-4
|
||||
pl-10
|
||||
text-lg
|
||||
border-2
|
||||
border-background-strong
|
||||
rounded-full
|
||||
bg-background-50
|
||||
text-text-700
|
||||
placeholder-text-400
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-primary-500
|
||||
focus:border-transparent
|
||||
transition duration-300 ease-in-out
|
||||
"
|
||||
/>
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
className="h-5 w-5 text-text-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{defaultAssistants.length == 0 &&
|
||||
nonDefaultAssistants.length == 0 &&
|
||||
assistants.length != 0 && (
|
||||
<div className="text-text-500">
|
||||
No assistants found for this search
|
||||
</div>
|
||||
)}
|
||||
|
||||
{defaultAssistants.length > 0 && (
|
||||
<>
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-text-900">
|
||||
Default Assistants
|
||||
</h2>
|
||||
|
||||
<h3 className="text-lg text-text-500">
|
||||
These are assistant created by your admins are and preferred.
|
||||
</h3>
|
||||
</section>
|
||||
<div
|
||||
className="
|
||||
w-full
|
||||
grid
|
||||
grid-cols-2
|
||||
gap-4
|
||||
py-2
|
||||
"
|
||||
>
|
||||
{defaultAssistants.map((assistant) => (
|
||||
<AssistantGalleryCard
|
||||
selectedAssistant={visibleAssistants.includes(assistant)}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{nonDefaultAssistants.length > 0 && (
|
||||
<section className="mt-12 mb-8 flex flex-col gap-y-2">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-2xl font-semibold text-text-900">
|
||||
Other Assistants
|
||||
</h2>
|
||||
<h3 className="text-lg text-text-500">
|
||||
These are community-contributed assistants.
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="
|
||||
w-full
|
||||
grid
|
||||
grid-cols-2
|
||||
gap-4
|
||||
py-2
|
||||
"
|
||||
>
|
||||
{nonDefaultAssistants.map((assistant) => (
|
||||
<AssistantGalleryCard
|
||||
selectedAssistant={visibleAssistants.includes(assistant)}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -73,7 +73,11 @@ export function AssistantSharingModal({
|
||||
let sharedStatus = null;
|
||||
if (assistant.is_public || !sharedUsersWithoutOwner.length) {
|
||||
sharedStatus = (
|
||||
<AssistantSharedStatusDisplay assistant={assistant} user={user} />
|
||||
<AssistantSharedStatusDisplay
|
||||
size="md"
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
sharedStatus = (
|
||||
@ -122,27 +126,30 @@ export function AssistantSharingModal({
|
||||
<>
|
||||
{popup}
|
||||
<Modal
|
||||
width="max-w-3xl w-full"
|
||||
title={
|
||||
<div className="flex">
|
||||
<AssistantIcon assistant={assistant} />{" "}
|
||||
<div className="ml-2 my-auto">{assistantName}</div>
|
||||
<div className="flex items-end space-x-3">
|
||||
<AssistantIcon size="large" assistant={assistant} />
|
||||
<h2 className="text-3xl text-text-800 font-semibold">
|
||||
{assistantName}
|
||||
</h2>
|
||||
</div>
|
||||
}
|
||||
onOutsideClick={onClose}
|
||||
>
|
||||
<div className="px-4">
|
||||
<div>
|
||||
{isUpdating && <Spinner />}
|
||||
<Text className="mb-5">
|
||||
Control which other users should have access to this assistant.
|
||||
</Text>
|
||||
<p className="text-text-600 text-lg mb-6">
|
||||
Manage access to this assistant by sharing it with other users.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-2">Current status:</p>
|
||||
{sharedStatus}
|
||||
<div className="mb-8 flex flex-col gap-y-4">
|
||||
<h3 className="text-lg font-semibold">Current Status</h3>
|
||||
<div className="bg-gray-50 rounded-lg">{sharedStatus}</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-default font-bold mb-4 mt-3">Share Assistant:</h3>
|
||||
<div className="mt-4">
|
||||
<div className="mb-8 flex flex-col gap-y-4">
|
||||
<h3 className="text-lg font-semibold">Share Assistant</h3>
|
||||
<SearchMultiSelectDropdown
|
||||
options={allUsers
|
||||
.filter(
|
||||
@ -153,12 +160,10 @@ export function AssistantSharingModal({
|
||||
.includes(u1.id) &&
|
||||
u1.id !== user?.id
|
||||
)
|
||||
.map((user) => {
|
||||
return {
|
||||
name: user.email,
|
||||
value: user.id,
|
||||
};
|
||||
})}
|
||||
.map((user) => ({
|
||||
name: user.email,
|
||||
value: user.id,
|
||||
}))}
|
||||
onSelect={(option) => {
|
||||
setSelectedUsers([
|
||||
...Array.from(
|
||||
@ -170,18 +175,22 @@ export function AssistantSharingModal({
|
||||
]);
|
||||
}}
|
||||
itemComponent={({ option }) => (
|
||||
<div className="flex px-4 py-2.5 cursor-pointer hover:bg-hover">
|
||||
<UsersIcon className="mr-2 my-auto" />
|
||||
{option.name}
|
||||
<div className="ml-auto my-auto">
|
||||
<FiPlus />
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2.5 cursor-pointer hover:bg-gray-100">
|
||||
<UsersIcon className="mr-3 text-gray-500" />
|
||||
<span className="flex-grow">{option.name}</span>
|
||||
<FiPlus className="text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-2 flex flex-wrap gap-x-2">
|
||||
{selectedUsers.length > 0 &&
|
||||
selectedUsers.map((selectedUser) => (
|
||||
</div>
|
||||
|
||||
{selectedUsers.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
||||
Selected Users:
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedUsers.map((selectedUser) => (
|
||||
<div
|
||||
key={selectedUser.id}
|
||||
onClick={() => {
|
||||
@ -191,35 +200,29 @@ export function AssistantSharingModal({
|
||||
)
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
flex
|
||||
rounded-lg
|
||||
px-2
|
||||
py-1
|
||||
border
|
||||
border-border
|
||||
hover:bg-hover-light
|
||||
cursor-pointer`}
|
||||
className="flex items-center bg-blue-50 text-blue-700 rounded-full px-3 py-1 text-sm hover:bg-blue-100 transition-colors duration-200 cursor-pointer"
|
||||
>
|
||||
{selectedUser.email} <FiX className="ml-1 my-auto" />
|
||||
{selectedUser.email}
|
||||
<FiX className="ml-2 text-blue-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedUsers.length > 0 && (
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
handleShare();
|
||||
setSelectedUsers([]);
|
||||
}}
|
||||
size="xs"
|
||||
color="blue"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{selectedUsers.length > 0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleShare();
|
||||
setSelectedUsers([]);
|
||||
}}
|
||||
size="sm"
|
||||
color="blue"
|
||||
className="w-full"
|
||||
>
|
||||
Share with Selected Users
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
@ -1,25 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import React, {
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { MinimalUserSnapshot, User } from "@/lib/types";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { Divider, Text } from "@tremor/react";
|
||||
import { Button, Divider, Text } from "@tremor/react";
|
||||
import {
|
||||
FiEdit2,
|
||||
FiFigma,
|
||||
FiMenu,
|
||||
FiList,
|
||||
FiMinus,
|
||||
FiMoreHorizontal,
|
||||
FiPlus,
|
||||
FiSearch,
|
||||
FiShare,
|
||||
FiShare2,
|
||||
FiToggleLeft,
|
||||
FiTrash,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
|
||||
import {
|
||||
addAssistantToList,
|
||||
removeAssistantFromList,
|
||||
@ -29,14 +30,12 @@ import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { DefaultPopover } from "@/components/popover/DefaultPopover";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { NavigationButton } from "../NavigationButton";
|
||||
import { AssistantsPageTitle } from "../AssistantsPageTitle";
|
||||
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
|
||||
import { AssistantSharingModal } from "./AssistantSharingModal";
|
||||
import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { AssistantTools } from "../ToolsDisplay";
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
@ -62,6 +61,12 @@ import {
|
||||
} from "@/app/admin/assistants/lib";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantModal";
|
||||
import {
|
||||
classifyAssistants,
|
||||
getUserCreatedAssistants,
|
||||
orderAssistantsForUser,
|
||||
} from "@/lib/assistants/utils";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
|
||||
function DraggableAssistantListItem(props: any) {
|
||||
const {
|
||||
@ -83,12 +88,12 @@ function DraggableAssistantListItem(props: any) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="flex items-center">
|
||||
<div ref={setNodeRef} style={style} className="flex mt-2 items-center">
|
||||
<div {...attributes} {...listeners} className="mr-2 cursor-grab">
|
||||
<DragHandle />
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<AssistantListItem {...props} />
|
||||
<AssistantListItem isDragging={isDragging} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -97,21 +102,21 @@ function DraggableAssistantListItem(props: any) {
|
||||
function AssistantListItem({
|
||||
assistant,
|
||||
user,
|
||||
allAssistantIds,
|
||||
allUsers,
|
||||
isVisible,
|
||||
setPopup,
|
||||
deleteAssistant,
|
||||
shareAssistant,
|
||||
isDragging,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
allUsers: MinimalUserSnapshot[];
|
||||
allAssistantIds: string[];
|
||||
isVisible: boolean;
|
||||
deleteAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
shareAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
isDragging?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [showSharingModal, setShowSharingModal] = useState(false);
|
||||
@ -133,155 +138,154 @@ function AssistantListItem({
|
||||
show={showSharingModal}
|
||||
/>
|
||||
<div
|
||||
className="flex bg-background-emphasis
|
||||
rounded-lg
|
||||
shadow-md
|
||||
p-4
|
||||
mb-4 flex-col"
|
||||
className={`rounded-lg px-4 py-6 transition-all duration-900 hover:bg-background-125 ${isDragging && "bg-background-125"}`}
|
||||
>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
"
|
||||
>
|
||||
<div className="w-3/4">
|
||||
<div className="flex items-center">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
<h2 className="text-xl line-clamp-2 font-semibold my-auto ml-2">
|
||||
{assistant.name}
|
||||
</h2>
|
||||
<div className="flex justify-between items-center">
|
||||
<AssistantIcon assistant={assistant} />
|
||||
|
||||
<h2 className="ml-6 w-fit flex-grow space-y-3 text-start flex text-xl font-semibold line-clamp-2 text-gray-800">
|
||||
{assistant.name}
|
||||
</h2>
|
||||
|
||||
{/* {isOwnedByUser && ( */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex mr-20 flex-wrap items-center gap-x-4">
|
||||
{assistant.tools.length > 0 && (
|
||||
<p className="text-base flex w-fit text-subtle">
|
||||
{assistant.tools.length} tool
|
||||
{assistant.tools.length > 1 && "s"}
|
||||
</p>
|
||||
)}
|
||||
<AssistantSharedStatusDisplay
|
||||
size="md"
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-sm mt-2">{assistant.description}</div>
|
||||
<div className="mt-2 flex items-start gap-y-2 flex-col gap-x-3">
|
||||
<AssistantSharedStatusDisplay assistant={assistant} user={user} />
|
||||
{assistant.tools.length != 0 && (
|
||||
<AssistantTools list assistant={assistant} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOwnedByUser && (
|
||||
<div className="ml-auto flex items-center">
|
||||
{!assistant.is_public && (
|
||||
<div
|
||||
className="mr-4 rounded p-2 cursor-pointer hover:bg-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowSharingModal(true);
|
||||
}}
|
||||
>
|
||||
<FiShare2 size={16} />
|
||||
</div>
|
||||
)}
|
||||
{isOwnedByUser ? (
|
||||
<Link
|
||||
href={`/assistants/edit/${assistant.id}`}
|
||||
className="mr-4 rounded p-2 cursor-pointer hover:bg-hover"
|
||||
className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200"
|
||||
title="Edit assistant"
|
||||
>
|
||||
<FiEdit2 size={16} />
|
||||
<FiEdit2 size={20} className="text-text-900" />
|
||||
</Link>
|
||||
|
||||
<DefaultPopover
|
||||
content={
|
||||
<div className="hover:bg-hover rounded p-2 cursor-pointer">
|
||||
<FiMoreHorizontal size={16} />
|
||||
</div>
|
||||
}
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={5}
|
||||
) : (
|
||||
<CustomTooltip
|
||||
showTick
|
||||
content="You don't have permission to edit this assistant"
|
||||
>
|
||||
{[
|
||||
isVisible ? (
|
||||
<div
|
||||
key="remove"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={async () => {
|
||||
if (
|
||||
currentChosenAssistants &&
|
||||
currentChosenAssistants.length === 1
|
||||
) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const success = await removeAssistantFromList(
|
||||
assistant.id,
|
||||
currentChosenAssistants || allAssistantIds
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiX /> {isOwnedByUser ? "Hide" : "Remove"}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key="add"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(
|
||||
assistant.id,
|
||||
currentChosenAssistants || allAssistantIds
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiPlus /> Add
|
||||
</div>
|
||||
),
|
||||
isOwnedByUser ? (
|
||||
<div
|
||||
key="delete"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={() => deleteAssistant(assistant)}
|
||||
>
|
||||
<FiTrash /> Delete
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
isOwnedByUser ? (
|
||||
<div
|
||||
key="delete"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={() => shareAssistant(assistant)}
|
||||
>
|
||||
{assistant.is_public ? <FiMinus /> : <FiPlus />} Make{" "}
|
||||
{assistant.is_public ? "Private" : "Public"}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
]}
|
||||
</DefaultPopover>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 cursor-not-allowed opacity-50 rounded-full hover:bg-gray-100 transition-colors duration-200">
|
||||
<FiEdit2 size={20} className="text-text-900" />
|
||||
</div>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
|
||||
<DefaultPopover
|
||||
content={
|
||||
<div className="p-2 rounded-full hover:bg-gray-100 transition-colors duration-200 cursor-pointer">
|
||||
<FiMoreHorizontal size={20} className="text-text-900" />
|
||||
</div>
|
||||
}
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
>
|
||||
{[
|
||||
isVisible ? (
|
||||
<button
|
||||
key="remove"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={async () => {
|
||||
if (currentChosenAssistants?.length === 1) {
|
||||
setPopup({
|
||||
message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const success = await removeAssistantFromList(
|
||||
assistant.id
|
||||
);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiX size={18} className="text-text-800" />{" "}
|
||||
{isOwnedByUser ? "Hide" : "Remove"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
key="add"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={async () => {
|
||||
const success = await addAssistantToList(assistant.id);
|
||||
if (success) {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FiPlus size={18} className="text-text-800" /> Add
|
||||
</button>
|
||||
),
|
||||
isOwnedByUser ? (
|
||||
<button
|
||||
key="delete"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left text-red-600"
|
||||
onClick={() => deleteAssistant(assistant)}
|
||||
>
|
||||
<FiTrash size={18} /> Delete
|
||||
</button>
|
||||
) : null,
|
||||
isOwnedByUser ? (
|
||||
<button
|
||||
key="visibility"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={() => shareAssistant(assistant)}
|
||||
>
|
||||
{assistant.is_public ? (
|
||||
<FiMinus size={18} className="text-text-800" />
|
||||
) : (
|
||||
<FiPlus size={18} className="text-text-800" />
|
||||
)}{" "}
|
||||
Make {assistant.is_public ? "Private" : "Public"}
|
||||
</button>
|
||||
) : null,
|
||||
!assistant.is_public ? (
|
||||
<button
|
||||
key="share"
|
||||
className="flex items-center gap-x-2 px-4 py-2 hover:bg-gray-100 w-full text-left"
|
||||
onClick={(e) => {
|
||||
setShowSharingModal(true);
|
||||
}}
|
||||
>
|
||||
<FiShare2 size={18} className="text-text-800" /> Share
|
||||
</button>
|
||||
) : null,
|
||||
]}
|
||||
</DefaultPopover>
|
||||
</div>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -294,17 +298,19 @@ export function AssistantsList({
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
const [filteredAssistants, setFilteredAssistants] = useState<Persona[]>([]);
|
||||
// Define the distinct groups of assistants
|
||||
const { visibleAssistants, hiddenAssistants } = classifyAssistants(
|
||||
user,
|
||||
assistants
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredAssistants(orderAssistantsForUser(assistants, user));
|
||||
}, [user, assistants, orderAssistantsForUser]);
|
||||
const [currentlyVisibleAssistants, setCurrentlyVisibleAssistants] = useState<
|
||||
Persona[]
|
||||
>(orderAssistantsForUser(visibleAssistants, user));
|
||||
|
||||
const ownedButHiddenAssistants = assistants.filter(
|
||||
(assistant) =>
|
||||
checkUserOwnsAssistant(user, assistant) &&
|
||||
user?.preferences?.chosen_assistants &&
|
||||
!user?.preferences?.chosen_assistants?.includes(assistant.id)
|
||||
const ownedButHiddenAssistants = getUserCreatedAssistants(
|
||||
user,
|
||||
hiddenAssistants
|
||||
);
|
||||
|
||||
const allAssistantIds = assistants.map((assistant) =>
|
||||
@ -332,9 +338,9 @@ export function AssistantsList({
|
||||
|
||||
async function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
filteredAssistants;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
setFilteredAssistants((assistants) => {
|
||||
setCurrentlyVisibleAssistants((assistants) => {
|
||||
const oldIndex = assistants.findIndex(
|
||||
(a) => a.id.toString() === active.id
|
||||
);
|
||||
@ -390,44 +396,36 @@ export function AssistantsList({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mx-auto mobile:w-[90%] desktop:w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>My Assistants</AssistantsPageTitle>
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Your Assistants</AssistantsPageTitle>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
<Link href="/assistants/new">
|
||||
<NavigationButton>
|
||||
<div className="flex justify-center">
|
||||
<FiPlus className="mr-2 my-auto" size={20} />
|
||||
Create New Assistant
|
||||
</div>
|
||||
</NavigationButton>
|
||||
</Link>
|
||||
<div className="grid grid-cols-2 gap-4 mt-4 mb-8">
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/new")}
|
||||
className="w-full py-3 text-lg rounded-full bg-background-800 text-white hover:bg-background-800 transition duration-300 ease-in-out"
|
||||
icon={FiPlus}
|
||||
>
|
||||
Create New Assistant
|
||||
</Button>
|
||||
|
||||
<Link href="/assistants/gallery">
|
||||
<NavigationButton>
|
||||
<div className="flex justify-center">
|
||||
<FiSearch className="mr-2 my-auto" size={20} />
|
||||
View Available Assistants
|
||||
</div>
|
||||
</NavigationButton>
|
||||
</Link>
|
||||
<Button
|
||||
onClick={() => router.push("/assistants/gallery")}
|
||||
className="w-full hover:border-border-strong py-3 text-lg rounded-full bg-white border !border-border shadow text-text-700 hover:bg-background-50 transition duration-300 ease-in-out"
|
||||
icon={FiList}
|
||||
>
|
||||
Assistant Gallery
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-center text-base">
|
||||
Assistants allow you to customize your experience for a specific
|
||||
purpose. Specifically, they combine instructions, extra knowledge, and
|
||||
any combination of tools.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mb-2 text-text-900">
|
||||
Active Assistants
|
||||
</h2>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h3 className="text-xl font-bold mb-4">Active Assistants</h3>
|
||||
|
||||
<Text>
|
||||
<h3 className="text-lg text-text-500">
|
||||
The order the assistants appear below will be the order they appear in
|
||||
the Assistants dropdown. The first assistant listed will be your
|
||||
default assistant when you start a new chat. Drag and drop to reorder.
|
||||
</Text>
|
||||
</h3>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@ -435,11 +433,11 @@ export function AssistantsList({
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAssistants.map((a) => a.id.toString())}
|
||||
items={currentlyVisibleAssistants.map((a) => a.id.toString())}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="w-full p-4 mt-3">
|
||||
{filteredAssistants.map((assistant, index) => (
|
||||
<div className="w-full items-center py-4">
|
||||
{currentlyVisibleAssistants.map((assistant, index) => (
|
||||
<DraggableAssistantListItem
|
||||
deleteAssistant={setDeletingPersona}
|
||||
shareAssistant={setMakePublicPersona}
|
||||
@ -462,10 +460,10 @@ export function AssistantsList({
|
||||
|
||||
<h3 className="text-xl font-bold mb-4">Your Hidden Assistants</h3>
|
||||
|
||||
<Text>
|
||||
<h3 className="text-lg text-text-500">
|
||||
Assistants you've created that aren't currently visible
|
||||
in the Assistants selector.
|
||||
</Text>
|
||||
</h3>
|
||||
|
||||
<div className="w-full p-4">
|
||||
{ownedButHiddenAssistants.map((assistant, index) => (
|
||||
@ -475,7 +473,6 @@ export function AssistantsList({
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
allAssistantIds={allAssistantIds}
|
||||
allUsers={users || []}
|
||||
isVisible={false}
|
||||
setPopup={setPopup}
|
||||
|
@ -86,7 +86,6 @@ import {
|
||||
import { ChatInputBar } from "./input/ChatInputBar";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
|
||||
import { ChatPopup } from "./ChatPopup";
|
||||
|
||||
import FunctionalHeader from "@/components/chat_search/Header";
|
||||
@ -101,6 +100,10 @@ import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
|
||||
import { SEARCH_TOOL_NAME } from "./tools/constants";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
|
||||
import {
|
||||
classifyAssistants,
|
||||
orderAssistantsForUser,
|
||||
} from "@/lib/assistants/utils";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@ -136,7 +139,6 @@ export function ChatPage({
|
||||
|
||||
const { user, refreshUser, isLoadingUser } = useUser();
|
||||
|
||||
// chat session
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
||||
|
||||
@ -155,7 +157,10 @@ export function ChatPage({
|
||||
const loadedIdSessionRef = useRef<number | null>(existingChatSessionId);
|
||||
|
||||
// Assistants
|
||||
const filteredAssistants = orderAssistantsForUser(availableAssistants, user);
|
||||
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
|
||||
user,
|
||||
availableAssistants
|
||||
);
|
||||
|
||||
const existingChatSessionAssistantId = selectedChatSession?.persona_id;
|
||||
const [selectedAssistant, setSelectedAssistant] = useState<
|
||||
@ -210,7 +215,7 @@ export function ChatPage({
|
||||
const liveAssistant =
|
||||
alternativeAssistant ||
|
||||
selectedAssistant ||
|
||||
filteredAssistants[0] ||
|
||||
visibleAssistants[0] ||
|
||||
availableAssistants[0];
|
||||
|
||||
useEffect(() => {
|
||||
@ -680,7 +685,7 @@ export function ChatPage({
|
||||
useEffect(() => {
|
||||
if (messageHistory.length === 0 && chatSessionIdRef.current === null) {
|
||||
setSelectedAssistant(
|
||||
filteredAssistants.find((persona) => persona.id === defaultAssistantId)
|
||||
visibleAssistants.find((persona) => persona.id === defaultAssistantId)
|
||||
);
|
||||
}
|
||||
}, [defaultAssistantId]);
|
||||
@ -2379,7 +2384,10 @@ export function ChatPage({
|
||||
showDocs={() => setDocumentSelection(true)}
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
assistantOptions={filteredAssistants}
|
||||
assistantOptions={orderAssistantsForUser(
|
||||
visibleAssistants,
|
||||
user
|
||||
)}
|
||||
selectedAssistant={liveAssistant}
|
||||
setSelectedAssistant={onAssistantChange}
|
||||
setAlternativeAssistant={setAlternativeAssistant}
|
||||
|
@ -20,6 +20,7 @@ import { getFinalLLM } from "@/lib/llm/utils";
|
||||
import React, { useState } from "react";
|
||||
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
|
||||
import { orderAssistantsForUser } from "@/lib/assistants/utils";
|
||||
|
||||
export function AssistantsTab({
|
||||
selectedAssistant,
|
||||
|
@ -6,6 +6,7 @@ import { ChatProvider } from "@/components/context/ChatContext";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import WrappedChat from "./WrappedChat";
|
||||
import { ProviderContextProvider } from "@/components/chat_search/ProviderContext";
|
||||
import { orderAssistantsForUser } from "@/lib/assistants/utils";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
|
@ -207,7 +207,6 @@ export function ChatSessionDisplay({
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
setIsMoreOptionsDropdownOpen(
|
||||
!isMoreOptionsDropdownOpen
|
||||
);
|
||||
|
@ -192,7 +192,7 @@ export function SearchMultiSelectDropdown({
|
||||
export const CustomDropdown = ({
|
||||
children,
|
||||
dropdown,
|
||||
direction = "down", // Default to 'down' if not specified
|
||||
direction = "down",
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
dropdown: JSX.Element | string;
|
||||
|
@ -3,6 +3,7 @@ import React from "react";
|
||||
import { Tooltip } from "../tooltip/Tooltip";
|
||||
import { createSVG } from "@/lib/assistantIconUtils";
|
||||
import { buildImgUrl } from "@/app/chat/files/images/utils";
|
||||
import { CustomTooltip } from "../tooltip/CustomTooltip";
|
||||
|
||||
export function darkerGenerateColorFromId(id: string): string {
|
||||
const hash = Array.from(id).reduce(
|
||||
@ -30,7 +31,7 @@ export function AssistantIcon({
|
||||
const color = darkerGenerateColorFromId(assistant.id.toString());
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={1000} content={assistant.description}>
|
||||
<CustomTooltip showTick line wrap content={assistant.description}>
|
||||
{
|
||||
// Prioritization order: image, graph, defaults
|
||||
assistant.uploaded_image_id ? (
|
||||
@ -44,7 +45,7 @@ export function AssistantIcon({
|
||||
<div
|
||||
className={`flex-none
|
||||
${border && "ring ring-[1px] ring-border-strong "}
|
||||
${size === "large" ? "w-8 h-8" : "w-6 h-6"} `}
|
||||
${size === "large" ? "w-10 h-10" : "w-6 h-6"} `}
|
||||
>
|
||||
{createSVG(
|
||||
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
|
||||
@ -62,6 +63,6 @@ export function AssistantIcon({
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Tooltip>
|
||||
</CustomTooltip>
|
||||
);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Popover } from "./Popover";
|
||||
|
||||
export function DefaultPopover(props: {
|
||||
content: JSX.Element;
|
||||
children: JSX.Element[];
|
||||
children: (JSX.Element | null)[];
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "start" | "center" | "end";
|
||||
sideOffset?: number;
|
||||
@ -39,15 +39,17 @@ export function DefaultPopover(props: {
|
||||
overscroll-contain
|
||||
`}
|
||||
>
|
||||
{props.children.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="cursor-pointer text-left text-sm p-2 hover:bg-hover-light"
|
||||
onClick={() => setPopoverOpen(false)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
{props.children
|
||||
.filter((child) => child !== null)
|
||||
.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="cursor-pointer text-left text-sm p-2 hover:bg-hover-light"
|
||||
onClick={() => setPopoverOpen(false)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
{...props}
|
||||
|
@ -121,7 +121,7 @@ export const CustomTooltip = ({
|
||||
{isVisible &&
|
||||
createPortal(
|
||||
<div
|
||||
className={`fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
|
||||
className={`min-w-8 fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
|
||||
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
|
||||
}
|
||||
transform -translate-x-1/2 text-sm
|
||||
|
@ -14,6 +14,6 @@ export function checkUserIdOwnsAssistant(
|
||||
(!userId ||
|
||||
checkUserIsNoAuthUser(userId) ||
|
||||
assistant.owner?.id === userId) &&
|
||||
!assistant.default_persona
|
||||
!assistant.builtin_persona
|
||||
);
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "../types";
|
||||
|
||||
export function orderAssistantsForUser(
|
||||
assistants: Persona[],
|
||||
user: User | null
|
||||
) {
|
||||
if (user && user.preferences && user.preferences.chosen_assistants) {
|
||||
const chosenAssistantsSet = new Set(user.preferences.chosen_assistants);
|
||||
const assistantOrderMap = new Map(
|
||||
user.preferences.chosen_assistants.map((id: number, index: number) => [
|
||||
id,
|
||||
index,
|
||||
])
|
||||
);
|
||||
|
||||
let filteredAssistants = assistants.filter((assistant) =>
|
||||
chosenAssistantsSet.has(assistant.id)
|
||||
);
|
||||
|
||||
if (filteredAssistants.length == 0) {
|
||||
return assistants;
|
||||
}
|
||||
|
||||
filteredAssistants.sort((a, b) => {
|
||||
const orderA = assistantOrderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
const orderB = assistantOrderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
return orderA - orderB;
|
||||
});
|
||||
return filteredAssistants;
|
||||
}
|
||||
|
||||
return assistants;
|
||||
}
|
@ -11,24 +11,33 @@ export async function updateUserAssistantList(
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
export async function updateAssistantVisibility(
|
||||
assistantId: number,
|
||||
show: boolean
|
||||
): Promise<boolean> {
|
||||
const response = await fetch(
|
||||
`/api/user/assistant-list/update/${assistantId}?show=${show}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
export async function removeAssistantFromList(
|
||||
assistantId: number,
|
||||
chosenAssistants: number[]
|
||||
assistantId: number
|
||||
): Promise<boolean> {
|
||||
const updatedAssistants = chosenAssistants.filter((id) => id !== assistantId);
|
||||
return updateUserAssistantList(updatedAssistants);
|
||||
return updateAssistantVisibility(assistantId, false);
|
||||
}
|
||||
|
||||
export async function addAssistantToList(
|
||||
assistantId: number,
|
||||
chosenAssistants: number[]
|
||||
assistantId: number
|
||||
): Promise<boolean> {
|
||||
if (!chosenAssistants.includes(assistantId)) {
|
||||
const updatedAssistants = [...chosenAssistants, assistantId];
|
||||
return updateUserAssistantList(updatedAssistants);
|
||||
}
|
||||
return false;
|
||||
return updateAssistantVisibility(assistantId, true);
|
||||
}
|
||||
|
||||
export async function moveAssistantUp(
|
||||
|
120
web/src/lib/assistants/utils.ts
Normal file
120
web/src/lib/assistants/utils.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "../types";
|
||||
import { checkUserIsNoAuthUser } from "../user";
|
||||
|
||||
export function checkUserOwnsAssistant(user: User | null, assistant: Persona) {
|
||||
return checkUserIdOwnsAssistant(user?.id, assistant);
|
||||
}
|
||||
|
||||
export function checkUserIdOwnsAssistant(
|
||||
userId: string | undefined,
|
||||
assistant: Persona
|
||||
) {
|
||||
return (
|
||||
(!userId ||
|
||||
checkUserIsNoAuthUser(userId) ||
|
||||
assistant.owner?.id === userId) &&
|
||||
!assistant.is_default_persona
|
||||
);
|
||||
}
|
||||
|
||||
export function classifyAssistants(user: User | null, assistants: Persona[]) {
|
||||
if (!user) {
|
||||
return {
|
||||
visibleAssistants: assistants.filter(
|
||||
(assistant) => assistant.is_default_persona
|
||||
),
|
||||
hiddenAssistants: [],
|
||||
};
|
||||
}
|
||||
|
||||
const visibleAssistants = assistants.filter((assistant) => {
|
||||
const isVisible = user.preferences?.visible_assistants?.includes(
|
||||
assistant.id
|
||||
);
|
||||
const isNotHidden = !user.preferences?.hidden_assistants?.includes(
|
||||
assistant.id
|
||||
);
|
||||
const isSelected = user.preferences?.chosen_assistants?.includes(
|
||||
assistant.id
|
||||
);
|
||||
const isBuiltIn = assistant.builtin_persona;
|
||||
const isDefault = assistant.is_default_persona;
|
||||
|
||||
const isOwnedByUser = checkUserOwnsAssistant(user, assistant);
|
||||
|
||||
const isShown =
|
||||
(isVisible && isNotHidden && isSelected) ||
|
||||
(isNotHidden && (isBuiltIn || isDefault || isOwnedByUser));
|
||||
return isShown;
|
||||
});
|
||||
|
||||
const hiddenAssistants = assistants.filter((assistant) => {
|
||||
return !visibleAssistants.includes(assistant);
|
||||
});
|
||||
|
||||
return {
|
||||
visibleAssistants,
|
||||
hiddenAssistants,
|
||||
};
|
||||
}
|
||||
|
||||
export function orderAssistantsForUser(
|
||||
assistants: Persona[],
|
||||
user: User | null
|
||||
) {
|
||||
let orderedAssistants = [...assistants];
|
||||
|
||||
if (user?.preferences?.chosen_assistants) {
|
||||
const chosenAssistantsSet = new Set(user.preferences.chosen_assistants);
|
||||
const assistantOrderMap = new Map(
|
||||
user.preferences.chosen_assistants.map((id: number, index: number) => [
|
||||
id,
|
||||
index,
|
||||
])
|
||||
);
|
||||
|
||||
// Sort chosen assistants based on user preferences
|
||||
orderedAssistants.sort((a, b) => {
|
||||
const orderA = assistantOrderMap.get(a.id);
|
||||
const orderB = assistantOrderMap.get(b.id);
|
||||
|
||||
if (orderA !== undefined && orderB !== undefined) {
|
||||
return orderA - orderB;
|
||||
} else if (orderA !== undefined) {
|
||||
return -1;
|
||||
} else if (orderB !== undefined) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out assistants not in the user's chosen list
|
||||
orderedAssistants = orderedAssistants.filter((assistant) =>
|
||||
chosenAssistantsSet.has(assistant.id)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort remaining assistants based on display_priority
|
||||
const remainingAssistants = assistants.filter(
|
||||
(assistant) => !orderedAssistants.includes(assistant)
|
||||
);
|
||||
remainingAssistants.sort((a, b) => {
|
||||
const priorityA = a.display_priority ?? Number.MAX_SAFE_INTEGER;
|
||||
const priorityB = b.display_priority ?? Number.MAX_SAFE_INTEGER;
|
||||
return priorityA - priorityB;
|
||||
});
|
||||
|
||||
// Combine ordered chosen assistants with remaining assistants
|
||||
return [...orderedAssistants, ...remainingAssistants];
|
||||
}
|
||||
|
||||
export function getUserCreatedAssistants(
|
||||
user: User | null,
|
||||
assistants: Persona[]
|
||||
) {
|
||||
return assistants.filter((assistant) =>
|
||||
checkUserOwnsAssistant(user, assistant)
|
||||
);
|
||||
}
|
@ -32,10 +32,7 @@ export async function deleteCredential<T>(
|
||||
});
|
||||
}
|
||||
|
||||
export async function forceDeleteCredential<T>(
|
||||
credentialId: number,
|
||||
force?: boolean
|
||||
) {
|
||||
export async function forceDeleteCredential<T>(credentialId: number) {
|
||||
return await fetch(`/api/manage/credential/force/${credentialId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
|
@ -5,6 +5,8 @@ import { ConnectorCredentialPairStatus } from "@/app/admin/connector/[ccPairId]/
|
||||
|
||||
export interface UserPreferences {
|
||||
chosen_assistants: number[] | null;
|
||||
visible_assistants: number[];
|
||||
hidden_assistants: number[];
|
||||
default_model: string | null;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user