mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-01 00:18:18 +02:00
Add assistant notifications + update assistant context (#2816)
* add assistant notifications * nit * update context * validated * ensure context passed properly * validated + cleaned * nit: naming * k * k * final validation + new ui * nit + video * nit * nit * nit * k * fix typos
This commit is contained in:
parent
6913efef90
commit
8b220d2dba
@ -0,0 +1,26 @@
|
||||
"""add additional data to notifications
|
||||
|
||||
Revision ID: 1b10e1fda030
|
||||
Revises: 6756efa39ada
|
||||
Create Date: 2024-10-15 19:26:44.071259
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1b10e1fda030"
|
||||
down_revision = "6756efa39ada"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"notification", sa.Column("additional_data", postgresql.JSONB(), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("notification", "additional_data")
|
@ -135,6 +135,7 @@ DocumentSourceRequiringTenantContext: list[DocumentSource] = [DocumentSource.FIL
|
||||
|
||||
class NotificationType(str, Enum):
|
||||
REINDEX = "reindex"
|
||||
PERSONA_SHARED = "persona_shared"
|
||||
|
||||
|
||||
class BlobType(str, Enum):
|
||||
|
@ -235,6 +235,9 @@ class Notification(Base):
|
||||
first_shown: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
user: Mapped[User] = relationship("User", back_populates="notifications")
|
||||
additional_data: Mapped[dict | None] = mapped_column(
|
||||
postgresql.JSONB(), nullable=True
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
|
@ -1,3 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql import func
|
||||
@ -8,16 +10,37 @@ from danswer.db.models import User
|
||||
|
||||
|
||||
def create_notification(
|
||||
user: User | None,
|
||||
user_id: UUID | None,
|
||||
notif_type: NotificationType,
|
||||
db_session: Session,
|
||||
additional_data: dict | None = None,
|
||||
) -> Notification:
|
||||
# Check if an undismissed notification of the same type and data exists
|
||||
existing_notification = (
|
||||
db_session.query(Notification)
|
||||
.filter_by(
|
||||
user_id=user_id,
|
||||
notif_type=notif_type,
|
||||
dismissed=False,
|
||||
)
|
||||
.filter(Notification.additional_data == additional_data)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_notification:
|
||||
# Update the last_shown timestamp
|
||||
existing_notification.last_shown = func.now()
|
||||
db_session.commit()
|
||||
return existing_notification
|
||||
|
||||
# Create a new notification if none exists
|
||||
notification = Notification(
|
||||
user_id=user.id if user else None,
|
||||
user_id=user_id,
|
||||
notif_type=notif_type,
|
||||
dismissed=False,
|
||||
last_shown=func.now(),
|
||||
first_shown=func.now(),
|
||||
additional_data=additional_data,
|
||||
)
|
||||
db_session.add(notification)
|
||||
db_session.commit()
|
||||
|
@ -57,6 +57,7 @@ from danswer.server.features.input_prompt.api import (
|
||||
admin_router as admin_input_prompt_router,
|
||||
)
|
||||
from danswer.server.features.input_prompt.api import basic_router as input_prompt_router
|
||||
from danswer.server.features.notifications.api import router as notification_router
|
||||
from danswer.server.features.persona.api import admin_router as admin_persona_router
|
||||
from danswer.server.features.persona.api import basic_router as persona_router
|
||||
from danswer.server.features.prompt.api import basic_router as prompt_router
|
||||
@ -246,6 +247,7 @@ def get_application() -> FastAPI:
|
||||
include_router_with_global_prefix_prepended(application, admin_persona_router)
|
||||
include_router_with_global_prefix_prepended(application, input_prompt_router)
|
||||
include_router_with_global_prefix_prepended(application, admin_input_prompt_router)
|
||||
include_router_with_global_prefix_prepended(application, notification_router)
|
||||
include_router_with_global_prefix_prepended(application, prompt_router)
|
||||
include_router_with_global_prefix_prepended(application, tool_router)
|
||||
include_router_with_global_prefix_prepended(application, admin_tool_router)
|
||||
|
47
backend/danswer/server/features/notifications/api.py
Normal file
47
backend/danswer/server/features/notifications/api.py
Normal file
@ -0,0 +1,47 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.auth.users import current_user
|
||||
from danswer.db.engine import get_session
|
||||
from danswer.db.models import User
|
||||
from danswer.db.notification import dismiss_notification
|
||||
from danswer.db.notification import get_notification_by_id
|
||||
from danswer.db.notification import get_notifications
|
||||
from danswer.server.settings.models import Notification as NotificationModel
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
router = APIRouter(prefix="/notifications")
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_notifications_api(
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> list[NotificationModel]:
|
||||
notifications = [
|
||||
NotificationModel.from_model(notif)
|
||||
for notif in get_notifications(user, db_session, include_dismissed=False)
|
||||
]
|
||||
return notifications
|
||||
|
||||
|
||||
@router.post("/{notification_id}/dismiss")
|
||||
def dismiss_notification_endpoint(
|
||||
notification_id: int,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
try:
|
||||
notification = get_notification_by_id(notification_id, user, db_session)
|
||||
except PermissionError:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to dismiss this notification"
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
dismiss_notification(notification, db_session)
|
@ -13,8 +13,10 @@ from danswer.auth.users import current_admin_user
|
||||
from danswer.auth.users import current_curator_or_admin_user
|
||||
from danswer.auth.users import current_user
|
||||
from danswer.configs.constants import FileOrigin
|
||||
from danswer.configs.constants import NotificationType
|
||||
from danswer.db.engine import get_session
|
||||
from danswer.db.models import User
|
||||
from danswer.db.notification import create_notification
|
||||
from danswer.db.persona import create_update_persona
|
||||
from danswer.db.persona import get_persona_by_id
|
||||
from danswer.db.persona import get_personas
|
||||
@ -28,6 +30,7 @@ from danswer.file_store.file_store import get_default_file_store
|
||||
from danswer.file_store.models import ChatFileType
|
||||
from danswer.llm.answering.prompts.utils import build_dummy_prompt
|
||||
from danswer.server.features.persona.models import CreatePersonaRequest
|
||||
from danswer.server.features.persona.models import PersonaSharedNotificationData
|
||||
from danswer.server.features.persona.models import PersonaSnapshot
|
||||
from danswer.server.features.persona.models import PromptTemplateResponse
|
||||
from danswer.server.models import DisplayPriorityRequest
|
||||
@ -183,11 +186,12 @@ class PersonaShareRequest(BaseModel):
|
||||
user_ids: list[UUID]
|
||||
|
||||
|
||||
# We notify each user when a user is shared with them
|
||||
@basic_router.patch("/{persona_id}/share")
|
||||
def share_persona(
|
||||
persona_id: int,
|
||||
persona_share_request: PersonaShareRequest,
|
||||
user: User | None = Depends(current_user),
|
||||
user: User = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
update_persona_shared_users(
|
||||
@ -197,6 +201,18 @@ def share_persona(
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
for user_id in persona_share_request.user_ids:
|
||||
# Don't notify the user that they have access to their own persona
|
||||
if user_id != user.id:
|
||||
create_notification(
|
||||
user_id=user_id,
|
||||
notif_type=NotificationType.PERSONA_SHARED,
|
||||
db_session=db_session,
|
||||
additional_data=PersonaSharedNotificationData(
|
||||
persona_id=persona_id,
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
|
||||
@basic_router.delete("/{persona_id}")
|
||||
def delete_persona(
|
||||
@ -216,23 +232,31 @@ def list_personas(
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
include_deleted: bool = False,
|
||||
persona_ids: list[int] = Query(None),
|
||||
) -> list[PersonaSnapshot]:
|
||||
return [
|
||||
PersonaSnapshot.from_model(persona)
|
||||
for persona in get_personas(
|
||||
user=user,
|
||||
include_deleted=include_deleted,
|
||||
db_session=db_session,
|
||||
get_editable=False,
|
||||
joinedload_all=True,
|
||||
)
|
||||
# If the persona has an image generation tool and it's not available, don't include it
|
||||
personas = get_personas(
|
||||
user=user,
|
||||
include_deleted=include_deleted,
|
||||
db_session=db_session,
|
||||
get_editable=False,
|
||||
joinedload_all=True,
|
||||
)
|
||||
|
||||
if persona_ids:
|
||||
personas = [p for p in personas if p.id in persona_ids]
|
||||
|
||||
# Filter out personas with unavailable tools
|
||||
personas = [
|
||||
p
|
||||
for p in personas
|
||||
if not (
|
||||
any(tool.in_code_tool_id == "ImageGenerationTool" for tool in persona.tools)
|
||||
any(tool.in_code_tool_id == "ImageGenerationTool" for tool in p.tools)
|
||||
and not is_image_generation_available(db_session=db_session)
|
||||
)
|
||||
]
|
||||
|
||||
return [PersonaSnapshot.from_model(p) for p in personas]
|
||||
|
||||
|
||||
@basic_router.get("/{persona_id}")
|
||||
def get_persona(
|
||||
|
@ -120,3 +120,7 @@ class PersonaSnapshot(BaseModel):
|
||||
|
||||
class PromptTemplateResponse(BaseModel):
|
||||
final_prompt_template: str
|
||||
|
||||
|
||||
class PersonaSharedNotificationData(BaseModel):
|
||||
persona_id: int
|
||||
|
@ -15,8 +15,6 @@ from danswer.db.engine import get_session
|
||||
from danswer.db.models import User
|
||||
from danswer.db.notification import create_notification
|
||||
from danswer.db.notification import dismiss_all_notifications
|
||||
from danswer.db.notification import dismiss_notification
|
||||
from danswer.db.notification import get_notification_by_id
|
||||
from danswer.db.notification import get_notifications
|
||||
from danswer.db.notification import update_notification_last_shown
|
||||
from danswer.key_value_store.factory import get_kv_store
|
||||
@ -55,7 +53,7 @@ def fetch_settings(
|
||||
"""Settings and notifications are stuffed into this single endpoint to reduce number of
|
||||
Postgres calls"""
|
||||
general_settings = load_settings()
|
||||
user_notifications = get_user_notifications(user, db_session)
|
||||
user_notifications = get_reindex_notification(user, db_session)
|
||||
|
||||
try:
|
||||
kv_store = get_kv_store()
|
||||
@ -70,25 +68,7 @@ def fetch_settings(
|
||||
)
|
||||
|
||||
|
||||
@basic_router.post("/notifications/{notification_id}/dismiss")
|
||||
def dismiss_notification_endpoint(
|
||||
notification_id: int,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
try:
|
||||
notification = get_notification_by_id(notification_id, user, db_session)
|
||||
except PermissionError:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to dismiss this notification"
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
dismiss_notification(notification, db_session)
|
||||
|
||||
|
||||
def get_user_notifications(
|
||||
def get_reindex_notification(
|
||||
user: User | None, db_session: Session
|
||||
) -> list[Notification]:
|
||||
"""Get notifications for the user, currently the logic is very specific to the reindexing flag"""
|
||||
@ -121,7 +101,7 @@ def get_user_notifications(
|
||||
|
||||
if not reindex_notifs:
|
||||
notif = create_notification(
|
||||
user=user,
|
||||
user_id=user.id if user else None,
|
||||
notif_type=NotificationType.REINDEX,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
@ -24,6 +24,7 @@ class Notification(BaseModel):
|
||||
dismissed: bool
|
||||
last_shown: datetime
|
||||
first_shown: datetime
|
||||
additional_data: dict | None = None
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, notif: NotificationDBModel) -> "Notification":
|
||||
@ -33,6 +34,7 @@ class Notification(BaseModel):
|
||||
dismissed=notif.dismissed,
|
||||
last_shown=notif.last_shown,
|
||||
first_shown=notif.first_shown,
|
||||
additional_data=notif.additional_data,
|
||||
)
|
||||
|
||||
|
||||
|
@ -15,12 +15,20 @@ export interface Settings {
|
||||
product_gating: GatingType;
|
||||
}
|
||||
|
||||
export enum NotificationType {
|
||||
PERSONA_SHARED = "persona_shared",
|
||||
REINDEX_NEEDED = "reindex_needed",
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
notif_type: string;
|
||||
time_created: string;
|
||||
dismissed: boolean;
|
||||
last_shown: string;
|
||||
first_shown: string;
|
||||
additional_data?: {
|
||||
persona_id?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NavigationItem {
|
||||
|
@ -26,14 +26,9 @@ interface SidebarWrapperProps<T extends object> {
|
||||
folders?: Folder[];
|
||||
initiallyToggled: boolean;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
content: (props: T) => ReactNode;
|
||||
headerProps: {
|
||||
page: pageType;
|
||||
user: User | null;
|
||||
};
|
||||
contentProps: T;
|
||||
page: pageType;
|
||||
size?: "sm" | "lg";
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function SidebarWrapper<T extends object>({
|
||||
@ -42,10 +37,8 @@ export default function SidebarWrapper<T extends object>({
|
||||
folders,
|
||||
openedFolders,
|
||||
page,
|
||||
headerProps,
|
||||
contentProps,
|
||||
content,
|
||||
size = "sm",
|
||||
children,
|
||||
}: SidebarWrapperProps<T>) {
|
||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||
const [showDocSidebar, setShowDocSidebar] = useState(false); // State to track if sidebar is open
|
||||
@ -144,7 +137,6 @@ export default function SidebarWrapper<T extends object>({
|
||||
sidebarToggled={toggledSidebar}
|
||||
toggleSidebar={toggleSidebar}
|
||||
page="assistants"
|
||||
user={headerProps.user}
|
||||
/>
|
||||
<div className="w-full flex">
|
||||
<div
|
||||
@ -163,7 +155,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
<div
|
||||
className={`mt-4 w-full ${size == "lg" ? "max-w-4xl" : "max-w-3xl"} mx-auto`}
|
||||
>
|
||||
{content(contentProps)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,6 +15,8 @@ import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AssistantTools } from "../ToolsDisplay";
|
||||
import { classifyAssistants } from "@/lib/assistants/utils";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
export function AssistantGalleryCard({
|
||||
assistant,
|
||||
user,
|
||||
@ -26,6 +28,7 @@ export function AssistantGalleryCard({
|
||||
setPopup: (popup: PopupSpec) => void;
|
||||
selectedAssistant: boolean;
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div
|
||||
@ -80,7 +83,7 @@ export function AssistantGalleryCard({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
@ -108,7 +111,7 @@ export function AssistantGalleryCard({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
@ -136,14 +139,10 @@ export function AssistantGalleryCard({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function AssistantsGallery({
|
||||
assistants,
|
||||
user,
|
||||
}: {
|
||||
assistants: Persona[];
|
||||
export function AssistantsGallery() {
|
||||
const { assistants } = useAssistants();
|
||||
const { user } = useUser();
|
||||
|
||||
user: User | null;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
@ -12,15 +12,11 @@ export default function WrappedAssistantsGallery({
|
||||
initiallyToggled,
|
||||
folders,
|
||||
openedFolders,
|
||||
user,
|
||||
assistants,
|
||||
}: {
|
||||
chatSessions: ChatSession[];
|
||||
folders: Folder[];
|
||||
initiallyToggled: boolean;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper
|
||||
@ -29,17 +25,8 @@ export default function WrappedAssistantsGallery({
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
headerProps={{ user, page: "chat" }}
|
||||
contentProps={{
|
||||
assistants: assistants,
|
||||
user: user,
|
||||
}}
|
||||
content={(contentProps) => (
|
||||
<AssistantsGallery
|
||||
assistants={contentProps.assistants}
|
||||
user={contentProps.user}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<AssistantsGallery />
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import WrappedAssistantsGallery from "./WrappedAssistantsGallery";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
|
||||
export default async function GalleryPage({
|
||||
searchParams,
|
||||
@ -26,22 +27,28 @@ export default async function GalleryPage({
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
toggleSidebar,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
<AssistantsProvider
|
||||
initialAssistants={assistants}
|
||||
hasAnyConnectors={hasAnyConnectors}
|
||||
hasImageCompatibleModel={hasImageCompatibleModel}
|
||||
>
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
|
||||
<InstantSSRAutoRefresh />
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<WrappedAssistantsGallery
|
||||
initiallyToggled={toggleSidebar}
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
user={user}
|
||||
assistants={assistants}
|
||||
/>
|
||||
<WrappedAssistantsGallery
|
||||
initiallyToggled={toggleSidebar}
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
/>
|
||||
</AssistantsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { Bubble } from "@/components/Bubble";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
|
||||
interface AssistantSharingModalProps {
|
||||
assistant: Persona;
|
||||
@ -32,7 +33,7 @@ export function AssistantSharingModal({
|
||||
show,
|
||||
onClose,
|
||||
}: AssistantSharingModalProps) {
|
||||
const router = useRouter();
|
||||
const { refreshAssistants } = useAssistants();
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedUsers, setSelectedUsers] = useState<MinimalUserSnapshot[]>([]);
|
||||
@ -54,7 +55,7 @@ export function AssistantSharingModal({
|
||||
assistant,
|
||||
selectedUsers.map((user) => user.id)
|
||||
);
|
||||
router.refresh();
|
||||
await refreshAssistants();
|
||||
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const remainingTime = Math.max(0, 1000 - elapsedTime);
|
||||
@ -96,7 +97,7 @@ export function AssistantSharingModal({
|
||||
assistant,
|
||||
[u.id]
|
||||
);
|
||||
router.refresh();
|
||||
await refreshAssistants();
|
||||
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const remainingTime = Math.max(0, 1000 - elapsedTime);
|
||||
@ -138,7 +139,6 @@ export function AssistantSharingModal({
|
||||
onOutsideClick={onClose}
|
||||
>
|
||||
<div>
|
||||
{isUpdating && <Spinner />}
|
||||
<p className="text-text-600 text-lg mb-6">
|
||||
Manage access to this assistant by sharing it with other users.
|
||||
</p>
|
||||
@ -225,6 +225,7 @@ export function AssistantSharingModal({
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
{isUpdating && <Spinner />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -55,12 +55,9 @@ 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";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
function DraggableAssistantListItem(props: any) {
|
||||
const {
|
||||
@ -112,6 +109,7 @@ function AssistantListItem({
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
isDragging?: boolean;
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const router = useRouter();
|
||||
const [showSharingModal, setShowSharingModal] = useState(false);
|
||||
|
||||
@ -206,7 +204,7 @@ function AssistantListItem({
|
||||
message: `"${assistant.name}" has been removed from your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be removed from your list.`,
|
||||
@ -229,7 +227,7 @@ function AssistantListItem({
|
||||
message: `"${assistant.name}" has been added to your list.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${assistant.name}" could not be added to your list.`,
|
||||
@ -284,32 +282,20 @@ function AssistantListItem({
|
||||
</>
|
||||
);
|
||||
}
|
||||
export function AssistantsList({
|
||||
user,
|
||||
assistants,
|
||||
}: {
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
// Define the distinct groups of assistants
|
||||
const { visibleAssistants, hiddenAssistants } = classifyAssistants(
|
||||
user,
|
||||
assistants
|
||||
);
|
||||
export function AssistantsList() {
|
||||
const {
|
||||
assistants,
|
||||
ownedButHiddenAssistants,
|
||||
finalAssistants,
|
||||
refreshAssistants,
|
||||
} = useAssistants();
|
||||
|
||||
const [currentlyVisibleAssistants, setCurrentlyVisibleAssistants] = useState<
|
||||
Persona[]
|
||||
>([]);
|
||||
const [currentlyVisibleAssistants, setCurrentlyVisibleAssistants] =
|
||||
useState(finalAssistants);
|
||||
|
||||
useEffect(() => {
|
||||
const orderedAssistants = orderAssistantsForUser(visibleAssistants, user);
|
||||
setCurrentlyVisibleAssistants(orderedAssistants);
|
||||
}, [assistants, user]);
|
||||
|
||||
const ownedButHiddenAssistants = getUserCreatedAssistants(
|
||||
user,
|
||||
hiddenAssistants
|
||||
);
|
||||
setCurrentlyVisibleAssistants(finalAssistants);
|
||||
}, [finalAssistants]);
|
||||
|
||||
const allAssistantIds = assistants.map((assistant) =>
|
||||
assistant.id.toString()
|
||||
@ -320,6 +306,8 @@ export function AssistantsList({
|
||||
null
|
||||
);
|
||||
|
||||
const { refreshUser, user } = useUser();
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
const { data: users } = useSWR<MinimalUserSnapshot[]>(
|
||||
@ -338,18 +326,22 @@ export function AssistantsList({
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
setCurrentlyVisibleAssistants((assistants) => {
|
||||
const oldIndex = assistants.findIndex(
|
||||
(a) => a.id.toString() === active.id
|
||||
);
|
||||
const newIndex = assistants.findIndex(
|
||||
(a) => a.id.toString() === over.id
|
||||
);
|
||||
const newAssistants = arrayMove(assistants, oldIndex, newIndex);
|
||||
const oldIndex = currentlyVisibleAssistants.findIndex(
|
||||
(item) => item.id.toString() === active.id
|
||||
);
|
||||
const newIndex = currentlyVisibleAssistants.findIndex(
|
||||
(item) => item.id.toString() === over.id
|
||||
);
|
||||
const updatedAssistants = arrayMove(
|
||||
currentlyVisibleAssistants,
|
||||
oldIndex,
|
||||
newIndex
|
||||
);
|
||||
|
||||
updateUserAssistantList(newAssistants.map((a) => a.id));
|
||||
return newAssistants;
|
||||
});
|
||||
setCurrentlyVisibleAssistants(updatedAssistants);
|
||||
await updateUserAssistantList(updatedAssistants.map((a) => a.id));
|
||||
await refreshUser();
|
||||
await refreshAssistants();
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,7 +360,7 @@ export function AssistantsList({
|
||||
message: `"${deletingPersona.name}" has been deleted.`,
|
||||
type: "success",
|
||||
});
|
||||
router.refresh();
|
||||
await refreshUser();
|
||||
} else {
|
||||
setPopup({
|
||||
message: `"${deletingPersona.name}" could not be deleted.`,
|
||||
@ -389,7 +381,7 @@ export function AssistantsList({
|
||||
makePublicPersona.id,
|
||||
newPublicStatus
|
||||
);
|
||||
router.refresh();
|
||||
await refreshAssistants();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -3,23 +3,17 @@ import { AssistantsList } from "./AssistantsList";
|
||||
import SidebarWrapper from "../SidebarWrapper";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
export default function WrappedAssistantsMine({
|
||||
chatSessions,
|
||||
initiallyToggled,
|
||||
folders,
|
||||
openedFolders,
|
||||
user,
|
||||
assistants,
|
||||
}: {
|
||||
chatSessions: ChatSession[];
|
||||
folders: Folder[];
|
||||
initiallyToggled: boolean;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarWrapper
|
||||
@ -28,17 +22,8 @@ export default function WrappedAssistantsMine({
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
headerProps={{ user, page: "chat" }}
|
||||
contentProps={{
|
||||
assistants: assistants,
|
||||
user: user,
|
||||
}}
|
||||
content={(contentProps) => (
|
||||
<AssistantsList
|
||||
assistants={contentProps.assistants}
|
||||
user={contentProps.user}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<AssistantsList />
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
import SidebarWrapper from "../SidebarWrapper";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
import { AssistantsPageTitle } from "../AssistantsPageTitle";
|
||||
@ -14,15 +13,11 @@ export default function WrappedPrompts({
|
||||
initiallyToggled,
|
||||
folders,
|
||||
openedFolders,
|
||||
user,
|
||||
assistants,
|
||||
}: {
|
||||
chatSessions: ChatSession[];
|
||||
folders: Folder[];
|
||||
initiallyToggled: boolean;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
const {
|
||||
data: promptLibrary,
|
||||
@ -39,24 +34,18 @@ export default function WrappedPrompts({
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
headerProps={{ user, page: "chat" }}
|
||||
contentProps={{
|
||||
assistants: assistants,
|
||||
user: user,
|
||||
}}
|
||||
content={(contentProps) => (
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Prompt Gallery</AssistantsPageTitle>
|
||||
<PromptSection
|
||||
promptLibrary={promptLibrary || []}
|
||||
isLoading={promptLibraryIsLoading}
|
||||
error={promptLibraryError}
|
||||
refreshPrompts={refreshPrompts}
|
||||
isPublic={false}
|
||||
centering
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Prompt Gallery</AssistantsPageTitle>
|
||||
<PromptSection
|
||||
promptLibrary={promptLibrary || []}
|
||||
isLoading={promptLibraryIsLoading}
|
||||
error={promptLibraryError}
|
||||
refreshPrompts={refreshPrompts}
|
||||
isPublic={false}
|
||||
centering
|
||||
/>
|
||||
</div>
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import WrappedAssistantsMine from "./WrappedAssistantsMine";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
|
||||
export default async function GalleryPage({
|
||||
searchParams,
|
||||
@ -21,27 +22,30 @@ export default async function GalleryPage({
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
assistants,
|
||||
folders,
|
||||
assistants,
|
||||
openedFolders,
|
||||
shouldShowWelcomeModal,
|
||||
toggleSidebar,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AssistantsProvider
|
||||
initialAssistants={assistants}
|
||||
hasAnyConnectors={hasAnyConnectors}
|
||||
hasImageCompatibleModel={hasImageCompatibleModel}
|
||||
>
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<WrappedAssistantsMine
|
||||
initiallyToggled={toggleSidebar}
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
user={user}
|
||||
assistants={assistants}
|
||||
/>
|
||||
</>
|
||||
</AssistantsProvider>
|
||||
);
|
||||
}
|
||||
|
@ -101,12 +101,9 @@ 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";
|
||||
import BlurBackground from "./shared_chat_search/BlurBackground";
|
||||
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@ -128,7 +125,6 @@ export function ChatPage({
|
||||
chatSessions,
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
availableAssistants,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
@ -138,9 +134,11 @@ export function ChatPage({
|
||||
refreshChatSessions,
|
||||
} = useChatContext();
|
||||
|
||||
const { assistants: availableAssistants, finalAssistants } = useAssistants();
|
||||
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(true);
|
||||
|
||||
const { user, refreshUser, isAdmin, isLoadingUser } = useUser();
|
||||
const { user, isAdmin, isLoadingUser } = useUser();
|
||||
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
||||
@ -157,18 +155,6 @@ export function ChatPage({
|
||||
// Useful for determining which session has been loaded (i.e. still on `new, empty session` or `previous session`)
|
||||
const loadedIdSessionRef = useRef<string | null>(existingChatSessionId);
|
||||
|
||||
// Assistants in order
|
||||
const { finalAssistants } = useMemo(() => {
|
||||
const { visibleAssistants, hiddenAssistants: _ } = classifyAssistants(
|
||||
user,
|
||||
availableAssistants
|
||||
);
|
||||
const finalAssistants = user
|
||||
? orderAssistantsForUser(visibleAssistants, user)
|
||||
: visibleAssistants;
|
||||
return { finalAssistants };
|
||||
}, [user, availableAssistants]);
|
||||
|
||||
const existingChatSessionAssistantId = selectedChatSession?.persona_id;
|
||||
const [selectedAssistant, setSelectedAssistant] = useState<
|
||||
Persona | undefined
|
||||
@ -1833,7 +1819,6 @@ export function ChatPage({
|
||||
setPopup={setPopup}
|
||||
setLlmOverride={llmOverrideManager.setGlobalDefault}
|
||||
defaultModel={user?.preferences.default_model!}
|
||||
refreshUser={refreshUser}
|
||||
llmProviders={llmProviders}
|
||||
onClose={() => setSettingsToggled(false)}
|
||||
/>
|
||||
@ -1953,7 +1938,6 @@ export function ChatPage({
|
||||
: undefined
|
||||
}
|
||||
toggleSidebar={toggleSidebar}
|
||||
user={user}
|
||||
currentChatSession={selectedChatSession}
|
||||
/>
|
||||
)}
|
||||
@ -2438,7 +2422,6 @@ export function ChatPage({
|
||||
showDocs={() => setDocumentSelection(true)}
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
assistantOptions={finalAssistants}
|
||||
selectedAssistant={liveAssistant}
|
||||
setSelectedAssistant={onAssistantChange}
|
||||
setAlternativeAssistant={setAlternativeAssistant}
|
||||
@ -2454,7 +2437,6 @@ export function ChatPage({
|
||||
handleFileUpload={handleImageUpload}
|
||||
textAreaRef={textAreaRef}
|
||||
chatSessionId={chatSessionIdRef.current!}
|
||||
refreshUser={refreshUser}
|
||||
/>
|
||||
|
||||
{enterpriseSettings &&
|
||||
|
@ -34,6 +34,7 @@ import { Hoverable } from "@/components/Hoverable";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { ChatState } from "../types";
|
||||
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
@ -52,7 +53,6 @@ export function ChatInputBar({
|
||||
|
||||
// assistants
|
||||
selectedAssistant,
|
||||
assistantOptions,
|
||||
setSelectedAssistant,
|
||||
setAlternativeAssistant,
|
||||
|
||||
@ -63,7 +63,6 @@ export function ChatInputBar({
|
||||
alternativeAssistant,
|
||||
chatSessionId,
|
||||
inputPrompts,
|
||||
refreshUser,
|
||||
}: {
|
||||
showConfigureAPIKey: () => void;
|
||||
openModelSettings: () => void;
|
||||
@ -71,7 +70,6 @@ export function ChatInputBar({
|
||||
stopGenerating: () => void;
|
||||
showDocs: () => void;
|
||||
selectedDocuments: DanswerDocument[];
|
||||
assistantOptions: Persona[];
|
||||
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
|
||||
setSelectedAssistant: (assistant: Persona) => void;
|
||||
inputPrompts: InputPrompt[];
|
||||
@ -87,7 +85,6 @@ export function ChatInputBar({
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
chatSessionId?: string;
|
||||
refreshUser: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
@ -118,6 +115,7 @@ export function ChatInputBar({
|
||||
};
|
||||
|
||||
const settings = useContext(SettingsContext);
|
||||
const { finalAssistants: assistantOptions } = useAssistants();
|
||||
|
||||
const { llmProviders } = useChatContext();
|
||||
const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null);
|
||||
@ -527,14 +525,12 @@ export function ChatInputBar({
|
||||
removePadding
|
||||
content={(close) => (
|
||||
<AssistantsTab
|
||||
availableAssistants={assistantOptions}
|
||||
llmProviders={llmProviders}
|
||||
selectedAssistant={selectedAssistant}
|
||||
onSelect={(assistant) => {
|
||||
setSelectedAssistant(assistant);
|
||||
close();
|
||||
}}
|
||||
refreshUser={refreshUser}
|
||||
/>
|
||||
)}
|
||||
flexPriority="shrink"
|
||||
|
@ -8,6 +8,7 @@ import { destructureValue, structureValue } from "@/lib/llm/utils";
|
||||
import { setUserDefaultModel } from "@/lib/users/UserSettings";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
export function SetDefaultModelModal({
|
||||
setPopup,
|
||||
@ -15,15 +16,14 @@ export function SetDefaultModelModal({
|
||||
onClose,
|
||||
setLlmOverride,
|
||||
defaultModel,
|
||||
refreshUser,
|
||||
}: {
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
setLlmOverride: Dispatch<SetStateAction<LlmOverride>>;
|
||||
onClose: () => void;
|
||||
defaultModel: string | null;
|
||||
refreshUser: () => void;
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const messageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -16,25 +16,29 @@ import {
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { getFinalLLM } from "@/lib/llm/utils";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
export function AssistantsTab({
|
||||
selectedAssistant,
|
||||
availableAssistants,
|
||||
llmProviders,
|
||||
onSelect,
|
||||
refreshUser,
|
||||
}: {
|
||||
selectedAssistant: Persona;
|
||||
availableAssistants: Persona[];
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
onSelect: (assistant: Persona) => void;
|
||||
refreshUser: () => void;
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const [_, llmName] = getFinalLLM(llmProviders, null, null);
|
||||
const [assistants, setAssistants] = useState(availableAssistants);
|
||||
const { finalAssistants, refreshAssistants } = useAssistants();
|
||||
const [assistants, setAssistants] = useState(finalAssistants);
|
||||
|
||||
useEffect(() => {
|
||||
setAssistants(finalAssistants);
|
||||
}, [finalAssistants]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
@ -57,7 +61,8 @@ export function AssistantsTab({
|
||||
|
||||
setAssistants(updatedAssistants);
|
||||
await updateUserAssistantList(updatedAssistants.map((a) => a.id));
|
||||
refreshUser();
|
||||
await refreshUser();
|
||||
await refreshAssistants();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrap
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import WrappedChat from "./WrappedChat";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
@ -24,7 +25,6 @@ export default async function Page({
|
||||
chatSessions,
|
||||
availableSources,
|
||||
documentSets,
|
||||
assistants,
|
||||
tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
@ -32,31 +32,38 @@ export default async function Page({
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
assistants,
|
||||
userInputPrompts,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
availableDocumentSets: documentSets,
|
||||
availableAssistants: assistants,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
userInputPrompts,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
<AssistantsProvider
|
||||
initialAssistants={assistants}
|
||||
hasAnyConnectors={hasAnyConnectors}
|
||||
hasImageCompatibleModel={hasImageCompatibleModel}
|
||||
>
|
||||
<WrappedChat initiallyToggled={toggleSidebar} />
|
||||
</ChatProvider>
|
||||
<ChatProvider
|
||||
value={{
|
||||
chatSessions,
|
||||
availableSources,
|
||||
availableDocumentSets: documentSets,
|
||||
availableTags: tags,
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
userInputPrompts,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
>
|
||||
<WrappedChat initiallyToggled={toggleSidebar} />
|
||||
</ChatProvider>
|
||||
</AssistantsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="absolute top-0 z-40 w-full">
|
||||
<FunctionalHeader page="shared" user={user} />
|
||||
<FunctionalHeader page="shared" />
|
||||
</div>
|
||||
|
||||
<div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen">
|
||||
|
@ -16,14 +16,7 @@ export default async function GalleryPage({
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const {
|
||||
user,
|
||||
chatSessions,
|
||||
assistants,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
} = data;
|
||||
const { chatSessions, folders, openedFolders, toggleSidebar } = data;
|
||||
|
||||
return (
|
||||
<WrappedPrompts
|
||||
@ -31,8 +24,6 @@ export default async function GalleryPage({
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
user={user}
|
||||
assistants={assistants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import WrappedSearch from "./WrappedSearch";
|
||||
import { SearchProvider } from "@/components/context/SearchContext";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "../admin/configuration/llm/interfaces";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export default async function Home({
|
||||
@ -193,36 +194,39 @@ export default async function Home({
|
||||
<HealthCheckBanner />
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
{shouldDisplayNoSourcesModal && <NoSourcesModal />}
|
||||
|
||||
{shouldDisplaySourcesIncompleteModal && (
|
||||
<NoCompleteSourcesModal ccPairs={ccPairs} />
|
||||
)}
|
||||
|
||||
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
|
||||
Only used in the EE version of the app. */}
|
||||
<ChatPopup />
|
||||
|
||||
<SearchProvider
|
||||
value={{
|
||||
querySessions,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
assistants,
|
||||
tags,
|
||||
agenticSearchEnabled,
|
||||
disabledAgentic: DISABLE_LLM_DOC_RELEVANCE,
|
||||
initiallyToggled: toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
shouldDisplayNoSources: shouldDisplayNoSourcesModal,
|
||||
}}
|
||||
<AssistantsProvider
|
||||
initialAssistants={assistants}
|
||||
hasAnyConnectors={hasAnyConnectors}
|
||||
hasImageCompatibleModel={false}
|
||||
>
|
||||
<WrappedSearch
|
||||
initiallyToggled={toggleSidebar}
|
||||
searchTypeDefault={searchTypeDefault}
|
||||
/>
|
||||
</SearchProvider>
|
||||
<SearchProvider
|
||||
value={{
|
||||
querySessions,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
assistants,
|
||||
tags,
|
||||
agenticSearchEnabled,
|
||||
disabledAgentic: DISABLE_LLM_DOC_RELEVANCE,
|
||||
initiallyToggled: toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
shouldDisplayNoSources: shouldDisplayNoSourcesModal,
|
||||
}}
|
||||
>
|
||||
<WrappedSearch
|
||||
initiallyToggled={toggleSidebar}
|
||||
searchTypeDefault={searchTypeDefault}
|
||||
/>
|
||||
</SearchProvider>
|
||||
</AssistantsProvider>
|
||||
s
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,19 +4,20 @@ import { useState, useRef, useContext, useEffect, useMemo } from "react";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { User, UserRole } from "@/lib/types";
|
||||
import { UserRole } from "@/lib/types";
|
||||
import { checkUserIsNoAuthUser, logout } from "@/lib/user";
|
||||
import { Popover } from "./popover/Popover";
|
||||
import { LOGOUT_DISABLED } from "@/lib/constants";
|
||||
import { SettingsContext } from "./settings/SettingsProvider";
|
||||
import {
|
||||
AssistantsIconSkeleton,
|
||||
LightSettingsIcon,
|
||||
UsersIcon,
|
||||
} from "./icons/icons";
|
||||
import { BellIcon, LightSettingsIcon } from "./icons/icons";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { NavigationItem } from "@/app/admin/settings/interfaces";
|
||||
import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
|
||||
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
|
||||
import { useUser } from "./user/UserProvider";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "./settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { Notifications } from "./chat_search/Notifications";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
interface DropdownOptionProps {
|
||||
href?: string;
|
||||
@ -55,24 +56,25 @@ const DropdownOption: React.FC<DropdownOptionProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
export function UserDropdown({
|
||||
user,
|
||||
page,
|
||||
}: {
|
||||
user: User | null;
|
||||
page?: pageType;
|
||||
}) {
|
||||
export function UserDropdown({ page }: { page?: pageType }) {
|
||||
const { user } = useUser();
|
||||
const [userInfoVisible, setUserInfoVisible] = useState(false);
|
||||
const userInfoRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
const customNavItems: NavigationItem[] = useMemo(
|
||||
() => combinedSettings?.enterpriseSettings?.custom_nav_items || [],
|
||||
[combinedSettings]
|
||||
);
|
||||
const {
|
||||
data: notifications,
|
||||
error,
|
||||
mutate: refreshNotifications,
|
||||
} = useSWR<Notification[]>("/api/notifications", errorHandlingFetcher);
|
||||
|
||||
useEffect(() => {
|
||||
const iconNames = customNavItems
|
||||
@ -110,15 +112,20 @@ export function UserDropdown({
|
||||
const showLogout =
|
||||
user && !checkUserIsNoAuthUser(user.id) && !LOGOUT_DISABLED;
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
setUserInfoVisible(open);
|
||||
setShowNotifications(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative" ref={userInfoRef}>
|
||||
<Popover
|
||||
open={userInfoVisible}
|
||||
onOpenChange={setUserInfoVisible}
|
||||
onOpenChange={onOpenChange}
|
||||
content={
|
||||
<div
|
||||
onClick={() => setUserInfoVisible(!userInfoVisible)}
|
||||
className="flex cursor-pointer"
|
||||
className="flex relative cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
@ -138,6 +145,9 @@ export function UserDropdown({
|
||||
>
|
||||
{user && user.email ? user.email[0].toUpperCase() : "A"}
|
||||
</div>
|
||||
{notifications && notifications.length > 0 && (
|
||||
<div className="absolute right-0 top-0 w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
popover={
|
||||
@ -161,14 +171,22 @@ export function UserDropdown({
|
||||
overscroll-contain
|
||||
`}
|
||||
>
|
||||
{customNavItems.map((item, i) => (
|
||||
<DropdownOption
|
||||
key={i}
|
||||
href={item.link}
|
||||
icon={
|
||||
item.svg_logo ? (
|
||||
<div
|
||||
className="
|
||||
{page != "admin" && showNotifications ? (
|
||||
<Notifications
|
||||
navigateToDropdown={() => setShowNotifications(false)}
|
||||
notifications={notifications || []}
|
||||
refreshNotifications={refreshNotifications}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{customNavItems.map((item, i) => (
|
||||
<DropdownOption
|
||||
key={i}
|
||||
href={item.link}
|
||||
icon={
|
||||
item.svg_logo ? (
|
||||
<div
|
||||
className="
|
||||
h-4
|
||||
w-4
|
||||
my-auto
|
||||
@ -178,57 +196,72 @@ export function UserDropdown({
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
aria-label={item.title}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
dangerouslySetInnerHTML={{ __html: item.svg_logo }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DynamicFaIcon
|
||||
name={item.icon!}
|
||||
className="h-4 w-4 my-auto mr-2"
|
||||
aria-label={item.title}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
dangerouslySetInnerHTML={{ __html: item.svg_logo }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DynamicFaIcon
|
||||
name={item.icon!}
|
||||
className="h-4 w-4 my-auto mr-2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={item.title}
|
||||
openInNewTab
|
||||
/>
|
||||
))}
|
||||
|
||||
{showAdminPanel ? (
|
||||
<DropdownOption
|
||||
href="/admin/indexing/status"
|
||||
icon={
|
||||
<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />
|
||||
}
|
||||
label="Admin Panel"
|
||||
/>
|
||||
) : (
|
||||
showCuratorPanel && (
|
||||
<DropdownOption
|
||||
href="/admin/indexing/status"
|
||||
icon={
|
||||
<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />
|
||||
}
|
||||
label="Curator Panel"
|
||||
/>
|
||||
)
|
||||
}
|
||||
label={item.title}
|
||||
openInNewTab
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
|
||||
{showAdminPanel ? (
|
||||
<DropdownOption
|
||||
href="/admin/indexing/status"
|
||||
icon={<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />}
|
||||
label="Admin Panel"
|
||||
/>
|
||||
) : (
|
||||
showCuratorPanel && (
|
||||
<DropdownOption
|
||||
href="/admin/indexing/status"
|
||||
icon={<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />}
|
||||
label="Curator Panel"
|
||||
onClick={() => {
|
||||
setUserInfoVisible(true);
|
||||
setShowNotifications(true);
|
||||
}}
|
||||
icon={<BellIcon className="h-5 w-5 my-auto mr-2" />}
|
||||
label={`Notifications ${notifications && notifications.length > 0 ? `(${notifications.length})` : ""}`}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{showLogout &&
|
||||
(showCuratorPanel ||
|
||||
showAdminPanel ||
|
||||
customNavItems.length > 0) && (
|
||||
<div className="border-t border-border my-1" />
|
||||
)}
|
||||
{showLogout &&
|
||||
(showCuratorPanel ||
|
||||
showAdminPanel ||
|
||||
customNavItems.length > 0) && (
|
||||
<div className="border-t border-border my-1" />
|
||||
)}
|
||||
|
||||
{showLogout && (
|
||||
<DropdownOption
|
||||
onClick={handleLogout}
|
||||
icon={<FiLogOut className="my-auto mr-2 text-lg" />}
|
||||
label="Log out"
|
||||
/>
|
||||
{showLogout && (
|
||||
<DropdownOption
|
||||
onClick={handleLogout}
|
||||
icon={<FiLogOut className="my-auto mr-2 text-lg" />}
|
||||
label="Log out"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
@ -418,7 +418,7 @@ export function ClientLayout({
|
||||
</div>
|
||||
<div className="pb-8 relative h-full overflow-y-auto w-full">
|
||||
<div className="fixed bg-background left-0 gap-x-4 mb-8 px-4 py-2 w-full items-center flex justify-end">
|
||||
<UserDropdown user={user} />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
<div className="pt-20 flex overflow-y-auto h-full px-4 md:px-12">
|
||||
{children}
|
||||
|
@ -11,9 +11,9 @@ import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChatBanner } from "@/app/chat/ChatBanner";
|
||||
import LogoType from "../header/LogoType";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
|
||||
export default function FunctionalHeader({
|
||||
user,
|
||||
page,
|
||||
currentChatSession,
|
||||
setSharingModalVisible,
|
||||
@ -23,7 +23,6 @@ export default function FunctionalHeader({
|
||||
}: {
|
||||
reset?: () => void;
|
||||
page: pageType;
|
||||
user: User | null;
|
||||
sidebarToggled?: boolean;
|
||||
currentChatSession?: ChatSession | null | undefined;
|
||||
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
||||
@ -109,7 +108,7 @@ export default function FunctionalHeader({
|
||||
)}
|
||||
|
||||
<div className="mobile:hidden flex my-auto">
|
||||
<UserDropdown user={user} />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
<Link
|
||||
className="desktop:hidden my-auto"
|
||||
|
231
web/src/components/chat_search/Notifications.tsx
Normal file
231
web/src/components/chat_search/Notifications.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import {
|
||||
Notification,
|
||||
NotificationType,
|
||||
} from "@/app/admin/settings/interfaces";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { useAssistants } from "../context/AssistantsContext";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
import { XIcon } from "../icons/icons";
|
||||
import { Spinner } from "@phosphor-icons/react";
|
||||
|
||||
export const Notifications = ({
|
||||
notifications,
|
||||
refreshNotifications,
|
||||
navigateToDropdown,
|
||||
}: {
|
||||
notifications: Notification[];
|
||||
refreshNotifications: () => void;
|
||||
navigateToDropdown: () => void;
|
||||
}) => {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
const { refreshAssistants } = useAssistants();
|
||||
|
||||
const { refreshUser } = useUser();
|
||||
const [personas, setPersonas] = useState<Record<number, Persona> | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPersonas = async () => {
|
||||
if (notifications) {
|
||||
const personaIds = notifications
|
||||
.filter(
|
||||
(n) =>
|
||||
n.notif_type.toLowerCase() === "persona_shared" &&
|
||||
n.additional_data?.persona_id !== undefined
|
||||
)
|
||||
.map((n) => n.additional_data!.persona_id!);
|
||||
|
||||
if (personaIds.length > 0) {
|
||||
const queryParams = personaIds
|
||||
.map((id) => `persona_ids=${id}`)
|
||||
.join("&");
|
||||
try {
|
||||
const response = await fetch(`/api/persona?${queryParams}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Error fetching personas: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
const personasData: Persona[] = await response.json();
|
||||
setPersonas(
|
||||
personasData.reduce(
|
||||
(acc, persona) => {
|
||||
acc[persona.id] = persona;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, Persona>
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch personas:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchPersonas();
|
||||
}, [notifications]);
|
||||
|
||||
const dismissNotification = async (notificationId: number) => {
|
||||
try {
|
||||
await fetch(`/api/notifications/${notificationId}/dismiss`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
refreshNotifications();
|
||||
} catch (error) {
|
||||
console.error("Error dismissing notification:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssistantShareAcceptance = async (
|
||||
notification: Notification,
|
||||
persona: Persona
|
||||
) => {
|
||||
addAssistantToList(persona.id);
|
||||
await dismissNotification(notification.id);
|
||||
await refreshUser();
|
||||
await refreshAssistants();
|
||||
};
|
||||
|
||||
const sortedNotifications = notifications
|
||||
? notifications
|
||||
.filter((notification) => {
|
||||
const personaId = notification.additional_data?.persona_id;
|
||||
return (
|
||||
personaId !== undefined &&
|
||||
personas &&
|
||||
personas[personaId] !== undefined
|
||||
);
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.time_created).getTime() -
|
||||
new Date(a.time_created).getTime()
|
||||
)
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
showDropdown &&
|
||||
!(event.target as Element).closest(".notification-dropdown")
|
||||
) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [showDropdown]);
|
||||
return (
|
||||
<div className="w-full">
|
||||
<button
|
||||
onClick={navigateToDropdown}
|
||||
className="absolute right-2 text-background-600 hover:text-background-900 transition-colors duration-150 ease-in-out rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
aria-label="Back"
|
||||
>
|
||||
<XIcon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{notifications && notifications.length > 0 ? (
|
||||
sortedNotifications.length > 0 && personas ? (
|
||||
sortedNotifications
|
||||
.filter(
|
||||
(notification) =>
|
||||
notification.notif_type === NotificationType.PERSONA_SHARED
|
||||
)
|
||||
.map((notification) => {
|
||||
const persona = notification.additional_data?.persona_id
|
||||
? personas[notification.additional_data.persona_id]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={notification.id}
|
||||
className="w-72 px-4 py-3 border-b last:border-b-0 hover:bg-gray-50 transition duration-150 ease-in-out"
|
||||
>
|
||||
<div className="flex items-start">
|
||||
{persona && (
|
||||
<div className="mt-2 flex-shrink-0 mr-3">
|
||||
<AssistantIcon assistant={persona} size="small" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-grow">
|
||||
<p className="font-semibold text-sm text-gray-800">
|
||||
New Assistant Shared: {persona?.name}
|
||||
</p>
|
||||
{persona?.description && (
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{persona.description}
|
||||
</p>
|
||||
)}
|
||||
{persona && (
|
||||
<div className="mt-2">
|
||||
{persona.tools.length > 0 && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Tools:{" "}
|
||||
{persona.tools
|
||||
.map((tool) => tool.name)
|
||||
.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
{persona.document_sets.length > 0 && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Document Sets:{" "}
|
||||
{persona.document_sets
|
||||
.map((set) => set.name)
|
||||
.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
{persona.llm_model_version_override && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Model: {persona.llm_model_version_override}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-2 space-x-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleAssistantShareAcceptance(notification, persona!)
|
||||
}
|
||||
className="px-3 py-1 text-sm font-medium text-blue-600 hover:text-blue-800 transition duration-150 ease-in-out"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
onClick={() => dismissNotification(notification.id)}
|
||||
className="px-3 py-1 text-sm font-medium text-gray-600 hover:text-gray-800 transition duration-150 ease-in-out"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-20 justify-center items-center w-72">
|
||||
<Spinner size={20} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="px-4 py-3 text-center text-gray-600">
|
||||
No new notifications
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
119
web/src/components/context/AssistantsContext.tsx
Normal file
119
web/src/components/context/AssistantsContext.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
import React, { createContext, useState, useContext, useMemo } from "react";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import {
|
||||
classifyAssistants,
|
||||
orderAssistantsForUser,
|
||||
getUserCreatedAssistants,
|
||||
} from "@/lib/assistants/utils";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
|
||||
interface AssistantsContextProps {
|
||||
assistants: Persona[];
|
||||
visibleAssistants: Persona[];
|
||||
hiddenAssistants: Persona[];
|
||||
finalAssistants: Persona[];
|
||||
ownedButHiddenAssistants: Persona[];
|
||||
refreshAssistants: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AssistantsContext = createContext<AssistantsContextProps | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export const AssistantsProvider: React.FC<{
|
||||
children: React.ReactNode;
|
||||
initialAssistants: Persona[];
|
||||
hasAnyConnectors: boolean;
|
||||
hasImageCompatibleModel: boolean;
|
||||
}> = ({
|
||||
children,
|
||||
initialAssistants,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
}) => {
|
||||
const [assistants, setAssistants] = useState<Persona[]>(
|
||||
initialAssistants || []
|
||||
);
|
||||
const { user } = useUser();
|
||||
|
||||
const refreshAssistants = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/persona", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to fetch assistants");
|
||||
let assistants: Persona[] = await response.json();
|
||||
if (!hasImageCompatibleModel) {
|
||||
assistants = assistants.filter(
|
||||
(assistant) =>
|
||||
!assistant.tools.some(
|
||||
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (!hasAnyConnectors) {
|
||||
assistants = assistants.filter(
|
||||
(assistant) => assistant.num_chunks === 0
|
||||
);
|
||||
}
|
||||
setAssistants(assistants);
|
||||
} catch (error) {
|
||||
console.error("Error refreshing assistants:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
visibleAssistants,
|
||||
hiddenAssistants,
|
||||
finalAssistants,
|
||||
ownedButHiddenAssistants,
|
||||
} = useMemo(() => {
|
||||
const { visibleAssistants, hiddenAssistants } = classifyAssistants(
|
||||
user,
|
||||
assistants
|
||||
);
|
||||
|
||||
const finalAssistants = user
|
||||
? orderAssistantsForUser(visibleAssistants, user)
|
||||
: visibleAssistants;
|
||||
|
||||
const ownedButHiddenAssistants = getUserCreatedAssistants(
|
||||
user,
|
||||
hiddenAssistants
|
||||
);
|
||||
|
||||
return {
|
||||
visibleAssistants,
|
||||
hiddenAssistants,
|
||||
finalAssistants,
|
||||
ownedButHiddenAssistants,
|
||||
};
|
||||
}, [user, assistants]);
|
||||
|
||||
return (
|
||||
<AssistantsContext.Provider
|
||||
value={{
|
||||
assistants,
|
||||
visibleAssistants,
|
||||
hiddenAssistants,
|
||||
finalAssistants,
|
||||
ownedButHiddenAssistants,
|
||||
refreshAssistants,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AssistantsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAssistants = (): AssistantsContextProps => {
|
||||
const context = useContext(AssistantsContext);
|
||||
if (!context) {
|
||||
throw new Error("useAssistants must be used within an AssistantsProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
@ -7,12 +7,12 @@ import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
|
||||
interface ChatContextProps {
|
||||
chatSessions: ChatSession[];
|
||||
availableSources: ValidSources[];
|
||||
availableDocumentSets: DocumentSet[];
|
||||
availableAssistants: Persona[];
|
||||
availableTags: Tag[];
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
folders: Folder[];
|
||||
@ -29,7 +29,10 @@ const ChatContext = createContext<ChatContextProps | undefined>(undefined);
|
||||
// We use Omit to exclude 'refreshChatSessions' from the value prop type
|
||||
// because we're defining it within the component
|
||||
export const ChatProvider: React.FC<{
|
||||
value: Omit<ChatContextProps, "refreshChatSessions">;
|
||||
value: Omit<
|
||||
ChatContextProps,
|
||||
"refreshChatSessions" | "refreshAvailableAssistants"
|
||||
>;
|
||||
children: React.ReactNode;
|
||||
}> = ({ value, children }) => {
|
||||
const [chatSessions, setChatSessions] = useState(value?.chatSessions || []);
|
||||
@ -47,7 +50,11 @@ export const ChatProvider: React.FC<{
|
||||
|
||||
return (
|
||||
<ChatContext.Provider
|
||||
value={{ ...value, chatSessions, refreshChatSessions }}
|
||||
value={{
|
||||
...value,
|
||||
chatSessions,
|
||||
refreshChatSessions,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
|
@ -959,6 +959,29 @@ export const SearchIcon = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const BellIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M12 1.25A7.75 7.75 0 0 0 4.25 9v.704a3.53 3.53 0 0 1-.593 1.958L2.51 13.385c-1.334 2-.316 4.718 2.003 5.35c.755.206 1.517.38 2.284.523l.002.005C7.567 21.315 9.622 22.75 12 22.75s4.433-1.435 5.202-3.487l.002-.005a28.472 28.472 0 0 0 2.284-.523c2.319-.632 3.337-3.35 2.003-5.35l-1.148-1.723a3.53 3.53 0 0 1-.593-1.958V9A7.75 7.75 0 0 0 12 1.25Zm3.376 18.287a28.46 28.46 0 0 1-6.753 0c.711 1.021 1.948 1.713 3.377 1.713c1.429 0 2.665-.692 3.376-1.713ZM5.75 9a6.25 6.25 0 1 1 12.5 0v.704c0 .993.294 1.964.845 2.79l1.148 1.723a2.02 2.02 0 0 1-1.15 3.071a26.96 26.96 0 0 1-14.187 0a2.021 2.021 0 0 1-1.15-3.07l1.15-1.724a5.03 5.03 0 0 0 .844-2.79V9Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LightSettingsIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
@ -709,7 +709,6 @@ export const SearchSection = ({
|
||||
reset={() => setQuery("")}
|
||||
toggleSidebar={toggleSidebar}
|
||||
page="search"
|
||||
user={user}
|
||||
/>
|
||||
<div className="w-full flex">
|
||||
<div
|
||||
|
@ -47,6 +47,8 @@ interface FetchChatDataResult {
|
||||
finalDocumentSidebarInitialWidth?: number;
|
||||
shouldShowWelcomeModal: boolean;
|
||||
userInputPrompts: InputPrompt[];
|
||||
hasAnyConnectors: boolean;
|
||||
hasImageCompatibleModel: boolean;
|
||||
}
|
||||
|
||||
export async function fetchChatData(searchParams: {
|
||||
@ -251,5 +253,7 @@ export async function fetchChatData(searchParams: {
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
userInputPrompts,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user